Files
cc-web/scripts/task-board-classifier-unit.js
2026-08-13 00:34:37 +08:00

294 lines
11 KiB
JavaScript

'use strict';
const assert = require('node:assert');
const http = require('node:http');
const {
CLASSIFICATION_EVENT_TYPES,
TaskStatusClassificationError,
buildTaskStatusClassificationPrompt,
createTaskStatusClassifier,
parseTaskStatusClassification,
parseResponsesBody,
requestOpenAIResponses,
} = require('../lib/task-board-classifier');
const DEFINITIONS = Object.freeze([
Object.freeze({
id: 'alpha-lane',
label: '甲列',
prompt: '当已有明确工作正在实际推进,且无需等待外部反馈时选择。',
enabled: true,
order: 10,
}),
Object.freeze({
id: 'beta_lane',
label: '乙列',
prompt: '当主对话已经交付本轮结果,但继续推进必须等待用户输入或验收时选择。',
enabled: true,
order: 20,
}),
Object.freeze({
id: 'disabled-lane',
label: '停用列',
prompt: '不得选择。',
enabled: false,
order: 30,
}),
]);
function taskFixture(overrides = {}) {
return {
sessionId: 'classifier-session',
title: '实现动态任务分类',
taskTracking: {
enabled: true,
statusId: 'beta_lane',
summary: '正在讨论分类方案',
version: 7,
},
status: DEFINITIONS[1],
...overrides,
};
}
function expectClassificationError(fn, code) {
assert.throws(fn, (error) => (
error instanceof TaskStatusClassificationError && error.code === code
));
}
async function main() {
const prompt = buildTaskStatusClassificationPrompt({
eventType: CLASSIFICATION_EVENT_TYPES.USER_MESSAGE_RECEIVED,
task: taskFixture(),
definitions: DEFINITIONS,
userMessage: '方案可以,开始实现。',
});
assert.deepEqual(prompt.schema.properties.statusId.enum, ['alpha-lane', 'beta_lane']);
assert.equal(prompt.schema.additionalProperties, false);
assert.deepEqual(prompt.schema.required, ['statusId', 'reason', 'summary']);
assert.equal(Object.hasOwn(prompt.schema.properties.reason, 'minLength'), false);
assert.equal(Object.hasOwn(prompt.schema.properties.reason, 'maxLength'), false);
assert.match(prompt.developerPrompt, /唯一职责是分类/);
assert.match(prompt.developerPrompt, /不要执行、继续、检查或验证任务/);
assert.match(prompt.developerPrompt, /分类提示词.*唯一状态语义/);
assert.match(prompt.developerPrompt, /不得根据列 ID、列名.*猜测/);
assert.match(prompt.developerPrompt, /用户消息进入/);
assert.match(prompt.developerPrompt, /主对话本轮完成/);
assert.match(prompt.developerPrompt, /请求用户输入.*本轮完成/);
assert.match(prompt.developerPrompt, /运行开始、运行停止.*网络失败或人工中断/);
assert.match(prompt.inputPrompt, /alpha-lane/);
assert.match(prompt.inputPrompt, /beta_lane/);
assert.match(prompt.inputPrompt, /已有明确工作正在实际推进/);
assert.match(prompt.inputPrompt, /方案可以,开始实现/);
assert.doesNotMatch(prompt.inputPrompt, /不得选择/);
assert.deepEqual(parseTaskStatusClassification(
'{"statusId":"alpha-lane","reason":"用户已确认开始实现","summary":"实现动态任务分类"}',
['alpha-lane', 'beta_lane'],
), {
statusId: 'alpha-lane',
reason: '用户已确认开始实现',
summary: '实现动态任务分类',
});
expectClassificationError(() => parseTaskStatusClassification(
'```json\n{"statusId":"alpha-lane","reason":"x","summary":"y"}\n```',
['alpha-lane'],
), 'invalid_json');
expectClassificationError(() => parseTaskStatusClassification(
'{"statusId":"disabled-lane","reason":"x","summary":"y"}',
['alpha-lane', 'beta_lane'],
), 'invalid_status');
expectClassificationError(() => parseTaskStatusClassification(
'{"statusId":"alpha-lane","reason":"x","summary":"y","extra":true}',
['alpha-lane'],
), 'invalid_shape');
assert.deepEqual(parseResponsesBody([
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","delta":"{\\"statusId\\":\\"alpha-lane\\","}',
'',
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","delta":"\\"reason\\":\\"x\\",\\"summary\\":\\"y\\"}"}',
'',
'data: [DONE]',
].join('\n')), {
status: 'completed',
output_text: '{"statusId":"alpha-lane","reason":"x","summary":"y"}',
});
assert.deepEqual(parseResponsesBody([
'data: {"type":"response.output_text.delta","delta":"{\\"statusId\\":\\"alpha-lane\\",\\"reason\\":\\"x\\",\\"summary\\":\\"y\\"}"}',
'',
'data: {"type":"response.completed","response":{"status":"completed","output":[]}}',
'',
].join('\n')), {
status: 'completed',
output_text: '{"statusId":"alpha-lane","reason":"x","summary":"y"}',
});
const streamingServer = http.createServer((req, res) => {
req.resume();
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
res.write(`event: response.completed\ndata: ${JSON.stringify({
type: 'response.completed',
response: {
status: 'completed',
output_text: '{"statusId":"alpha-lane","reason":"x","summary":"y"}',
},
})}\n\n`);
setTimeout(() => res.end(), 1200);
});
await new Promise((resolve) => streamingServer.listen(0, '127.0.0.1', resolve));
try {
const startedAt = Date.now();
const streamed = await requestOpenAIResponses({
runtime: {
apiBase: `http://127.0.0.1:${streamingServer.address().port}/v1`,
apiKey: 'test-key',
},
body: { model: 'test-model' },
}, { timeoutMs: 2000 });
assert.equal(streamed.ok, true);
assert.equal(streamed.text, '{"statusId":"alpha-lane","reason":"x","summary":"y"}');
assert(Date.now() - startedAt < 600, 'SSE response.completed 后不应继续等待连接关闭');
} finally {
await new Promise((resolve) => streamingServer.close(resolve));
}
let currentTask = taskFixture();
const requests = [];
const updates = [];
const events = [];
const service = {
getTask() {
return JSON.parse(JSON.stringify(currentTask));
},
getStatusDefinitions() {
return JSON.parse(JSON.stringify(DEFINITIONS));
},
updateStatus(sessionId, update, actor) {
assert.equal(sessionId, currentTask.sessionId);
assert.deepEqual(actor, { source: 'classifier', id: 'task-status-classifier' });
if (update.expectedVersion !== currentTask.taskTracking.version) {
const error = new Error('版本冲突');
error.code = 'task_version_conflict';
throw error;
}
updates.push({ update, actor });
currentTask = taskFixture({
taskTracking: {
...currentTask.taskTracking,
statusId: update.statusId,
reason: update.reason,
summary: update.summary,
source: actor.source,
version: currentTask.taskTracking.version + 1,
},
status: DEFINITIONS.find((item) => item.id === update.statusId),
});
return { ...JSON.parse(JSON.stringify(currentTask)), changed: true };
},
};
const classifier = createTaskStatusClassifier({
taskBoardService: service,
loadSession() {
return { id: currentTask.sessionId, agent: 'codexapp', model: 'gpt-test(high)' };
},
resolveRuntime() {
return {
apiBase: 'https://provider.example/v1',
apiKey: 'secret-not-for-logs',
model: 'gpt-test',
effort: 'high',
providerName: 'fixture',
};
},
async requestStructured(request) {
requests.push(request);
return {
ok: true,
text: '{"statusId":"alpha-lane","reason":"用户已授权实施","summary":"实现动态任务分类"}',
};
},
onTaskChanged(event) {
events.push(event);
},
});
const moved = await classifier.enqueue(currentTask.sessionId, {
eventType: CLASSIFICATION_EVENT_TYPES.USER_MESSAGE_RECEIVED,
eventId: 'message-1',
userMessage: '按这个方案开始实现。',
});
assert.equal(moved.ok, true);
assert.equal(moved.changed, true);
assert.equal(currentTask.taskTracking.statusId, 'alpha-lane');
assert.equal(currentTask.taskTracking.source, 'classifier');
assert.equal(requests.length, 1);
assert.deepEqual(requests[0].body.tools, []);
assert.equal(requests[0].body.stream, true);
assert.equal(requests[0].body.reasoning.effort, 'low');
assert.equal(requests[0].body.text.format.type, 'json_schema');
assert.equal(requests[0].body.text.format.strict, true);
assert.deepEqual(requests[0].body.text.format.schema.properties.statusId.enum, ['alpha-lane', 'beta_lane']);
assert.equal(events.length, 1);
const duplicate = await classifier.enqueue(currentTask.sessionId, {
eventType: CLASSIFICATION_EVENT_TYPES.USER_MESSAGE_RECEIVED,
eventId: 'message-1',
userMessage: '重复消息不应再次调用。',
});
assert.equal(duplicate.skipped, 'duplicate_event');
assert.equal(requests.length, 1);
const invalidClassifier = createTaskStatusClassifier({
taskBoardService: service,
loadSession: () => ({ id: currentTask.sessionId, agent: 'codexapp', model: 'gpt-test' }),
resolveRuntime: () => ({ apiBase: 'https://provider.example/v1', apiKey: 'secret', model: 'gpt-test' }),
requestStructured: async () => ({
ok: true,
text: '```json\n{"statusId":"beta_lane","reason":"等待用户","summary":"等待确认"}\n```',
}),
});
const beforeInvalid = currentTask.taskTracking.version;
const invalid = await invalidClassifier.enqueue(currentTask.sessionId, {
eventType: CLASSIFICATION_EVENT_TYPES.TURN_COMPLETED,
eventId: 'turn-invalid',
assistantResult: '请用户确认后继续。',
});
assert.equal(invalid.ok, false);
assert.equal(invalid.errorCode, 'invalid_json');
assert.equal(currentTask.taskTracking.version, beforeInvalid);
let unavailableCalled = false;
const unavailableClassifier = createTaskStatusClassifier({
taskBoardService: service,
loadSession: () => ({ id: currentTask.sessionId, agent: 'codexapp', model: 'gpt-test' }),
resolveRuntime: () => null,
requestStructured: async () => { unavailableCalled = true; },
});
const unavailable = await unavailableClassifier.enqueue(currentTask.sessionId, {
eventType: CLASSIFICATION_EVENT_TYPES.TURN_COMPLETED,
eventId: 'turn-no-provider',
assistantResult: '已完成本轮。',
});
assert.equal(unavailable.skipped, 'provider_unavailable');
assert.equal(unavailableCalled, false);
currentTask = taskFixture({
taskTracking: { ...taskFixture().taskTracking, enabled: false },
});
const disabled = await classifier.enqueue(currentTask.sessionId, {
eventType: CLASSIFICATION_EVENT_TYPES.USER_MESSAGE_RECEIVED,
eventId: 'message-disabled',
userMessage: '未加入看板时不分类。',
});
assert.equal(disabled.skipped, 'tracking_disabled');
console.log('Task board classifier unit checks passed.');
}
main().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});