'use strict'; const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const { createTaskBoardService } = require('../lib/task-board-service'); const { LIFECYCLE_EVENT_TYPES, createTaskBoardLifecycle, normalizeLifecycleEvent, } = require('../lib/task-board-lifecycle'); function clone(value) { return JSON.parse(JSON.stringify(value)); } function createHarness() { const session = { id: 'dynamic-lifecycle', title: '动态列生命周期', agent: 'codexapp', cwd: '/home/cc-web', created: '2026-08-12T00:00:00.000Z', updated: '2026-08-12T00:00:00.000Z', messages: [], }; const sessions = new Map([[session.id, clone(session)]]); let statusConfig = null; let tick = 0; const service = createTaskBoardService({ loadSession(sessionId) { return sessions.has(sessionId) ? clone(sessions.get(sessionId)) : null; }, saveSession(next) { sessions.set(next.id, clone(next)); return true; }, listSessions() { return [...sessions.values()].map(clone); }, loadStatusDefinitions() { return statusConfig; }, saveStatusDefinitions(next) { statusConfig = clone(next); return true; }, now() { return new Date(Date.UTC(2026, 7, 12, 0, 0, tick++)); }, }); return { service, sessions }; } async function main() { assert.deepEqual(Object.values(LIFECYCLE_EVENT_TYPES), [ 'user_message_received', 'turn_started', 'user_input_requested', 'turn_completed', 'turn_failed', ]); assert.equal(normalizeLifecycleEvent({ type: 'turn_start' }).type, 'turn_started'); assert.equal(normalizeLifecycleEvent({ type: 'turn_completed', outcome: 'failed' }).type, 'turn_failed'); assert.equal(normalizeLifecycleEvent({ type: 'user_input_requested', structured: false }).structured, false); const lifecycleSource = fs.readFileSync(path.join(__dirname, '..', 'lib', 'task-board-lifecycle.js'), 'utf8'); assert.equal(/reportingStatus|本轮未上报|mark_reporting_missing/.test(lifecycleSource), false); assert.equal(/['"](?:in_progress|waiting_user|blocked|completed)['"]/.test(lifecycleSource), false); const { service, sessions } = createHarness(); service.upsertStatusDefinition({ id: 'review_ready', label: '等待评审', prompt: '当实现与验证已完成,下一步明确需要评审时选择此列。', color: '#38bdf8', order: 36, enabled: true, expectedVersion: 0, }, { source: 'user' }); const enabled = service.setTracking('dynamic-lifecycle', true, { source: 'user', expectedVersion: 0, }); const selected = service.updateStatus('dynamic-lifecycle', { statusId: 'review_ready', reason: '等待真实评审', version: enabled.taskTracking.version, }, { source: 'mcp' }); const lifecycle = createTaskBoardLifecycle(service, { isTrackingEnabled(sessionId) { return service.getTask(sessionId).taskTracking.enabled; }, }); assert.equal(typeof lifecycle.noteTaskStatusUpdate, 'undefined', '生命周期不再维护本轮显式上报审计状态'); let version = selected.taskTracking.version; const events = [ { type: 'user_message_received', eventId: 'message-1' }, { type: 'turn_started', turnId: 'turn-1' }, { type: 'user_input_requested', turnId: 'turn-1', eventId: 'input-1', structured: true }, { type: 'turn_failed', turnId: 'turn-1', error: '运行异常' }, { type: 'turn_completed', turnId: 'turn-2', outcome: 'completed' }, ]; for (const event of events) { const result = await lifecycle.handleEvent('dynamic-lifecycle', event); const task = service.getTask('dynamic-lifecycle'); assert.equal(task.taskTracking.statusId, 'review_ready', `${event.type} 不得推断或迁移任务列`); assert.equal(Object.hasOwn(task.taskTracking, 'reportingStatus'), false); assert.equal(task.taskTracking.version, version, `${event.type} 不得制造无意义状态写入`); assert.equal(result.ok, true); } const duplicate = await lifecycle.handleEvent('dynamic-lifecycle', { type: 'turn_completed', turnId: 'turn-2', outcome: 'completed', }); assert.equal(duplicate.ignored, true); const archived = service.setArchived('dynamic-lifecycle', true, { source: 'user', expectedVersion: version, }); version = archived.taskTracking.version; assert(archived.taskTracking.archivedAt); const resumed = await lifecycle.handleEvent('dynamic-lifecycle', { type: 'user_message_received', eventId: 'message-resume', }); assert.equal(resumed.changed, true); const resumedTask = service.getTask('dynamic-lifecycle'); assert.equal(resumedTask.taskTracking.statusId, 'review_ready'); assert.equal(resumedTask.taskTracking.archivedAt, null); assert.equal(resumedTask.taskTracking.version, version + 1); assert.equal(Object.hasOwn(sessions.get('dynamic-lifecycle').taskTracking, 'reportingStatus'), false); const disabled = service.setTracking('dynamic-lifecycle', false, { source: 'user', expectedVersion: resumedTask.taskTracking.version, }); const gated = await lifecycle.handleEvent('dynamic-lifecycle', { type: 'turn_started', turnId: 'turn-disabled', }); assert.equal(gated.ignored, true); assert.equal(gated.reason, 'task_tracking_disabled'); assert.equal(service.getTask('dynamic-lifecycle').taskTracking.statusId, 'review_ready'); assert.equal(service.getTask('dynamic-lifecycle').taskTracking.version, disabled.taskTracking.version); const unstructured = await lifecycle.handleEvent('dynamic-lifecycle', { type: 'user_input_requested', structured: false, }); assert.equal(unstructured.ignored, true); console.log('Task board lifecycle unit checks passed.'); } main().catch((error) => { console.error(error.stack || error.message || error); process.exitCode = 1; });