127 lines
7.2 KiB
JavaScript
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; });
|