154 lines
6.9 KiB
JavaScript
154 lines
6.9 KiB
JavaScript
'use strict';
|
|
|
|
/** Gitea Workflow Codex/App 协议适配器离线单测。 */
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
buildGiteaMcpServerConfig,
|
|
buildGiteaThreadConfig,
|
|
buildGiteaMcpConfig,
|
|
extractTurnId,
|
|
extractThreadId,
|
|
buildWorkflowMarker,
|
|
parseWorkflowMarker,
|
|
encodeMarker,
|
|
isBotComment,
|
|
createGiteaWorkflowCodex,
|
|
WORKFLOW_STATES,
|
|
} = require('../lib/gitea-workflow-codex');
|
|
|
|
async function main() {
|
|
const mcp = buildGiteaMcpServerConfig({
|
|
host: 'https://gitea.example/',
|
|
accessToken: 'secret-token',
|
|
});
|
|
assert.deepEqual(mcp.args.slice(0, 4), ['-t', 'stdio', '-H', 'https://gitea.example']);
|
|
assert.equal(mcp.env.GITEA_ACCESS_TOKEN, 'secret-token');
|
|
assert.equal(mcp.env.GITEA_HOST, 'https://gitea.example');
|
|
assert.equal(buildGiteaMcpConfig({ gitea: { host: 'https://gitea.example', token: 't' } }).type, 'stdio');
|
|
|
|
const thread = buildGiteaThreadConfig({
|
|
cwd: '/tmp/gitea-workspace',
|
|
host: 'https://gitea.example',
|
|
accessToken: 'secret-token',
|
|
});
|
|
assert.equal(thread.config['mcp_servers.gitea'].type, 'stdio');
|
|
assert.equal(thread.config['mcp_servers.gitea'].env.GITEA_ACCESS_TOKEN, 'secret-token');
|
|
assert.equal(Object.hasOwn(thread.config, 'model'), false);
|
|
|
|
assert.equal(extractTurnId({ params: { turn: { id: 'turn-1' } } }), 'turn-1');
|
|
assert.equal(extractThreadId({ params: { thread: { id: 'thread-1' } } }), 'thread-1');
|
|
const marker = buildWorkflowMarker({ taskId: 'task-1', turnId: 'turn-1', resourceKey: 'default:a/r:issue:1' });
|
|
assert.deepEqual(parseWorkflowMarker(`正文\n${marker}`), {
|
|
version: '1', taskId: 'task-1', turnId: 'turn-1', resourceKey: 'default:a/r:issue:1', kind: 'final',
|
|
});
|
|
assert.equal(parseWorkflowMarker('<!-- ccweb-gitea v="1" taskId="x" -->'), null);
|
|
assert.equal(encodeMarker({ taskId: 'task-1', turnId: 'turn-1', resourceKey: 'r', state: 'fallback' }).includes('kind="fallback"'), true);
|
|
|
|
const bot = { id: '42', login: 'ccweb-bot' };
|
|
assert.equal(isBotComment({ user: { id: 42, login: 'renamed-bot' } }, bot), true);
|
|
assert.equal(isBotComment({ user: { login: 'CCWEB-BOT' } }, bot), true);
|
|
assert.equal(isBotComment({ user: { login: 'alice' } }, bot), false);
|
|
assert.equal(
|
|
createGiteaWorkflowCodex({ botIdentity: bot }).matchWorkflowReply(
|
|
{ body: `已完成\n${buildWorkflowMarker({ taskId: 'task-1', turnId: 'turn-old', resourceKey: 'r' })}`, user: bot },
|
|
{ taskId: 'task-1', turnId: 'turn-new', turnIds: ['turn-new', 'turn-old'], resourceKey: 'r' },
|
|
),
|
|
true,
|
|
);
|
|
|
|
let nowTick = 0;
|
|
const retryCalls = [];
|
|
const restCalls = [];
|
|
let verifyCalls = 0;
|
|
const adapter = createGiteaWorkflowCodex({
|
|
botIdentity: bot,
|
|
now: () => new Date(Date.UTC(2026, 7, 24, 0, 0, nowTick++)),
|
|
verifyReply: async ({ source, expected }) => {
|
|
verifyCalls += 1;
|
|
if (source === 'mcp' && verifyCalls === 1) return { status: 'missing' };
|
|
if (source === 'mcp' && verifyCalls === 2) return {
|
|
comments: [{
|
|
id: 99,
|
|
body: `已完成\n${buildWorkflowMarker(expected)}`,
|
|
user: { id: 42, login: 'ccweb-bot' },
|
|
}],
|
|
};
|
|
return { status: 'missing' };
|
|
},
|
|
startTurn: async ({ threadId, kind, hidden, prompt }) => {
|
|
retryCalls.push({ threadId, kind, hidden, prompt });
|
|
return { turnId: 'turn-retry-1' };
|
|
},
|
|
sendRestReply: async (input) => {
|
|
restCalls.push(input);
|
|
return { confirmed: true, commentId: 100 };
|
|
},
|
|
});
|
|
adapter.createTask({ taskId: 'task-1', sessionKey: 'default:r:issue:1', resourceKey: 'default:r:issue:1' });
|
|
adapter.attachThread('task-1', 'thread-1');
|
|
adapter.handleTurnStarted({ taskId: 'task-1', threadId: 'thread-1', turnId: 'turn-1' });
|
|
const retry = await adapter.handleTurnCompleted({ taskId: 'task-1', turnId: 'turn-1', finalText: '最终答案' });
|
|
assert.equal(retry.event, 'reply_retry_started');
|
|
assert.equal(retryCalls.length, 1);
|
|
assert.equal(retryCalls[0].threadId, 'thread-1');
|
|
assert.equal(retryCalls[0].kind, 'reply_retry');
|
|
assert.equal(retryCalls[0].hidden, true);
|
|
const confirmed = await adapter.handleTurnCompleted({ taskId: 'task-1', turnId: 'turn-retry-1', finalText: '最终答案' });
|
|
assert.equal(confirmed.event, 'reply_confirmed');
|
|
assert.equal(confirmed.source, 'mcp');
|
|
assert.equal(restCalls.length, 0);
|
|
|
|
let unknownRetry = 0;
|
|
let unknownRest = 0;
|
|
const unknown = createGiteaWorkflowCodex({
|
|
verifyReply: async () => ({ status: 'unknown' }),
|
|
startTurn: async () => { unknownRetry += 1; return { turnId: 'should-not-run' }; },
|
|
sendRestReply: async () => { unknownRest += 1; return { confirmed: true }; },
|
|
});
|
|
unknown.createTask({ taskId: 'task-unknown', sessionKey: 's', resourceKey: 'r' });
|
|
unknown.handleTurnStarted({ taskId: 'task-unknown', threadId: 'thread-u', turnId: 'turn-u' });
|
|
const unknownResult = await unknown.handleTurnCompleted({ taskId: 'task-unknown', turnId: 'turn-u', finalText: 'x' });
|
|
assert.equal(unknownResult.event, 'reply_verification_unknown');
|
|
assert.equal(unknownRetry, 0);
|
|
assert.equal(unknownRest, 0);
|
|
assert.equal(unknown.getTask('task-unknown').state, WORKFLOW_STATES.FAILED_REPLY);
|
|
|
|
const continuation = adapter.queueWaitingUserComment({
|
|
taskId: 'task-1',
|
|
nextTaskId: 'task-2',
|
|
comment: { id: 7, body: '@ccweb-bot 请继续', user: { id: 9, login: 'alice' } },
|
|
});
|
|
assert.equal(continuation.threadId, 'thread-1');
|
|
assert.equal(adapter.getTask('task-2').parentTaskId, 'task-1');
|
|
|
|
const waitingAdapter = createGiteaWorkflowCodex({ botIdentity: bot });
|
|
waitingAdapter.createTask({ taskId: 'task-wait', sessionKey: 's-wait', resourceKey: 'r-wait', threadId: 'thread-wait' });
|
|
waitingAdapter.handleTurnStarted({ taskId: 'task-wait', threadId: 'thread-wait', turnId: 'turn-wait' });
|
|
const waiting = waitingAdapter.handleTurnEvent({
|
|
taskId: 'task-wait',
|
|
notification: { method: 'item/tool/requestUserInput', params: { turnId: 'turn-wait', questions: [{ id: 'q1' }] } },
|
|
});
|
|
assert.equal(waiting.event, 'waiting_user');
|
|
assert.equal(waitingAdapter.getTask('task-wait').state, WORKFLOW_STATES.WAITING_USER);
|
|
const waitingContinuation = waitingAdapter.queueWaitingUserComment({
|
|
taskId: 'task-wait', nextTaskId: 'task-wait-2',
|
|
comment: { id: 10, body: '@ccweb-bot 答案是 main', user: { id: 9, login: 'alice' } },
|
|
});
|
|
assert.equal(waitingContinuation.threadId, 'thread-wait');
|
|
assert.equal(adapter.queueWaitingUserComment({
|
|
taskId: 'task-1', nextTaskId: 'task-bot',
|
|
comment: { id: 8, body: 'bot', user: { id: 42, login: 'ccweb-bot' } },
|
|
}).ignored, true);
|
|
assert.equal(adapter.queueWaitingUserComment({
|
|
taskId: 'task-1', nextTaskId: 'task-marker',
|
|
comment: { id: 9, body: `重复回执 ${buildWorkflowMarker({ taskId: 'task-1', turnId: 'turn-1', resourceKey: 'default:r:issue:1' })}`, user: { id: 9, login: 'alice' } },
|
|
}).ignored, true);
|
|
|
|
console.log('Gitea Workflow Codex unit checks passed.');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error.message || error);
|
|
process.exitCode = 1;
|
|
});
|