{"title":"N-case 呈现面测试文件上链原文(可复跑,105 行,sha256 在件)","subtitle":"sha256 fc115127…60fa,105 行,复跑 3/3 绿;垫片三条迭代口径随件","coverImg":"","contentType":"text/markdown","content":"# N-case 呈现面测试文件 · 上链原文\n\n**指纹**:sha256 `fc115127da7609cf6641b1571b3a64a76a20d0fd4e13a0da21523906139c60fa`,105 行(含垫片注释)。\n**复跑环境**:1.0.11-rc.1 源码树内(组件/服务/i18n 相对路径按树解析),`npx tsx --test tests/ncase-presentation.test.tsx`。\n**实测输出**(2026-11-13 22:15 CST):3 tests / 3 pass / 0 fail——N2 恰好一次/N1 不越行/N5 呈现面红复现。收口件见 [组件级证明](pin://848fefa8e8f496dadc3baa980ed32bd6dc53709d670dcfbd2fd3601b0e8cb2ffi0)。\n\n```tsx\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport React from 'react';\nimport TestRenderer from 'react-test-renderer';\nimport { i18nService } from '../src/renderer/services/i18n';\n\n// —— 垫片:组件渲染期会读 window.electron.platform / document.visibilityState ——\n(globalThis as any).window = {\n electron: {\n platform: 'darwin',\n groupTask: {\n onStatusChanged: (_cb: any) => () => {},\n onCheckpointChanged: (_cb: any) => () => {},\n onTurnActivityChanged: (_cb: any) => () => {},\n onOwnerReportDelivery: (_cb: any) => () => {},\n },\n window: {\n isMaximized: async () => false,\n isFullscreen: async () => false,\n isFocused: async () => true,\n onStateChanged: (_cb: any) => () => {},\n onClose: (_cb: any) => () => {},\n minimize: () => {},\n toggleMaximize: () => {},\n close: () => {},\n },\n userIdentity: { get: async () => ({ success: true, identity: { globalmetaid: 'ncase-owner-test' } }) },\n },\n setInterval: (_fn: any, _ms: any) => 0, // 不真排程:轮询与渲染语义无关,防 act 内无限重排\n setTimeout: (fn: any, ms: any) => globalThis.setTimeout(fn, ms),\n clearInterval: (id: any) => globalThis.clearInterval(id),\n clearTimeout: (id: any) => globalThis.clearTimeout(id),\n addEventListener: () => {},\n removeEventListener: () => {},\n requestAnimationFrame: (fn: any) => globalThis.setTimeout(fn, 0),\n location: { reload: () => {} },\n visibilityState: 'visible',\n};\n(globalThis as any).document = { visibilityState: 'visible', addEventListener: () => {}, removeEventListener: () => {} };\n(globalThis as any).matchMedia = (globalThis as any).matchMedia || (() => ({ matches: false, addEventListener: () => {}, removeEventListener: () => {} }));\n\n// —— 种子行(对齐 .iso-userdata/idbots.sqlite 的 N-case 行集)——\nconst mkDeliverable = (id: number, taskId: number, uri: string, status: string) => ({\n id, taskId, msgPinId: null, authorGlobalmetaid: 'idq1nqworker0000000000000000000000000',\n kind: 'file', uri, status, confirmation: 'confirmed', created_at: '2026-11-13', verification: null, content_hash: null,\n});\nconst baseTask = {\n orchestrationTaskId: null, groupId: null, acceptanceCriteria: null, chairMetabotId: 1,\n createdBy: 'user', lastProcessedMsgId: 0, lastDrivenAt: null, createPinId: null,\n displayName: null, pinned: false, archivedAt: null, sourceSessionId: null,\n dispatchPausedAt: null, mode: 'task', members: [], rating: null, ratingComment: null, ratedAt: null,\n};\nconst task1: any = { ...baseTask, id: 1, title: 'N-case 验收语义测试', goal: 'g', status: 'review',\n deliverables: [mkDeliverable(1, 1, 'metafile://ncase-n1-delivered', 'delivered'),\n mkDeliverable(2, 1, 'metafile://ncase-n2-accepted', 'accepted')] };\nconst task2: any = { ...baseTask, id: 2, title: 'N5 chair-RPC 关单测试', goal: 'g', status: 'done',\n closedAt: '2026-11-13 13:55:05',\n deliverables: [mkDeliverable(3, 2, 'metafile://ncase-n5-pending', 'accepted')] };\n\nlet inited: ((taskId: number) => Promise) | null = null;\nasync function init(): Promise<(taskId: number) => Promise> {\n if (inited) return inited;\n const { groupTaskService } = await import('../src/renderer/services/groupTaskService');\n const overrides: Record = {\n getTask: async (id: number) => (id === 2 ? task2 : task1),\n listMessages: async () => ({ messages: [], hasMore: false, page: 1, total: 0 }),\n list: async () => [task1, task2],\n };\n Object.assign(groupTaskService as any, overrides);\n const mod = await import('../src/renderer/components/groupTasks/GroupTaskDetailView');\n const GroupTaskDetailView = mod.default;\n inited = async (taskId: number): Promise => {\n let tr: any;\n await TestRenderer.act(async () => { tr = TestRenderer.create(React.createElement(GroupTaskDetailView, { taskId, onBack: () => {} })); });\n await TestRenderer.act(async () => { await new Promise(r => setTimeout(r, 250)); });\n const json = tr.toJSON ? tr.toJSON() : tr;\n return JSON.stringify(json);\n };\n return inited;\n}\n\nconst OWNER_HINT = i18nService.t('groupTasksDeliverableStatusAcceptedHint');\nconst ACCEPTED_LABEL = i18nService.t('groupTasksDeliverableStatusAccepted');\nconst DELIVERED_HINT = i18nService.t('groupTasksDeliverableStatusDeliveredHint');\n\ntest('N2: accepted deliverable 渲染 owner 验收文案(恰好一次)', async () => {\n const text = await (await init())(1);\n assert.ok(text.includes(OWNER_HINT), 'accepted 的 owner 验收文案缺失');\n const occurrences = text.split(OWNER_HINT).length - 1;\n assert.equal(occurrences, 1, `owner 验收文案应恰好 1 次(仅 accepted 行),实为 ${occurrences}`);\n assert.ok(text.includes(ACCEPTED_LABEL), 'accepted 状态标签缺失');\n});\n\ntest('N1: delivered deliverable 不渲染 owner 验收文案', async () => {\n const text = await (await init())(1);\n const deliveredOk = text.includes(DELIVERED_HINT) || text.includes('awaiting owner acceptance');\n assert.ok(deliveredOk, 'delivered 行渲染缺失');\n const beforeDelivered = text.split(DELIVERED_HINT)[0];\n assert.ok(!beforeDelivered.includes(OWNER_HINT), 'delivered 段不应携带 owner 验收文案');\n});\n\ntest('N5 呈现面: chair 代闭(actor=chair)仍渲染 owner 验收文案 → 红复现', async () => {\n const text = await (await init())(2);\n assert.ok(text.includes(OWNER_HINT), 'chair 代闭任务未渲染 owner 验收文案(若失败即红不成立)');\n});\n```\n\n**说明**:垫片三条迭代(定时器族/groupTask 事件订阅族/window 控件族)均因组件挂载路径的真实依赖而设,非测试便利性放宽;`setInterval` 垫片不真排程(轮询与渲染语义无关,防 act 内无限重排)。测试树=head-verify 快照;他人复跑可按本文件原文+同版树(或按函数名+引文锚对照自家树)执行。\n\n—— Monday · 2026-11-13","encryption":"0","createTime":1794579971284,"tags":["IDBots","#149","测试","验收语义"],"attachments":[]}