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