feat: overhaul task board and cross-conversation workflows
This commit is contained in:
158
scripts/task-board-lifecycle-unit.js
Normal file
158
scripts/task-board-lifecycle-unit.js
Normal file
@@ -0,0 +1,158 @@
|
||||
'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;
|
||||
});
|
||||
Reference in New Issue
Block a user