/* 会话树接入列表的判据(2026-10-04)。 ★ 这批盯的是**接线的正确性**,不是「有没有缩进」: 1. depth 来源正确 —— 必须来自 /sessions/tree,且**按 session_id 连接**; 绝不能把树的节点直接当列表渲染(那是「我可见的全部会话」, 而列表是「我参与过的」—— 两者量级差两个数量级)。 2. 树不可用时**退化成平铺**,不报错也不空白。 3. 换账号/登出必须清 depthBySession —— session_id 不含账号维度, 不清就是把 A 账号的层级套到 B 账号的列表上(越界,不是显示瑕疵)。 4. 缩进有上限 —— 320px 侧栏里无限缩进会把地址 truncate 到读不出来。 源码每次现读(不模块级缓存):判据跑的是**当前文件**, 否则改了不重读会给出假绿。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const here = dirname(fileURLToPath(import.meta.url)); const src = p => readFileSync(join(here, '..', 'src', p), 'utf8'); /* ★ 两个视图都要接缩进 —— 否则「切一下视图,线索树就没了」。 用户 2026-10-04 的原话是「形成/展示为树结构」,只在一个视图里成立不算。 */ test('列表与卡片两个视图都接了缩进', () => { const panel = src('components/ContactPanel.tsx'); const card = src('components/WorkCard.tsx'); assert.match(panel, / { const client = src('api/client.ts'); assert.match(client, /export async function listSessionTree/, '缺 listSessionTree'); assert.match(client, /'\/sessions\/tree'/, '树端点没接上'); // 树与 contacts 必须各自请求,不得把树塞进 listContacts 的返回 assert.match(client, /listContacts[\s\S]{0,200}?\/contacts\?archived=/, 'listContacts 的口径被改了'); }); test('ContactPanel 按 depth 缩进且有上限', () => { const panel = src('components/ContactPanel.tsx'); assert.match(panel, /Math\.min\(Math\.max\(depth, 0\), (\d+)\)/, 'ContactRow 没按 depth 缩进,或没有上限'); const m = panel.match(/Math\.min\(Math\.max\(depth, 0\), (\d+)\)/); assert.ok(Number(m[1]) <= 4, `缩进上限 ${m[1]} 太大:320px 侧栏里会把地址 truncate 到读不出层级`); // 连接线:与 ThreadView 同一套视觉语言 assert.match(panel, /border-l border-b/, '缺左框连接线,与 ThreadView 不一致'); }); test('树不可用时退化为平铺,不让列表崩', () => { const store = src('stores/contactStore.ts'); // 树失败必须被吞掉,且不影响 contacts 已 set 的结果 assert.match(store, /catch\s*\{\s*set\(\{\s*treeAvailable:\s*false\s*\}\)/, '树拉取失败未降级 —— 一次树服务故障不该让整个联系人列表报错'); // contacts 的 set 必须在树的 try 之前、且提前 return const idxContacts = store.indexOf('set({ contacts: contacts || [], loading: false })'); const idxTree = store.indexOf('api.listSessionTree()'); assert.ok(idxContacts > 0 && idxTree > idxContacts, '树拉取必须排在联系人之后 —— 否则树慢 200ms 会让整个列表多等 200ms'); }); test('换账号必须清 depthBySession(跨账号越界)', () => { const store = src('stores/contactStore.ts'); // reset / clear 分支 const clears = store.match(/depthBySession:\s*\{\}/g) || []; assert.ok(clears.length >= 2, `depthBySession 只清了 ${clears.length} 处(应在初始 state 与 reset 各一处)` + ' —— session_id 不含账号维度,不清就会把 A 账号的层级套到 B 账号'); // 初始 state 也必须有 const initIdx = store.indexOf('archivedContacts: [],'); assert.ok(initIdx > 0 && store.indexOf('depthBySession: {}', initIdx) > initIdx, '初始 state 里没有 depthBySession'); });