Files
cc-web/scripts/task-board-lifecycle-unit.js

159 lines
5.8 KiB
JavaScript

'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;
});