Files
cc-web/scripts/gitea-workflow-management-unit.js

127 lines
7.2 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Readable } = require('stream');
const {
createGiteaWorkflowManagement,
handleGiteaWorkflowManagementApi,
} = require('../lib/gitea-workflow-management');
function request(method, body = null) {
const stream = Readable.from(body == null ? [] : [Buffer.from(JSON.stringify(body))]);
stream.method = method;
return stream;
}
function response() {
return {
statusCode: 0,
headersSent: false,
headers: {},
writeHead(code, headers) { this.statusCode = code; this.headers = headers; },
end(value) { this.body = String(value || ''); this.headersSent = true; },
};
}
function url(value) { return new URL(`http://localhost${value}`); }
async function call(management, method, pathname, body, authenticated = true) {
const res = response();
await handleGiteaWorkflowManagementApi(request(method, body), res, url(pathname), management, {
authenticate: () => authenticated,
});
return { status: res.statusCode, body: JSON.parse(res.body || '{}') };
}
async function main() {
const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
const frontend = fs.readFileSync(path.join(__dirname, '..', 'public', 'gitea-workflow.js'), 'utf8');
assert(html.includes('gitea-workflow-open') && html.includes('gitea-workflow-panel'), '页面入口未挂接');
assert(html.includes('gitea-workflow.js?v=__CC_WEB_GITEA_WORKFLOW_ASSET_VERSION__'), 'Gitea 管理脚本应使用独立缓存版本');
assert(html.includes('gitea-workflow.css?v=__CC_WEB_GITEA_WORKFLOW_ASSET_VERSION__'), 'Gitea 管理样式应使用独立缓存版本');
for (const marker of ['仓库与工作区', '任务队列与 Turn', '运行日志', '操作记录', '暂停全局', '中止 Turn']) {
assert(frontend.includes(marker), `前端缺少 ${marker} 展示/控制`);
}
for (const marker of ['gitea-workflow-actor', 'gitea-workflow-reason', 'gitea-config-webhook-secret', 'gitea-config-concurrency', 'operatorReason', 'controlFields', '填写本次运维操作原因', '凭据仅写入 0600 运行目录,不回显;保存后重启 cc-web 才会应用到 Webhook/MCP。', '全局并发上限', 'Webhook Secret']) {
assert(!frontend.includes(marker), `前端不应再暴露 ${marker}`);
}
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccweb-gitea-workflow-'));
const statePath = path.join(tempDir, 'management.json');
const readOnly = createGiteaWorkflowManagement({ statePath });
const overview = await call(readOnly, 'GET', '/api/gitea-workflow/overview');
assert.strictEqual(overview.status, 200);
assert.strictEqual(overview.body.ok, true);
assert.strictEqual(overview.body.available, false);
assert.strictEqual(overview.body.summary.queued, 0);
const config = await call(readOnly, 'GET', '/api/gitea-workflow/config');
assert.strictEqual(config.status, 200);
assert.strictEqual(config.body.secrets.webhookSecret, undefined);
const invalidHost = await call(readOnly, 'PUT', '/api/gitea-workflow/config', {
host: 'https://user:password@gitea.example', actor: 'alice', reason: 'invalid host test',
});
assert.strictEqual(invalidHost.status, 400);
assert.strictEqual(invalidHost.body.code, 'invalid_gitea_host');
const missingReason = await call(readOnly, 'POST', '/api/gitea-workflow/control/pause', { actor: 'alice' });
assert.strictEqual(missingReason.status, 400);
assert.strictEqual(missingReason.body.code, 'reason_required');
const unauthorized = await call(readOnly, 'GET', '/api/gitea-workflow/overview', null, false);
assert.strictEqual(unauthorized.status, 401);
const unavailable = await call(readOnly, 'POST', '/api/gitea-workflow/control/pause', { actor: 'alice', reason: '维护' });
assert.strictEqual(unavailable.status, 503);
assert.strictEqual(unavailable.body.code, 'workflow_service_unavailable');
const calls = [];
const service = {
getOverview() { return { control: { globalPaused: false }, summary: {} }; },
listRepositories() { return [{ repoKey: 'default:acme/widget', owner: 'acme', name: 'widget', status: 'active', dirty: true, dirtyReason: '未提交修改' }]; },
listTasks() { return [
{ taskId: 'task-1', repoKey: 'default:acme/widget', state: 'queued', turnId: null },
{ taskId: 'task-blocked', repoKey: 'default:acme/widget', state: 'blocked_workspace', errorMessage: '目录有未提交修改' },
]; },
listAudit() { return []; },
listLogs() { return [{ level: 'info', message: 'queued' }]; },
pause(input) { calls.push(['pause', input]); return { globalPaused: true }; },
resume(input) { calls.push(['resume', input]); return { globalPaused: false }; },
disableRepository(input) { calls.push(['disable', input]); return { status: 'disabled' }; },
enableRepository(input) { calls.push(['enable', input]); return { status: 'active' }; },
cancelTask(input) { calls.push(['cancel', input]); return { state: 'cancelled' }; },
abortTask(input) { calls.push(['abort', input]); return { state: 'aborted' }; },
};
const management = createGiteaWorkflowManagement({ statePath, workflowService: service });
const listed = await call(management, 'GET', '/api/gitea-workflow/tasks?state=queued');
assert.strictEqual(listed.body.total, 1);
assert.strictEqual(listed.body.items[0].taskId, 'task-1');
const overviewWithDerivedFields = await call(management, 'GET', '/api/gitea-workflow/overview');
assert.strictEqual(overviewWithDerivedFields.body.summary.maxConcurrency, undefined);
assert.strictEqual(overviewWithDerivedFields.body.repositories[0].queuedCount, 1);
assert.strictEqual(overviewWithDerivedFields.body.repositories[0].dirty, true);
assert.match(overviewWithDerivedFields.body.repositories[0].dirtyReason, /未提交修改/);
assert(overviewWithDerivedFields.body.logs.some((entry) => entry.message === 'queued'));
for (const [route, expected] of [
['/control/pause', 'pause'], ['/control/resume', 'resume'],
['/repos/default%3Aacme%2Fwidget/disable', 'disable'], ['/repos/default%3Aacme%2Fwidget/enable', 'enable'],
['/tasks/task-1/cancel', 'cancel'], ['/tasks/task-1/abort', 'abort'],
]) {
const result = await call(management, 'POST', `/api/gitea-workflow${route}`, { actor: 'alice', reason: `test ${expected}`, requestId: expected });
assert.strictEqual(result.status, 200, expected);
assert.strictEqual(result.body.ok, true, expected);
}
assert.strictEqual(calls.length, 6);
const duplicate = await call(management, 'POST', '/api/gitea-workflow/control/pause', { actor: 'alice', reason: 'test pause', requestId: 'pause' });
assert.strictEqual(duplicate.status, 200);
assert.strictEqual(duplicate.body.idempotent, true);
assert.strictEqual(calls.length, 6, '重复 requestId 不应再次调用核心服务');
const audits = await call(management, 'GET', '/api/gitea-workflow/audit');
assert.strictEqual(audits.body.total, 6);
assert(audits.body.items.every((entry) => entry.actor === 'alice' && entry.reason.startsWith('test')));
const persisted = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(persisted.audits.length, 6);
console.log('gitea-workflow-management-unit: ok');
}
main().catch((error) => { console.error(error); process.exitCode = 1; });