489 lines
23 KiB
JavaScript
489 lines
23 KiB
JavaScript
'use strict';
|
||
|
||
const assert = require('assert');
|
||
const {
|
||
MAX_VERSION,
|
||
STATUS_ID_PATTERN,
|
||
SYSTEM_STATUS_IDS,
|
||
TaskBoardError,
|
||
createTaskBoardService,
|
||
} = require('../lib/task-board-service');
|
||
const {
|
||
TASK_BOARD_MCP_TOOL_DEFINITIONS,
|
||
createTaskBoardMcpToolDefinitions,
|
||
createTaskBoardMcpHandler,
|
||
createTaskBoardMcpHandlers,
|
||
} = require('../lib/task-board-mcp');
|
||
|
||
function clone(value) {
|
||
return JSON.parse(JSON.stringify(value));
|
||
}
|
||
|
||
function sessionFixture(id, overrides = {}) {
|
||
return {
|
||
id,
|
||
title: `任务 ${id}`,
|
||
agent: 'codexapp',
|
||
cwd: '/home/cc-web',
|
||
created: '2026-08-11T00:00:00.000Z',
|
||
updated: '2026-08-11T00:00:00.000Z',
|
||
messages: [],
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function createHarness() {
|
||
const sessions = new Map([
|
||
['ordinary-a', sessionFixture('ordinary-a')],
|
||
['migration-b', sessionFixture('migration-b')],
|
||
['priority-c', sessionFixture('priority-c')],
|
||
['lifecycle-d', sessionFixture('lifecycle-d')],
|
||
['mcp-e', sessionFixture('mcp-e')],
|
||
['classifier-h', sessionFixture('classifier-h')],
|
||
]);
|
||
let statusConfig = null;
|
||
let clockTick = 0;
|
||
const service = createTaskBoardService({
|
||
loadSession(sessionId) {
|
||
const session = sessions.get(sessionId);
|
||
return session ? clone(session) : null;
|
||
},
|
||
saveSession(session) {
|
||
sessions.set(session.id, clone(session));
|
||
return true;
|
||
},
|
||
listSessions() {
|
||
return Array.from(sessions.values(), clone);
|
||
},
|
||
loadStatusDefinitions() {
|
||
return statusConfig;
|
||
},
|
||
saveStatusDefinitions(nextConfig) {
|
||
statusConfig = clone(nextConfig);
|
||
return true;
|
||
},
|
||
now() {
|
||
const value = new Date(Date.UTC(2026, 7, 11, 1, 0, clockTick));
|
||
clockTick += 1;
|
||
return value;
|
||
},
|
||
});
|
||
return {
|
||
service,
|
||
sessions,
|
||
getStatusConfig: () => clone(statusConfig),
|
||
};
|
||
}
|
||
|
||
function expectTaskError(fn, code) {
|
||
assert.throws(fn, (error) => (
|
||
error instanceof TaskBoardError && error.code === code
|
||
), `应抛出 ${code}`);
|
||
}
|
||
|
||
async function main() {
|
||
const harness = createHarness();
|
||
const { service } = harness;
|
||
|
||
// 普通会话默认关闭,读取不会把默认结构写回会话。
|
||
const ordinaryDefault = service.getTask('ordinary-a');
|
||
assert.strictEqual(ordinaryDefault.taskTracking.enabled, false);
|
||
assert.strictEqual(ordinaryDefault.taskTracking.statusId, 'unassigned');
|
||
assert.strictEqual('baseStatus' in ordinaryDefault.taskTracking, false);
|
||
assert.strictEqual('progress' in ordinaryDefault.taskTracking, false);
|
||
assert.strictEqual(ordinaryDefault.taskTracking.version, 0);
|
||
assert.strictEqual(harness.sessions.get('ordinary-a').taskTracking, undefined);
|
||
assert.deepStrictEqual(service.listTasks(), []);
|
||
assert.deepStrictEqual(service.listTasks({ includeDisabled: true, archived: 'all' }), []);
|
||
|
||
// 历史百分比字段只在读取时忽略,不返回,也不触发会话改写。
|
||
const legacyProgressSession = sessionFixture('legacy-progress-g', {
|
||
taskTracking: {
|
||
enabled: true,
|
||
statusId: 'in_progress',
|
||
source: 'mcp',
|
||
reason: '历史任务',
|
||
summary: '遗留百分比字段',
|
||
progress: 64,
|
||
reportingStatus: 'reported',
|
||
version: 3,
|
||
enabledAt: '2026-08-11T00:00:00.000Z',
|
||
disabledAt: null,
|
||
statusUpdatedAt: '2026-08-11T00:01:00.000Z',
|
||
completedAt: null,
|
||
archivedAt: null,
|
||
archivedBy: null,
|
||
},
|
||
});
|
||
harness.sessions.set(legacyProgressSession.id, clone(legacyProgressSession));
|
||
const legacyBeforeRead = clone(harness.sessions.get(legacyProgressSession.id));
|
||
const legacyTask = service.getTask(legacyProgressSession.id);
|
||
assert.strictEqual(legacyTask.taskTracking.statusId, 'in_progress');
|
||
assert.strictEqual('progress' in legacyTask.taskTracking, false);
|
||
assert.strictEqual('progress' in service.listTasks({ sessionId: legacyProgressSession.id })[0].taskTracking, false);
|
||
assert.deepStrictEqual(harness.sessions.get(legacyProgressSession.id), legacyBeforeRead);
|
||
harness.sessions.delete(legacyProgressSession.id);
|
||
|
||
expectTaskError(() => service.updateStatus('ordinary-a', {
|
||
statusId: 'in_progress',
|
||
}, { source: 'mcp' }), 'task_tracking_disabled');
|
||
expectTaskError(() => service.setTracking('ordinary-a', true, { source: 'mcp' }), 'task_tracking_forbidden');
|
||
|
||
const enabledA = service.setTracking('ordinary-a', true, {
|
||
source: 'user', id: 'user-a', expectedVersion: 0,
|
||
});
|
||
assert.strictEqual(enabledA.taskTracking.enabled, true);
|
||
assert.strictEqual(enabledA.taskTracking.source, 'default');
|
||
assert.strictEqual('progress' in enabledA.taskTracking, false);
|
||
assert.strictEqual(enabledA.taskTracking.version, 1);
|
||
assert.strictEqual(enabledA.agent, 'codexapp', 'Agent 仍应作为只读会话元数据返回');
|
||
assert.strictEqual(service.listTasks().length, 1);
|
||
assert.strictEqual(service.listTasks({ query: '', archived: false }).length, 1);
|
||
expectTaskError(() => service.listTasks({ agent: 'codexapp' }), 'task_status_invalid');
|
||
expectTaskError(() => service.listTasks({ baseStatus: 'in_progress' }), 'task_status_invalid');
|
||
expectTaskError(() => service.listTasks({ priority: 'high' }), 'task_status_invalid');
|
||
harness.sessions.set('preenabled-f', sessionFixture('preenabled-f', { taskTracking: { enabled: true } }));
|
||
const initializedF = service.setTracking('preenabled-f', true, { source: 'user', expectedVersion: 0 });
|
||
assert.strictEqual(initializedF.taskTracking.enabled, true);
|
||
assert.strictEqual(initializedF.taskTracking.version, 1);
|
||
assert(initializedF.taskTracking.enabledAt);
|
||
assert(initializedF.taskTracking.statusUpdatedAt);
|
||
|
||
// 每个定义都是独立列;系统列 ID 稳定,所有列都有可编辑分类提示词。
|
||
const defaults = service.getStatusDefinitions();
|
||
assert.deepStrictEqual(defaults.filter((item) => item.system).map((item) => item.id).sort(), [...SYSTEM_STATUS_IDS].sort());
|
||
assert(defaults.every((item) => typeof item.prompt === 'string' && item.prompt.length > 0));
|
||
assert(defaults.every((item) => !Object.prototype.hasOwnProperty.call(item, 'baseStatus')));
|
||
assert(STATUS_ID_PATTERN.test('waiting-release'));
|
||
assert(STATUS_ID_PATTERN.test('Waiting-Release_2'));
|
||
assert(STATUS_ID_PATTERN.test(`A${'b'.repeat(79)}`));
|
||
assert.strictEqual(STATUS_ID_PATTERN.test(`A${'b'.repeat(80)}`), false);
|
||
const updatedSystem = service.upsertStatusDefinition({
|
||
id: 'waiting_user',
|
||
label: '等待用户',
|
||
color: '#F59E0B',
|
||
order: 35,
|
||
prompt: '当后续推进必须等待用户补充、确认、授权或验收时选择此列。',
|
||
version: 0,
|
||
}, { source: 'user', id: 'admin' });
|
||
assert.strictEqual(updatedSystem.version, 1);
|
||
assert.strictEqual(updatedSystem.definition.prompt, '当后续推进必须等待用户补充、确认、授权或验收时选择此列。');
|
||
assert.strictEqual('baseStatus' in updatedSystem.definition, false);
|
||
assert.strictEqual(updatedSystem.definition.color, '#f59e0b');
|
||
expectTaskError(() => service.upsertStatusDefinition({
|
||
id: 'waiting_user', baseStatus: 'blocked', version: 1,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
expectTaskError(() => service.removeStatusDefinition('completed', null, { source: 'user' }), 'task_status_invalid');
|
||
|
||
// 与 Firefox 真实提交完全一致:definition 不包含 description。
|
||
const frontendDefinition = {
|
||
id: 'waiting-release',
|
||
label: '等待发布',
|
||
color: '#f97316',
|
||
order: 38,
|
||
enabled: true,
|
||
system: false,
|
||
prompt: '当实现和验证已完成,但仍需等待部署环境或发布窗口时选择此列。',
|
||
};
|
||
assert.strictEqual(Object.hasOwn(frontendDefinition, 'description'), false);
|
||
const customResult = service.upsertStatusDefinition({
|
||
...frontendDefinition,
|
||
expectedVersion: 1,
|
||
}, { source: 'user', id: 'admin' });
|
||
assert.strictEqual(customResult.version, 2);
|
||
assert.strictEqual(customResult.definition.system, false);
|
||
assert.strictEqual(customResult.definition.id, 'waiting-release');
|
||
assert.strictEqual(customResult.definition.prompt, '当实现和验证已完成,但仍需等待部署环境或发布窗口时选择此列。');
|
||
assert.strictEqual('baseStatus' in customResult.definition, false);
|
||
const queriedCustomDefinition = service.getStatusDefinitions()
|
||
.find((item) => item.id === 'waiting-release');
|
||
assert(queriedCustomDefinition);
|
||
assert.strictEqual(queriedCustomDefinition.prompt, frontendDefinition.prompt);
|
||
assert.strictEqual('baseStatus' in queriedCustomDefinition, false);
|
||
assert.strictEqual(harness.getStatusConfig().version, 2);
|
||
assert.strictEqual(
|
||
harness.getStatusConfig().definitions.find((item) => item.id === 'waiting-release').prompt,
|
||
'当实现和验证已完成,但仍需等待部署环境或发布窗口时选择此列。',
|
||
);
|
||
assert(harness.getStatusConfig().definitions.every((item) => !('baseStatus' in item)));
|
||
expectTaskError(() => service.upsertStatusDefinition({
|
||
id: 'waiting-release', prompt: '', expectedVersion: 2,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
expectTaskError(() => service.upsertStatusDefinition({
|
||
id: 'waiting-release', baseStatus: 'blocked', expectedVersion: 2,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
|
||
const disabledCustom = service.upsertStatusDefinition({
|
||
id: 'paused_external',
|
||
label: '外部暂停',
|
||
description: '暂时停用的状态。',
|
||
prompt: '当任务明确因外部条件暂停时选择此列。',
|
||
color: '#a855f7',
|
||
order: 39,
|
||
enabled: false,
|
||
expectedVersion: 2,
|
||
}, { source: 'user', id: 'admin' });
|
||
assert.strictEqual(disabledCustom.version, 3);
|
||
expectTaskError(() => service.updateStatus('ordinary-a', {
|
||
statusId: 'paused_external', version: 1,
|
||
}, { source: 'mcp' }), 'task_status_invalid');
|
||
|
||
const customTaskA = service.updateStatus('ordinary-a', {
|
||
statusId: 'waiting-release',
|
||
reason: '代码已完成',
|
||
summary: '等待部署环境',
|
||
version: 1,
|
||
}, { source: 'mcp', id: 'agent-a' });
|
||
assert.strictEqual(customTaskA.taskTracking.statusId, 'waiting-release');
|
||
assert.strictEqual('baseStatus' in customTaskA.taskTracking, false);
|
||
assert.strictEqual('progress' in customTaskA.taskTracking, false);
|
||
assert.strictEqual('baseStatus' in customTaskA.status, false);
|
||
assert.strictEqual(customTaskA.taskTracking.source, 'mcp');
|
||
assert.strictEqual(customTaskA.taskTracking.version, 2);
|
||
const queriedCustomTasks = service.listTasks({ statusId: 'waiting-release' });
|
||
assert.deepStrictEqual(queriedCustomTasks.map((task) => task.sessionId), ['ordinary-a']);
|
||
assert.strictEqual('progress' in queriedCustomTasks[0].taskTracking, false);
|
||
|
||
// 自动分类器是独立审计来源,仍由 expectedVersion 负责并发门禁。
|
||
const classifierEnabled = service.setTracking('classifier-h', true, {
|
||
source: 'user', expectedVersion: 0,
|
||
});
|
||
const classifierUserMove = service.updateStatus('classifier-h', {
|
||
statusId: 'waiting_user', expectedVersion: classifierEnabled.taskTracking.version,
|
||
}, { source: 'user', id: 'user-h' });
|
||
const classifierMove = service.updateStatus('classifier-h', {
|
||
statusId: 'waiting-release',
|
||
reason: '用户已确认继续推进',
|
||
summary: '等待发布前的最终处理',
|
||
expectedVersion: classifierUserMove.taskTracking.version,
|
||
}, { source: 'classifier', id: 'task-status-classifier' });
|
||
assert.strictEqual(classifierMove.changed, true);
|
||
assert.strictEqual(classifierMove.taskTracking.statusId, 'waiting-release');
|
||
assert.strictEqual(classifierMove.taskTracking.source, 'classifier');
|
||
assert.strictEqual(classifierMove.taskTracking.version, classifierUserMove.taskTracking.version + 1);
|
||
expectTaskError(() => service.updateStatus('classifier-h', {
|
||
statusId: 'completed', expectedVersion: classifierUserMove.taskTracking.version,
|
||
}, { source: 'classifier' }), 'task_version_conflict');
|
||
|
||
// 停用跟踪仍保留状态;迁移会覆盖启用和停用会话中的引用。
|
||
const enabledB = service.setTracking('migration-b', true, { source: 'user', expectedVersion: 0 });
|
||
const customTaskB = service.updateStatus('migration-b', {
|
||
statusId: 'waiting-release', version: enabledB.taskTracking.version,
|
||
}, { source: 'mcp' });
|
||
const disabledB = service.setTracking('migration-b', false, {
|
||
source: 'user', expectedVersion: customTaskB.taskTracking.version,
|
||
});
|
||
assert.strictEqual(disabledB.taskTracking.enabled, false);
|
||
assert.strictEqual(disabledB.taskTracking.statusId, 'waiting-release');
|
||
assert.strictEqual(service.listTasks().some((task) => task.sessionId === 'migration-b'), false);
|
||
assert(service.listTasks({ includeDisabled: true, archived: 'all' })
|
||
.some((task) => task.sessionId === 'migration-b'));
|
||
|
||
expectTaskError(() => service.removeStatusDefinition(
|
||
'waiting-release', null, { source: 'user', expectedVersion: 3 },
|
||
), 'task_status_in_use');
|
||
expectTaskError(() => service.removeStatusDefinition(
|
||
'waiting-release', 'paused_external', { source: 'user', expectedVersion: 3 },
|
||
), 'task_status_invalid');
|
||
const migration = service.removeStatusDefinition(
|
||
'waiting-release', 'waiting_user', { source: 'user', expectedVersion: 3 },
|
||
);
|
||
assert.strictEqual(migration.version, 4);
|
||
assert.deepStrictEqual(migration.migratedSessionIds.sort(), ['classifier-h', 'migration-b', 'ordinary-a']);
|
||
assert.strictEqual(service.getTask('classifier-h').taskTracking.statusId, 'waiting_user');
|
||
assert.strictEqual(service.getTask('ordinary-a').taskTracking.statusId, 'waiting_user');
|
||
assert.strictEqual(service.getTask('migration-b').taskTracking.statusId, 'waiting_user');
|
||
assert.strictEqual(service.getTask('migration-b').taskTracking.enabled, false);
|
||
assert.strictEqual(service.getStatusDefinitions().some((item) => item.id === 'waiting-release'), false);
|
||
|
||
const reviewColumn = service.upsertStatusDefinition({
|
||
id: 'review_ready',
|
||
label: '等待评审',
|
||
description: '实现完成,等待代码或产品评审。',
|
||
prompt: '当实现与自测已完成,下一步明确是代码评审或产品评审时选择此列。',
|
||
color: '#38bdf8',
|
||
order: 36,
|
||
enabled: true,
|
||
version: 4,
|
||
}, { source: 'user', id: 'admin' });
|
||
assert.strictEqual(reviewColumn.version, 5);
|
||
assert.strictEqual('baseStatus' in reviewColumn.definition, false);
|
||
|
||
// 用户与来源 MCP 均按最后一次有效写入生效,版本检查独立处理并发冲突。
|
||
const enabledC = service.setTracking('priority-c', true, { source: 'user', expectedVersion: 0 });
|
||
const userStatus = service.updateStatus('priority-c', {
|
||
statusId: 'blocked',
|
||
reason: '用户手动标记',
|
||
version: enabledC.taskTracking.version,
|
||
}, { source: 'user', id: 'owner' });
|
||
const mcpStatus = service.updateStatus('priority-c', {
|
||
statusId: 'review_ready', version: userStatus.taskTracking.version,
|
||
}, { source: 'mcp', id: 'agent-c' });
|
||
assert.strictEqual(mcpStatus.changed, true);
|
||
assert.strictEqual(Object.hasOwn(mcpStatus, 'ignored'), false);
|
||
assert.strictEqual(Object.hasOwn(mcpStatus, 'ignoredReason'), false);
|
||
assert.strictEqual(mcpStatus.taskTracking.statusId, 'review_ready');
|
||
assert.strictEqual(mcpStatus.taskTracking.version, userStatus.taskTracking.version + 1);
|
||
assert.strictEqual(mcpStatus.taskTracking.source, 'mcp');
|
||
const ignoredHook = service.recordLifecycleEvent('priority-c', {
|
||
type: 'turn_started',
|
||
occurredAt: '2026-08-11T02:00:00.000Z',
|
||
version: mcpStatus.taskTracking.version,
|
||
});
|
||
assert.strictEqual(ignoredHook.ignored, true);
|
||
assert.strictEqual(ignoredHook.reason, 'no_change');
|
||
assert.strictEqual(ignoredHook.task.taskTracking.statusId, 'review_ready');
|
||
expectTaskError(() => service.updateStatus('priority-c', {
|
||
statusId: 'completed', version: 1,
|
||
}, { source: 'user' }), 'task_version_conflict');
|
||
|
||
// 严格校验状态 ID、颜色、退役字段、文本长度与版本。
|
||
expectTaskError(() => service.updateStatus('priority-c', {
|
||
statusId: 'Bad.Status', version: mcpStatus.taskTracking.version,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
expectTaskError(() => service.updateStatus('priority-c', {
|
||
statusId: 'blocked', progress: 42, version: mcpStatus.taskTracking.version,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
expectTaskError(() => service.updateStatus('priority-c', {
|
||
statusId: 'blocked', reason: 'x'.repeat(2001), version: mcpStatus.taskTracking.version,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
expectTaskError(() => service.updateStatus('priority-c', {
|
||
statusId: 'blocked', version: MAX_VERSION + 1,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
expectTaskError(() => service.upsertStatusDefinition({
|
||
id: 'bad_color',
|
||
label: '坏颜色',
|
||
description: '',
|
||
prompt: '当颜色校验测试命中时选择此列。',
|
||
color: 'red',
|
||
order: 1,
|
||
enabled: true,
|
||
version: 5,
|
||
}, { source: 'user' }), 'task_status_invalid');
|
||
|
||
// 归档是软标记,Agent 无权归档;新用户消息只恢复归档,不推断任务列。
|
||
const enabledD = service.setTracking('lifecycle-d', true, { source: 'user', expectedVersion: 0 });
|
||
const completedD = service.updateStatus('lifecycle-d', {
|
||
statusId: 'completed',
|
||
summary: '已完成',
|
||
version: enabledD.taskTracking.version,
|
||
}, { source: 'mcp' });
|
||
expectTaskError(() => service.setArchived('lifecycle-d', true, {
|
||
source: 'mcp', expectedVersion: completedD.taskTracking.version,
|
||
}), 'task_archive_forbidden');
|
||
const archivedD = service.setArchived('lifecycle-d', true, {
|
||
source: 'user', id: 'owner', expectedVersion: completedD.taskTracking.version,
|
||
});
|
||
assert.strictEqual(archivedD.archived, true);
|
||
assert.strictEqual(archivedD.taskTracking.statusId, 'completed');
|
||
assert.strictEqual(service.listTasks().some((task) => task.sessionId === 'lifecycle-d'), false);
|
||
assert.strictEqual(service.listTasks({ archived: true }).length, 1);
|
||
const archivedTurn = service.recordLifecycleEvent('lifecycle-d', {
|
||
type: 'turn_started', version: archivedD.taskTracking.version,
|
||
});
|
||
assert.strictEqual(archivedTurn.reason, 'task_archived');
|
||
const reopenedD = service.recordLifecycleEvent('lifecycle-d', {
|
||
type: 'user_message_received',
|
||
occurredAt: '2026-08-11T03:00:00.000Z',
|
||
version: archivedD.taskTracking.version,
|
||
});
|
||
assert.strictEqual(reopenedD.changed, true);
|
||
assert.strictEqual(reopenedD.task.archived, false);
|
||
assert.strictEqual(reopenedD.task.taskTracking.statusId, 'completed');
|
||
assert.strictEqual('baseStatus' in reopenedD.task.taskTracking, false);
|
||
assert.strictEqual(Object.hasOwn(reopenedD.task.taskTracking, 'reportingStatus'), false);
|
||
|
||
const completedTurn = service.recordLifecycleEvent('lifecycle-d', {
|
||
type: 'turn_completed',
|
||
occurredAt: '2026-08-11T03:30:00.000Z',
|
||
version: reopenedD.task.taskTracking.version,
|
||
});
|
||
assert.strictEqual(completedTurn.changed, false);
|
||
assert.strictEqual(completedTurn.reason, 'no_change');
|
||
assert.strictEqual(completedTurn.task.taskTracking.statusId, 'completed');
|
||
assert.strictEqual(completedTurn.task.taskTracking.version, reopenedD.task.taskTracking.version);
|
||
assert.strictEqual(Object.hasOwn(completedTurn.task.taskTracking, 'reportingStatus'), false);
|
||
|
||
// MCP 工具按来源会话动态生成;静态工具表不再泄漏任何任务工具。
|
||
assert.deepStrictEqual(TASK_BOARD_MCP_TOOL_DEFINITIONS, []);
|
||
const mcpHandlers = createTaskBoardMcpHandlers(service);
|
||
assert.deepStrictEqual(
|
||
createTaskBoardMcpToolDefinitions(service, { sourceSessionId: 'mcp-e' }),
|
||
[],
|
||
);
|
||
const disabledUpdate = await mcpHandlers.ccweb_task_update({
|
||
statusId: 'in_progress',
|
||
}, { sourceSessionId: 'mcp-e' });
|
||
assert.strictEqual(disabledUpdate.ok, false);
|
||
assert.strictEqual(disabledUpdate.code, 'task_tracking_disabled');
|
||
|
||
const enabledE = service.setTracking('mcp-e', true, { source: 'user', expectedVersion: 0 });
|
||
const dynamicTools = createTaskBoardMcpToolDefinitions(service, { sourceSessionId: 'mcp-e' });
|
||
assert.deepStrictEqual(dynamicTools.map((tool) => tool.name), ['ccweb_task_update']);
|
||
const updateToolDefinition = dynamicTools[0];
|
||
const enabledDefinitions = service.getStatusDefinitions().filter((definition) => definition.enabled);
|
||
assert.deepStrictEqual(
|
||
updateToolDefinition.inputSchema.properties.statusId.enum,
|
||
enabledDefinitions.map((definition) => definition.id),
|
||
);
|
||
assert.deepStrictEqual(
|
||
Object.keys(updateToolDefinition.inputSchema.properties),
|
||
['statusId', 'reason', 'summary'],
|
||
);
|
||
assert.match(updateToolDefinition.description, /当前状态/);
|
||
assert.match(updateToolDefinition.description, /仅当当前对话任务状态真实发生变化时调用/);
|
||
assert.match(updateToolDefinition.description, /单轮开始、结束、运行停止不代表任务状态变化/);
|
||
for (const definition of enabledDefinitions) {
|
||
assert(updateToolDefinition.description.includes(definition.label));
|
||
assert(updateToolDefinition.description.includes(definition.id));
|
||
assert(updateToolDefinition.description.includes(definition.prompt));
|
||
}
|
||
assert.strictEqual(updateToolDefinition.description.includes('ccweb_task_status_list'), false);
|
||
const retiredMcpProgress = await mcpHandlers.ccweb_task_update({
|
||
statusId: 'review_ready',
|
||
progress: 10,
|
||
}, { sourceSessionId: 'mcp-e', actorId: 'agent-e' });
|
||
assert.strictEqual(retiredMcpProgress.ok, false);
|
||
assert.strictEqual(retiredMcpProgress.code, 'task_status_invalid');
|
||
assert.strictEqual(service.getTask('mcp-e').taskTracking.version, enabledE.taskTracking.version);
|
||
const mcpSuccess = await mcpHandlers.ccweb_task_update({
|
||
statusId: 'review_ready',
|
||
summary: 'MCP 根据列提示词判断为等待评审',
|
||
}, { sourceSessionId: 'mcp-e', actorId: 'agent-e' });
|
||
assert.strictEqual(mcpSuccess.ok, true);
|
||
assert.strictEqual(mcpSuccess.sessionId, 'mcp-e');
|
||
assert.strictEqual(mcpSuccess.currentStatus.statusId, 'review_ready');
|
||
assert.strictEqual(mcpSuccess.currentStatus.source, 'mcp');
|
||
assert.strictEqual(Object.hasOwn(mcpSuccess.currentStatus, 'progress'), false);
|
||
assert.strictEqual(Object.hasOwn(mcpSuccess.task.taskTracking, 'progress'), false);
|
||
assert.strictEqual(mcpSuccess.currentStatusDefinition.prompt, reviewColumn.definition.prompt);
|
||
assert.strictEqual(JSON.stringify(mcpSuccess).includes('baseStatus'), false);
|
||
|
||
const refreshedTools = createTaskBoardMcpToolDefinitions(service, { sourceSessionId: 'mcp-e' });
|
||
assert.match(refreshedTools[0].description, /等待评审[^\n]*review_ready|review_ready[^\n]*等待评审/);
|
||
|
||
const unknownStatus = await mcpHandlers.ccweb_task_update({
|
||
statusId: 'does_not_exist',
|
||
}, { sourceSessionId: 'mcp-e' });
|
||
assert.strictEqual(unknownStatus.code, 'task_status_unknown');
|
||
const missingContext = await mcpHandlers.ccweb_task_update({ statusId: 'in_progress' }, {});
|
||
assert.strictEqual(missingContext.code, 'task_session_not_found');
|
||
const spoofedContext = await mcpHandlers.ccweb_task_update({
|
||
statusId: 'in_progress', sourceSessionId: 'priority-c',
|
||
}, { sourceSessionId: 'mcp-e' });
|
||
assert.strictEqual(spoofedContext.code, 'task_status_invalid');
|
||
|
||
const dispatchMcp = createTaskBoardMcpHandler({ taskBoardService: service });
|
||
const retiredList = await dispatchMcp('ccweb_task_status_list', {}, { sessionId: 'mcp-e' });
|
||
assert.strictEqual(retiredList.code, 'unknown_tool');
|
||
const unknownTool = await dispatchMcp('ccweb_task_archive', {}, { sourceSessionId: 'mcp-e' });
|
||
assert.strictEqual(unknownTool.code, 'unknown_tool');
|
||
|
||
console.log('Task board service and MCP unit checks passed.');
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(error.stack || error.message);
|
||
process.exit(1);
|
||
});
|