import test from 'node:test'; import assert from 'node:assert/strict'; import React from 'react'; import TestRenderer from 'react-test-renderer'; import { i18nService } from '../src/renderer/services/i18n'; // —— 垫片:组件渲染期会读 window.electron.platform / document.visibilityState —— (globalThis as any).window = { electron: { platform: 'darwin', window: { isMaximized: async () => false, isFullscreen: async () => false, isFocused: async () => true, onStateChanged: (_cb: any) => () => {}, onClose: (_cb: any) => () => {}, minimize: () => {}, toggleMaximize: () => {}, close: () => {}, }, groupTask: { onStatusChanged: (_cb: any) => () => {}, onCheckpointChanged: (_cb: any) => () => {}, onTurnActivityChanged: (_cb: any) => () => {}, onOwnerReportDelivery: (_cb: any) => () => {}, }, userIdentity: { get: async () => ({ success: true, identity: { globalmetaid: 'ncase-owner-test' } }) }, }, setInterval: (_fn: any, _ms: any) => 0, // 不真排程:轮询与渲染语义无关,防 act 内无限重排 setTimeout: (fn: any, ms: any) => globalThis.setTimeout(fn, ms), clearInterval: (id: any) => globalThis.clearInterval(id), clearTimeout: (id: any) => globalThis.clearTimeout(id), addEventListener: () => {}, removeEventListener: () => {}, requestAnimationFrame: (fn: any) => globalThis.setTimeout(fn, 0), location: { reload: () => {} }, visibilityState: 'visible', }; (globalThis as any).document = { visibilityState: 'visible', addEventListener: () => {}, removeEventListener: () => {} }; (globalThis as any).matchMedia = (globalThis as any).matchMedia || (() => ({ matches: false, addEventListener: () => {}, removeEventListener: () => {} })); // —— 种子行(对齐 .iso-userdata/idbots.sqlite 的 N-case 行集)—— const mkDeliverable = (id: number, taskId: number, uri: string, status: string) => ({ id, taskId, msgPinId: null, authorGlobalmetaid: 'idq1nqworker0000000000000000000000000', kind: 'file', uri, status, confirmation: 'confirmed', created_at: '2026-11-13', verification: null, content_hash: null, }); const baseTask = { orchestrationTaskId: null, groupId: null, acceptanceCriteria: null, chairMetabotId: 1, createdBy: 'user', lastProcessedMsgId: 0, lastDrivenAt: null, createPinId: null, displayName: null, pinned: false, archivedAt: null, sourceSessionId: null, dispatchPausedAt: null, mode: 'task', members: [], rating: null, ratingComment: null, ratedAt: null, }; const task1: any = { ...baseTask, id: 1, title: 'N-case 验收语义测试', goal: 'g', status: 'review', deliverables: [mkDeliverable(1, 1, 'metafile://ncase-n1-delivered', 'delivered'), mkDeliverable(2, 1, 'metafile://ncase-n2-accepted', 'accepted')] }; const task2: any = { ...baseTask, id: 2, title: 'N5 chair-RPC 关单测试', goal: 'g', status: 'done', closedAt: '2026-11-13 13:55:05', deliverables: [mkDeliverable(3, 2, 'metafile://ncase-n5-pending', 'accepted')] }; let inited: ((taskId: number) => Promise) | null = null; async function init(): Promise<(taskId: number) => Promise> { if (inited) return inited; const { groupTaskService } = await import('../src/renderer/services/groupTaskService'); const overrides: Record = { getTask: async (id: number) => (id === 2 ? task2 : task1), listMessages: async () => ({ messages: [], hasMore: false, page: 1, total: 0 }), list: async () => [task1, task2], }; Object.assign(groupTaskService as any, overrides); const mod = await import('../src/renderer/components/groupTasks/GroupTaskDetailView'); const GroupTaskDetailView = mod.default; inited = async (taskId: number): Promise => { let tr: any; await TestRenderer.act(async () => { tr = TestRenderer.create(React.createElement(GroupTaskDetailView, { taskId, onBack: () => {} })); }); await TestRenderer.act(async () => { await new Promise(r => setTimeout(r, 250)); }); const json = tr.toJSON ? tr.toJSON() : tr; return JSON.stringify(json); }; return inited; } const OWNER_HINT = i18nService.t('groupTasksDeliverableStatusAcceptedHint'); const ACCEPTED_LABEL = i18nService.t('groupTasksDeliverableStatusAccepted'); const DELIVERED_HINT = i18nService.t('groupTasksDeliverableStatusDeliveredHint'); test('N2: accepted deliverable 渲染 owner 验收文案(恰好一次)', async () => { const text = await (await init())(1); assert.ok(text.includes(OWNER_HINT), 'accepted 的 owner 验收文案缺失'); const occurrences = text.split(OWNER_HINT).length - 1; assert.equal(occurrences, 1, `owner 验收文案应恰好 1 次(仅 accepted 行),实为 ${occurrences}`); assert.ok(text.includes(ACCEPTED_LABEL), 'accepted 状态标签缺失'); }); test('N1: delivered deliverable 不渲染 owner 验收文案', async () => { const text = await (await init())(1); const deliveredOk = text.includes(DELIVERED_HINT) || text.includes('awaiting owner acceptance'); assert.ok(deliveredOk, 'delivered 行渲染缺失'); const beforeDelivered = text.split(DELIVERED_HINT)[0]; assert.ok(!beforeDelivered.includes(OWNER_HINT), 'delivered 段不应携带 owner 验收文案'); }); test('N5 呈现面: chair 代闭(actor=chair)仍渲染 owner 验收文案 → 红复现', async () => { const text = await (await init())(2); assert.ok(text.includes(OWNER_HINT), 'chair 代闭任务未渲染 owner 验收文案(若失败即红不成立)'); });