fix(chat): rename default thread label to general (#3319)

This commit is contained in:
Steven Enamakel
2026-06-03 22:23:46 -04:00
committed by GitHub
parent c3b9b2d665
commit 0fa0c3a1a0
25 changed files with 132 additions and 67 deletions
+1 -1
View File
@@ -1678,7 +1678,7 @@ const messages: TranslationMap = {
'common.enable': 'تفعيل',
'chat.safetyTimeout': 'لا استجابة من الوكيل بعد دقيقتين. حاول مرة أخرى أو تحقق من اتصالك.',
'chat.filter.all': 'الكل',
'chat.filter.work': 'العمل',
'chat.filter.general': 'عام',
'chat.filter.briefing': 'الإحاطة',
'chat.filter.notification': 'الإشعار',
'chat.filter.workers': 'العمال',
+1 -1
View File
@@ -1712,7 +1712,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'২ মিনিট পরেও এজেন্টের কোনো সাড়া নেই। আবার চেষ্টা করুন বা সংযোগ পরীক্ষা করুন।',
'chat.filter.all': 'সব',
'chat.filter.work': 'কাজ',
'chat.filter.general': 'সাধারণ',
'chat.filter.briefing': 'ব্রিফিং',
'chat.filter.notification': 'বিজ্ঞপ্তি',
'chat.filter.workers': 'ওয়ার্কার',
+1 -1
View File
@@ -1757,7 +1757,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'Keine Antwort vom Agenten nach 2 Minuten. Versuche es erneut oder prüfe deine Verbindung.',
'chat.filter.all': 'Alle',
'chat.filter.work': 'Arbeit',
'chat.filter.general': 'Allgemein',
'chat.filter.briefing': 'Briefing',
'chat.filter.notification': 'Benachrichtigung',
'chat.filter.workers': 'Arbeiter',
+1 -1
View File
@@ -1960,7 +1960,7 @@ const en: TranslationMap = {
'chat.safetyTimeout':
'No response from the agent after 2 minutes. Try again or check your connection.',
'chat.filter.all': 'All',
'chat.filter.work': 'Work',
'chat.filter.general': 'General',
'chat.filter.briefing': 'Briefing',
'chat.filter.notification': 'Notification',
'chat.filter.workers': 'Workers',
+1 -1
View File
@@ -1751,7 +1751,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'Sin respuesta del agente después de 2 minutos. Intenta de nuevo o verifica tu conexión.',
'chat.filter.all': 'Todos',
'chat.filter.work': 'Trabajo',
'chat.filter.general': 'General',
'chat.filter.briefing': 'Resumen',
'chat.filter.notification': 'Notificación',
'chat.filter.workers': 'Trabajadores',
+1 -1
View File
@@ -1755,7 +1755,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
"Aucune réponse de l'agent après 2 minutes. Réessaie ou vérifie ta connexion.",
'chat.filter.all': 'Tous',
'chat.filter.work': 'Travail',
'chat.filter.general': 'Général',
'chat.filter.briefing': 'Briefing',
'chat.filter.notification': 'Notification',
'chat.filter.workers': 'Travailleurs',
+1 -1
View File
@@ -1713,7 +1713,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'2 मिनट बाद भी एजेंट से कोई जवाब नहीं मिला। दोबारा कोशिश करें या अपना कनेक्शन चेक करें।',
'chat.filter.all': 'सभी',
'chat.filter.work': 'वर्क',
'chat.filter.general': 'सामान्य',
'chat.filter.briefing': 'ब्रीफिंग',
'chat.filter.notification': 'नोटिफिकेशन',
'chat.filter.workers': 'वर्कर्स',
+1 -1
View File
@@ -1715,7 +1715,7 @@ const messages: TranslationMap = {
'common.enable': 'Aktifkan',
'chat.safetyTimeout': 'Tidak ada respons dari agen setelah 2 menit. Coba lagi atau cek koneksi.',
'chat.filter.all': 'Semua',
'chat.filter.work': 'Kerja',
'chat.filter.general': 'Umum',
'chat.filter.briefing': 'Ringkasan',
'chat.filter.notification': 'Notifikasi',
'chat.filter.workers': 'Worker',
+1 -1
View File
@@ -1741,7 +1741,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
"Nessuna risposta dall'agente dopo 2 minuti. Riprova o controlla la connessione.",
'chat.filter.all': 'Tutti',
'chat.filter.work': 'Lavoro',
'chat.filter.general': 'Generale',
'chat.filter.briefing': 'Briefing',
'chat.filter.notification': 'Notifica',
'chat.filter.workers': 'Worker',
+1 -1
View File
@@ -1693,7 +1693,7 @@ const messages: TranslationMap = {
'common.enable': '활성화',
'chat.safetyTimeout': '2분 후에도 에이전트의 응답이 없습니다. 다시 시도하거나 연결을 확인하세요.',
'chat.filter.all': '전체',
'chat.filter.work': '업무',
'chat.filter.general': '일반',
'chat.filter.briefing': '브리핑',
'chat.filter.notification': '알림',
'chat.filter.workers': '워커',
+1 -1
View File
@@ -1734,7 +1734,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'Brak odpowiedzi agenta po 2 minutach. Spróbuj ponownie lub sprawdź połączenie.',
'chat.filter.all': 'Wszystkie',
'chat.filter.work': 'Praca',
'chat.filter.general': 'Ogólne',
'chat.filter.briefing': 'Briefing',
'chat.filter.notification': 'Powiadomienia',
'chat.filter.workers': 'Workery',
+1 -1
View File
@@ -1749,7 +1749,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'Nenhuma resposta do agente após 2 minutos. Tente novamente ou verifique sua conexão.',
'chat.filter.all': 'Todos',
'chat.filter.work': 'Trabalho',
'chat.filter.general': 'Geral',
'chat.filter.briefing': 'Resumo',
'chat.filter.notification': 'Notificação',
'chat.filter.workers': 'Trabalhadores',
+1 -1
View File
@@ -1726,7 +1726,7 @@ const messages: TranslationMap = {
'chat.safetyTimeout':
'Агент не ответил в течение 2 минут. Попробуй снова или проверь соединение.',
'chat.filter.all': 'Все',
'chat.filter.work': 'Работа',
'chat.filter.general': 'Общее',
'chat.filter.briefing': 'Брифинг',
'chat.filter.notification': 'Уведомление',
'chat.filter.workers': 'Воркеры',
+1 -1
View File
@@ -1619,7 +1619,7 @@ const messages: TranslationMap = {
'common.enable': '启用',
'chat.safetyTimeout': '助手 2 分钟内未响应。请重试或检查你的连接。',
'chat.filter.all': '全部',
'chat.filter.work': '工作',
'chat.filter.general': '常规',
'chat.filter.briefing': '简报',
'chat.filter.notification': '通知',
'chat.filter.workers': '工作线程',
+14 -10
View File
@@ -96,7 +96,11 @@ import {
formatResetTime,
getInlineCompletionSuffix,
} from './conversations/utils/format';
import { isThreadVisibleInTab, WORKERS_TAB_VALUE } from './conversations/utils/threadFilter';
import {
GENERAL_TAB_VALUE,
isThreadVisibleInTab,
WORKERS_TAB_VALUE,
} from './conversations/utils/threadFilter';
// Chat uses the reasoning model; `agentic-v1` is reserved for sub-agents
// that execute tool calls, not the primary user-facing conversation.
@@ -213,7 +217,7 @@ const Conversations = ({
const [isTranscribing, setIsTranscribing] = useState(false);
const [voiceStatus, setVoiceStatus] = useState<string | null>(null);
const [isPlayingReply, setIsPlayingReply] = useState(false);
const [selectedLabel, setSelectedLabel] = useState<string>('all');
const [selectedLabel, setSelectedLabel] = useState<string>(GENERAL_TAB_VALUE);
const [inlineSuggestionValue, setInlineSuggestionValue] = useState('');
const [sendError, setSendError] = useState<ChatSendError | null>(null);
const [attachError, setAttachError] = useState<ChatSendError | null>(null);
@@ -399,11 +403,9 @@ const Conversations = ({
.then(data => {
if (cancelled) return;
const threadStateForSelect = store.getState().thread;
// Worker/subagent threads are hidden from the conversation list
// (see tinyhumansai/openhuman#1624). Match the sidebar filter here so
// initial/resume selection can't auto-pick a hidden thread and leave
// the UI showing a thread that isn't in the list.
const visibleThreads = data.threads.filter(t => !t.parentThreadId);
// Match the sidebar's default General filter here so initial/resume
// selection can't auto-pick a thread hidden by the selected tab.
const visibleThreads = data.threads.filter(t => isThreadVisibleInTab(t, GENERAL_TAB_VALUE));
if (visibleThreads.length > 0) {
// Prefer the thread the user was last viewing (persisted across
// reloads via redux-persist on the `thread` slice). Only fall
@@ -1248,11 +1250,13 @@ const Conversations = ({
// transcript is the inline `WorkerThreadRefCard` inside the parent.
const labelTabs = [
{ label: t('chat.filter.all'), value: 'all' },
{ label: t('chat.filter.work'), value: 'work' },
{ label: t('chat.filter.general'), value: GENERAL_TAB_VALUE },
{ label: t('chat.filter.briefing'), value: 'briefing' },
{ label: t('chat.filter.notification'), value: 'notification' },
{ label: t('chat.filter.workers'), value: WORKERS_TAB_VALUE },
];
const selectedLabelDisplay =
labelTabs.find(tab => tab.value === selectedLabel)?.label ?? selectedLabel;
const isSidebar = variant === 'sidebar';
const effectiveShowSidebar = showSidebar;
@@ -1321,7 +1325,7 @@ const Conversations = ({
items={labelTabs}
selected={selectedLabel}
onChange={setSelectedLabel}
containerClassName="flex gap-1 overflow-x-auto py-1 scrollbar-hide"
containerClassName="flex flex-wrap gap-1 py-1"
/>
</div>
<div className="flex-1 overflow-y-auto">
@@ -1331,7 +1335,7 @@ const Conversations = ({
? t('chat.noThreads')
: selectedLabel === WORKERS_TAB_VALUE
? t('chat.noWorkerThreads')
: t('chat.noLabelThreads').replace('{label}', selectedLabel)}
: t('chat.noLabelThreads').replace('{label}', selectedLabelDisplay)}
</p>
) : (
sortedThreads.map(thread => (
@@ -181,7 +181,7 @@ function makeThread(overrides: Partial<Thread> = {}): Thread {
messageCount: 0,
lastMessageAt: '2026-01-01T00:00:00.000Z',
createdAt: '2026-01-01T00:00:00.000Z',
labels: [],
labels: ['general'],
...overrides,
};
}
@@ -322,13 +322,14 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
});
// Covers line 941 empty branch
it('shows "No threads yet" when thread list is empty', async () => {
it('shows "No threads yet" when All is selected and the thread list is empty', async () => {
await act(async () => {
await renderConversations({ thread: emptyThreadState });
});
// Sidebar is hidden by default — open it first.
await openSidebar();
fireEvent.click(screen.getByRole('tab', { name: 'All' }));
expect(screen.getByText('No threads yet')).toBeInTheDocument();
});
@@ -1266,7 +1267,7 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
//
// The tab set is fixed so categories do not disappear when the thread list
// is empty, and the active-filter state remains unambiguous.
it('renders all four fixed category tabs with stable labels', async () => {
it('renders all fixed category tabs with stable labels', async () => {
await act(async () => {
await renderConversations({ thread: emptyThreadState });
});
@@ -1274,14 +1275,16 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
// Sidebar is hidden by default — open it first.
await openSidebar();
// All four tabs must be present regardless of thread count.
// All tabs must be present regardless of thread count.
expect(screen.getByRole('tab', { name: 'All' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Work' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'General' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Briefing' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Notification' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Workers' })).toBeInTheDocument();
expect(screen.getByRole('tablist')).toHaveClass('flex-wrap');
});
it('starts with the "All" tab selected', async () => {
it('starts with the "General" tab selected', async () => {
await act(async () => {
await renderConversations({ thread: emptyThreadState });
});
@@ -1289,8 +1292,8 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
// Sidebar is hidden by default — open it first.
await openSidebar();
expect(screen.getByRole('tab', { name: 'All' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByRole('tab', { name: 'Work' })).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tab', { name: 'General' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByRole('tab', { name: 'All' })).toHaveAttribute('aria-selected', 'false');
});
it('shows "No threads yet" placeholder when All tab is active and list is empty', async () => {
@@ -1301,6 +1304,8 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
// Sidebar is hidden by default — open it first.
await openSidebar();
fireEvent.click(screen.getByRole('tab', { name: 'All' }));
expect(screen.getByText('No threads yet')).toBeInTheDocument();
});
@@ -1312,10 +1317,10 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
// Sidebar is hidden by default — open it first.
await openSidebar();
fireEvent.click(screen.getByRole('tab', { name: 'Work' }));
fireEvent.click(screen.getByRole('tab', { name: 'General' }));
await waitFor(() => {
expect(screen.getByText(/"work" threads/i)).toBeInTheDocument();
expect(screen.getByText(/"General" threads/i)).toBeInTheDocument();
});
});
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { Thread } from '../../../types/thread';
import { isThreadVisibleInTab, WORKERS_TAB_VALUE } from './threadFilter';
import { GENERAL_TAB_VALUE, isThreadVisibleInTab, WORKERS_TAB_VALUE } from './threadFilter';
// Issue #1624: this is the pure rule that backs the sidebar
// `filteredThreads` memo + the `Workers` tab. The tests pin both halves
@@ -35,20 +35,25 @@ describe('isThreadVisibleInTab', () => {
});
});
describe('label-scoped tabs (work, briefing, notification, )', () => {
describe('label-scoped tabs (general, briefing, notification, ...)', () => {
it('keeps a non-worker thread that carries the matching label', () => {
const t = thread({ id: 'a', labels: ['work', 'urgent'] });
expect(isThreadVisibleInTab(t, 'work')).toBe(true);
const t = thread({ id: 'a', labels: [GENERAL_TAB_VALUE, 'urgent'] });
expect(isThreadVisibleInTab(t, GENERAL_TAB_VALUE)).toBe(true);
});
it('keeps legacy work-labeled threads in the General tab', () => {
const t = thread({ id: 'legacy', labels: ['work', 'urgent'] });
expect(isThreadVisibleInTab(t, GENERAL_TAB_VALUE)).toBe(true);
});
it('drops a non-worker thread that does not carry the matching label', () => {
const t = thread({ id: 'a', labels: ['briefing'] });
expect(isThreadVisibleInTab(t, 'work')).toBe(false);
expect(isThreadVisibleInTab(t, GENERAL_TAB_VALUE)).toBe(false);
});
it('still hides worker threads even when the label would otherwise match', () => {
const t = thread({ id: 'w', parentThreadId: 'p', labels: ['work'] });
expect(isThreadVisibleInTab(t, 'work')).toBe(false);
const t = thread({ id: 'w', parentThreadId: 'p', labels: [GENERAL_TAB_VALUE] });
expect(isThreadVisibleInTab(t, GENERAL_TAB_VALUE)).toBe(false);
});
it('treats threads with no labels array as not matching', () => {
@@ -13,6 +13,8 @@ import type { Thread } from '../../../types/thread';
* future rename can never silently desync the three.
*/
export const WORKERS_TAB_VALUE = 'workers';
export const GENERAL_TAB_VALUE = 'general';
export const LEGACY_GENERAL_LABEL = 'work';
/**
* Pure, side-effect-free thread filter shared between
@@ -30,12 +32,18 @@ export const WORKERS_TAB_VALUE = 'workers';
* orchestrator-spawned background work.
* - Within the non-Workers tabs, `selectedLabel === 'all'` keeps every
* non-worker thread; any other value scopes by the existing thread
* `labels[]` array (`work`, `briefing`, `notification`, …).
* `labels[]` array (`general`, `briefing`, `notification`, …). The
* General tab also accepts legacy `work` labels from older cores.
*/
export function isThreadVisibleInTab(thread: Thread, selectedLabel: string): boolean {
const isWorker = Boolean(thread.parentThreadId);
if (selectedLabel === WORKERS_TAB_VALUE) return isWorker;
if (isWorker) return false;
if (selectedLabel === 'all') return true;
if (selectedLabel === GENERAL_TAB_VALUE) {
return Boolean(
thread.labels?.includes(GENERAL_TAB_VALUE) || thread.labels?.includes(LEGACY_GENERAL_LABEL)
);
}
return Boolean(thread.labels?.includes(selectedLabel));
}
@@ -13,6 +13,7 @@ const SECRET_WORD = 'XYZZY';
const FIRST_PROMPT = `Remember: the secret word is ${SECRET_WORD}`;
const SECOND_PROMPT = 'What was the secret word?';
const TURN_TWO_CANARY = `canary-memory-m1n2o3-${SECRET_WORD}`;
const FIRST_RESPONSE = `Got it! I will remember that the secret word is ${SECRET_WORD}.`;
interface MockRequest {
method: string;
@@ -126,19 +127,16 @@ test.describe('Chat Conversation History', () => {
page,
}) => {
await resetMock();
await setMockBehavior(
'llmForcedResponses',
JSON.stringify([
{ content: `Got it! I will remember that the secret word is ${SECRET_WORD}.` },
])
);
await setMockBehavior('llmForcedResponses', JSON.stringify([{ content: FIRST_RESPONSE }]));
await setMockBehavior('llmStreamChunkDelayMs', '10');
await openChat(page);
await createNewThread(page);
await sendMessage(page, FIRST_PROMPT);
await expect(page.getByText('Got it!')).toBeVisible({ timeout: 20_000 });
await expect(
page.locator('div.bg-stone-200').filter({ hasText: FIRST_RESPONSE }).first()
).toBeVisible({ timeout: 20_000 });
await resetMock();
await setMockBehavior(
+1 -1
View File
@@ -233,7 +233,7 @@ fn persist_channel_turn(
title,
created_at: created_at.clone(),
parent_thread_id: None,
labels: Some(vec!["work".to_string()]),
labels: Some(vec!["general".to_string()]),
personality_id: None,
},
)?;
+27 -6
View File
@@ -137,6 +137,7 @@ impl ConversationStore {
let root = self.ensure_root()?;
let threads_path = root.join(THREADS_FILENAME);
let now = request.created_at.clone();
let labels = request.labels.clone().map(normalize_labels);
append_jsonl(
&threads_path,
&ThreadLogEntry::Upsert {
@@ -145,7 +146,7 @@ impl ConversationStore {
created_at: request.created_at.clone(),
updated_at: now,
parent_thread_id: request.parent_thread_id.clone(),
labels: request.labels.clone(),
labels,
personality_id: request.personality_id.clone(),
},
)?;
@@ -479,6 +480,7 @@ impl ConversationStore {
.get(thread_id)
.ok_or_else(|| format!("thread {} not found", thread_id))?;
let threads_path = self.ensure_root()?.join(THREADS_FILENAME);
let labels = normalize_labels(labels);
append_jsonl(
&threads_path,
&ThreadLogEntry::Upsert {
@@ -689,7 +691,7 @@ impl ConversationStore {
last_message_at,
created_at: entry.created_at.clone(),
parent_thread_id: entry.parent_thread_id.clone(),
labels: entry.labels.clone(),
labels: normalize_labels(entry.labels.clone()),
personality_id: entry.personality_id.clone(),
}
})
@@ -749,7 +751,7 @@ impl ConversationStore {
last_message_at,
created_at: entry.created_at.clone(),
parent_thread_id: entry.parent_thread_id.clone(),
labels: entry.labels.clone(),
labels: normalize_labels(entry.labels.clone()),
personality_id: entry.personality_id.clone(),
}))
}
@@ -784,13 +786,17 @@ impl ConversationStore {
Some(existing) => (
existing.created_at.clone(),
parent_thread_id.or_else(|| existing.parent_thread_id.clone()),
labels.unwrap_or_else(|| existing.labels.clone()),
labels
.map(normalize_labels)
.unwrap_or_else(|| existing.labels.clone()),
existing.message_count,
existing.last_message_at.clone(),
personality_id.or_else(|| existing.personality_id.clone()),
),
None => {
let inferred = labels.unwrap_or_else(|| infer_labels(&thread_id));
let inferred = labels
.map(normalize_labels)
.unwrap_or_else(|| infer_labels(&thread_id));
(
created_at,
parent_thread_id,
@@ -880,10 +886,25 @@ fn infer_labels(thread_id: &str) -> Vec<String> {
} else if thread_id.starts_with("proactive:") {
vec!["notification".to_string()]
} else {
vec!["work".to_string()]
vec!["general".to_string()]
}
}
fn normalize_labels(labels: Vec<String>) -> Vec<String> {
let mut normalized = Vec::with_capacity(labels.len());
for label in labels {
let next = if label == "work" {
"general".to_string()
} else {
label
};
if !normalized.contains(&next) {
normalized.push(next);
}
}
normalized
}
fn read_jsonl<T>(path: &Path) -> Result<Vec<T>, String>
where
T: for<'de> serde::Deserialize<'de>,
@@ -353,7 +353,7 @@ fn store_handles_labels_and_inference() {
})
.unwrap();
// 4. Default inferred labels (work)
// 4. Default inferred labels (general)
store
.ensure_thread(CreateConversationThread {
parent_thread_id: None,
@@ -365,6 +365,22 @@ fn store_handles_labels_and_inference() {
})
.unwrap();
// 5. Legacy explicit "work" labels normalize into General.
store
.ensure_thread(CreateConversationThread {
parent_thread_id: None,
id: "legacy-work-thread".to_string(),
title: "Legacy Work Chat".to_string(),
created_at: "2026-04-10T12:00:00Z".to_string(),
labels: Some(vec![
"work".to_string(),
"urgent".to_string(),
"work".to_string(),
]),
personality_id: None,
})
.unwrap();
let threads = store.list_threads().unwrap();
{
let t1 = threads.iter().find(|t| t.id == "t1").unwrap();
@@ -383,10 +399,17 @@ fn store_handles_labels_and_inference() {
}
{
let user = threads.iter().find(|t| t.id == "user-thread").unwrap();
assert_eq!(user.labels, vec!["work"]);
assert_eq!(user.labels, vec!["general"]);
}
{
let legacy = threads
.iter()
.find(|t| t.id == "legacy-work-thread")
.unwrap();
assert_eq!(legacy.labels, vec!["general", "urgent"]);
}
// 5. Update labels
// 6. Update labels
store
.update_thread_labels("t1", vec!["updated".to_string()], "2026-04-10T12:05:00Z")
.unwrap();
@@ -396,7 +419,7 @@ fn store_handles_labels_and_inference() {
assert_eq!(t1.labels, vec!["updated"]);
}
// 6. Title update preserves labels
// 7. Title update preserves labels
store
.update_thread_title("t1", "New Title", "2026-04-10T12:06:00Z")
.unwrap();
+2 -2
View File
@@ -342,7 +342,7 @@ fn sample_thread() -> ConversationThread {
last_message_at: "2026-01-01T00:00:00Z".into(),
created_at: "2026-01-01T00:00:00Z".into(),
parent_thread_id: None,
labels: vec!["work".to_string()],
labels: vec!["general".to_string()],
personality_id: None,
}
}
@@ -368,7 +368,7 @@ fn thread_to_summary_preserves_all_fields() {
assert_eq!(summary.message_count, 5);
assert_eq!(summary.last_message_at, "2026-01-01T00:00:00Z");
assert_eq!(summary.created_at, "2026-01-01T00:00:00Z");
assert_eq!(summary.labels, vec!["work".to_string()]);
assert_eq!(summary.labels, vec!["general".to_string()]);
}
#[test]
@@ -333,7 +333,7 @@ async fn round24_threads_public_ops_cover_crud_and_error_branches() {
.value
.data
.expect("labels data");
assert_eq!(relabeled.labels, vec!["work"]);
assert_eq!(relabeled.labels, vec!["general"]);
let empty_title = thread_update_title(UpdateConversationThreadTitleRequest {
thread_id: created.id.clone(),
+5 -4
View File
@@ -1825,7 +1825,8 @@ async fn json_rpc_thread_labels_create_and_update() {
"created thread should have labels=[\"custom\"]"
);
// 2. Update labels on the thread.
// 2. Update labels on the thread. Legacy "work" input normalizes to
// "general" for backward compatibility with older callers.
let update = post_json_rpc(
&rpc_base,
9002,
@@ -1846,8 +1847,8 @@ async fn json_rpc_thread_labels_create_and_update() {
.iter()
.map(|v| v.as_str().unwrap_or(""))
.collect::<Vec<_>>(),
vec!["work", "briefing"],
"updated thread should have labels=[\"work\", \"briefing\"]"
vec!["general", "briefing"],
"updated thread should normalize legacy work label to general"
);
// 3. Verify the updated labels are reflected in threads_list.
@@ -1873,7 +1874,7 @@ async fn json_rpc_thread_labels_create_and_update() {
.iter()
.map(|v| v.as_str().unwrap_or(""))
.collect::<Vec<_>>(),
vec!["work", "briefing"],
vec!["general", "briefing"],
"threads_list must reflect the updated labels"
);