159 lines
5.8 KiB
JavaScript
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;
|
|
});
|