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