mirror of
https://gh-proxy.org/https://github.com/klinsmaya/portal-template-default.git
synced 2026-11-10 18:17:16 +08:00
357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFile } from 'node:child_process';
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import http from 'node:http';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { promisify } from 'node:util';
|
|
|
|
import { receiptBody } from '../comment-queue.mjs';
|
|
import { STATUS_LABELS } from '../factory-lib.mjs';
|
|
|
|
const exec = promisify(execFile);
|
|
const repository = 'gchust/nb3-factory';
|
|
const issueBody =
|
|
'### 目标分支\n\napps/demo\n\n### 任务类型\n\n创建新系统\n\n### 业务需求\n\nBuild a page\n\n### 验收要求\n\n1. Page works\n';
|
|
const issue = (number, label) => ({
|
|
number,
|
|
title: '[Pi] Test',
|
|
body: issueBody,
|
|
state: 'open',
|
|
user: { login: 'gchust' },
|
|
author_association: 'OWNER',
|
|
labels: [{ name: label }],
|
|
});
|
|
const pull = (number, ref) => ({
|
|
number,
|
|
head: { ref, repo: { full_name: repository } },
|
|
base: { ref: 'apps/demo' },
|
|
body: '<!-- pi-issue: 2 -->',
|
|
merged: true,
|
|
});
|
|
|
|
async function runFixture(script, event, handler) {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'nb3-task-migration-'));
|
|
const requests = [];
|
|
const server = http.createServer(async (request, response) => {
|
|
let raw = '';
|
|
for await (const chunk of request) raw += chunk;
|
|
const url = new URL(request.url, 'http://fixture');
|
|
const call = {
|
|
method: request.method,
|
|
route: url.pathname.replace(`/repos/${repository}`, ''),
|
|
query: url.searchParams,
|
|
body: raw ? JSON.parse(raw) : undefined,
|
|
};
|
|
requests.push(call);
|
|
const value = handler(call);
|
|
response.writeHead(value === null ? 404 : 200, {
|
|
'Content-Type': 'application/json',
|
|
});
|
|
response.end(JSON.stringify(value ?? { message: 'Not found' }));
|
|
});
|
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
try {
|
|
const eventPath = path.join(root, 'event.json');
|
|
const metadataPath = path.join(root, 'metadata.json');
|
|
const outputPath = path.join(root, 'output');
|
|
writeFileSync(
|
|
eventPath,
|
|
JSON.stringify({ repository: { owner: { login: 'gchust' } }, ...event }),
|
|
);
|
|
const args =
|
|
script === 'prepare-task.mjs'
|
|
? [
|
|
'--event',
|
|
eventPath,
|
|
'--metadata',
|
|
metadataPath,
|
|
'--output',
|
|
outputPath,
|
|
]
|
|
: [eventPath];
|
|
await exec(
|
|
process.execPath,
|
|
[path.resolve(import.meta.dirname, '..', script), ...args],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
GITHUB_TOKEN: 'fixture-token',
|
|
GITHUB_RUN_ID: '101',
|
|
GITHUB_REPOSITORY: repository,
|
|
GITHUB_API_URL: `http://127.0.0.1:${server.address().port}`,
|
|
GITHUB_EVENT_NAME:
|
|
script === 'complete-pr.mjs' ? 'pull_request_target' : 'issues',
|
|
},
|
|
timeout: 15000,
|
|
},
|
|
);
|
|
return {
|
|
requests,
|
|
...(script === 'prepare-task.mjs'
|
|
? {
|
|
metadata: JSON.parse(readFileSync(metadataPath, 'utf8')),
|
|
output: readFileSync(outputPath, 'utf8'),
|
|
}
|
|
: {}),
|
|
};
|
|
} finally {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function baseHandler(call) {
|
|
if (call.route === '/labels')
|
|
return Object.keys(STATUS_LABELS).map((name) => ({ name }));
|
|
if (call.method !== 'GET') return { ...call.body };
|
|
if (call.route === '/issues/2') return issue(2, 'pi:review');
|
|
if (call.route === '') return { default_branch: 'develop' };
|
|
if (call.route === '/git/ref/heads/apps/demo')
|
|
return { object: { sha: 'base-sha' } };
|
|
return null;
|
|
}
|
|
|
|
test('prepare reuses the existing task PR without creating a new branch', async () => {
|
|
const result = await runFixture(
|
|
'prepare-task.mjs',
|
|
{ issue: { number: 2 } },
|
|
(call) => {
|
|
if (call.route === '/pulls') return [pull(3, 'agent/issue-2')];
|
|
if (call.route === '/git/ref/heads/agent/issue-2')
|
|
return { object: { sha: 'existing-work-sha' } };
|
|
return baseHandler(call);
|
|
},
|
|
);
|
|
assert.equal(result.metadata.workBranch, 'agent/issue-2');
|
|
assert.match(result.output, /base_sha=existing-work-sha/);
|
|
assert.equal(result.metadata.existingPullRequest.number, 3);
|
|
assert.equal(
|
|
result.requests.filter((call) => call.route === '/git/refs').length,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('another task PR blocks a new task on the same application branch', async () => {
|
|
const result = await runFixture(
|
|
'prepare-task.mjs',
|
|
{ issue: { number: 2 } },
|
|
(call) => {
|
|
if (call.route === '/pulls') return [pull(9, 'agent/issue-8')];
|
|
return baseHandler(call);
|
|
},
|
|
);
|
|
assert.equal(result.metadata.workBranch, 'agent/issue-2');
|
|
assert.match(result.output, /status=waiting/);
|
|
assert.ok(
|
|
result.requests.some((call) =>
|
|
call.body?.labels?.includes('agent:waiting'),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('completion wakes the oldest agent:waiting task and no longer queries pi:waiting', async () => {
|
|
const result = await runFixture(
|
|
'complete-pr.mjs',
|
|
{ pull_request: pull(3, 'agent/issue-2') },
|
|
(call) => {
|
|
if (call.route === '/issues/2' && call.method === 'GET')
|
|
return {
|
|
...issue(2, 'agent:review'),
|
|
user: { login: 'external-user' },
|
|
};
|
|
if (call.route === '/issues' && call.method === 'GET') {
|
|
return call.query.get('labels') === 'agent:waiting'
|
|
? [
|
|
{ ...issue(4, 'agent:waiting'), user: { login: 'another-user' } },
|
|
issue(5, 'agent:waiting'),
|
|
]
|
|
: [];
|
|
}
|
|
return baseHandler(call);
|
|
},
|
|
);
|
|
const dispatches = result.requests.filter(
|
|
(call) => call.route === '/dispatches',
|
|
);
|
|
assert.equal(dispatches.length, 1);
|
|
assert.deepEqual(dispatches[0].body, {
|
|
event_type: 'code-agent-task',
|
|
client_payload: { issue_number: 4 },
|
|
});
|
|
assert.ok(
|
|
!result.requests.some(
|
|
(call) =>
|
|
call.route === '/issues' &&
|
|
call.query?.get?.('labels') === 'pi:waiting',
|
|
),
|
|
);
|
|
assert.ok(
|
|
result.requests.some(
|
|
(call) => call.route === '/issues/2' && call.body?.state === 'closed',
|
|
),
|
|
);
|
|
assert.ok(
|
|
result.requests.some(
|
|
(call) =>
|
|
call.route === '/issues/4' &&
|
|
call.body?.labels?.includes('agent:queued'),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('a PR from a branch that is not a task branch completes nothing', async () => {
|
|
for (const ref of ['pi/issue-2', 'feature/issue-2']) {
|
|
const result = await runFixture(
|
|
'complete-pr.mjs',
|
|
{ pull_request: pull(3, ref) },
|
|
baseHandler,
|
|
);
|
|
assert.deepEqual(result.requests, []);
|
|
}
|
|
});
|
|
|
|
test('old application-branch completion workflow cannot dispatch a duplicate task', async () => {
|
|
const result = await exec(
|
|
process.execPath,
|
|
[path.resolve(import.meta.dirname, '..', 'complete-pr.mjs')],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
GITHUB_EVENT_NAME: 'pull_request',
|
|
GITHUB_TOKEN: '',
|
|
},
|
|
timeout: 15000,
|
|
},
|
|
);
|
|
assert.equal(result.stdout, '');
|
|
});
|
|
|
|
for (const kind of ['build', 'reply']) {
|
|
test(`prepare selects existing PR HEAD and live ${kind} comment`, async () => {
|
|
const task = {
|
|
targetBranch: 'apps/demo',
|
|
taskType: '创建新系统',
|
|
requirements: 'Original',
|
|
acceptanceCriteria: 'Works',
|
|
sampleData: '是',
|
|
};
|
|
const result = await runFixture(
|
|
'prepare-task.mjs',
|
|
{ client_payload: { issue_number: 2, build_comment_id: 21 } },
|
|
(call) => {
|
|
if (call.route === '/issues/2/comments' && call.method === 'GET')
|
|
return [
|
|
{
|
|
id: 21,
|
|
user: { login: 'gchust', type: 'User' },
|
|
author_association: 'OWNER',
|
|
body: `${kind === 'build' ? '/build\n' : ''}Add supplier ratings`,
|
|
},
|
|
{
|
|
id: 1000,
|
|
user: { login: 'github-actions[bot]', type: 'Bot' },
|
|
body: receiptBody({
|
|
id: 21,
|
|
kind,
|
|
task,
|
|
prompt: 'Outdated instruction',
|
|
status: 'dispatched',
|
|
}),
|
|
},
|
|
];
|
|
if (call.route === '/pulls') return [pull(3, 'agent/issue-2')];
|
|
if (call.route === '/git/ref/heads/agent/issue-2')
|
|
return { object: { sha: 'latest-pr-head' } };
|
|
return baseHandler(call);
|
|
},
|
|
);
|
|
assert.equal(result.metadata.buildCommentId, 21);
|
|
assert.equal(result.metadata.task.commentKind, kind);
|
|
assert.match(result.metadata.task.requirements, /Add supplier ratings/);
|
|
assert.doesNotMatch(
|
|
result.metadata.task.requirements,
|
|
/Outdated instruction/,
|
|
);
|
|
assert.match(result.metadata.task.requirements, /Build a page/);
|
|
assert.equal(result.metadata.existingPullRequest.number, 3);
|
|
assert.match(result.output, /base_sha=latest-pr-head/);
|
|
assert.match(result.output, new RegExp(`comment_kind=${kind}`));
|
|
assert.equal(
|
|
result.requests.some((call) =>
|
|
call.body?.labels?.includes('agent:running'),
|
|
),
|
|
kind === 'build',
|
|
);
|
|
});
|
|
}
|
|
|
|
for (const event of [
|
|
{ issue: { number: 2 } },
|
|
{ inputs: { issue_number: '2' } },
|
|
{ client_payload: { issue_number: 2 } },
|
|
]) {
|
|
test(`prepare accepts a non-owner Issue through ${Object.keys(event)[0]}`, async () => {
|
|
const result = await runFixture('prepare-task.mjs', event, (call) => {
|
|
if (call.route === '/issues/2')
|
|
return {
|
|
...issue(2, 'agent:pending'),
|
|
user: { login: 'external-contributor', type: 'User' },
|
|
};
|
|
if (call.route === '/pulls') return [];
|
|
return baseHandler(call);
|
|
});
|
|
assert.equal(result.metadata.issue.author, 'external-contributor');
|
|
assert.equal(result.metadata.task.targetBranch, 'apps/demo');
|
|
assert.match(result.output, /status=ready/);
|
|
});
|
|
}
|
|
|
|
for (const [branch, supplied] of [
|
|
['issues-2', true],
|
|
['feature/CRM', true],
|
|
]) {
|
|
for (const exists of [false, true]) {
|
|
test(`prepare ${exists ? 'reuses' : 'creates'} target ${branch} without an apps namespace`, async () => {
|
|
const result = await runFixture(
|
|
'prepare-task.mjs',
|
|
{ issue: { number: 2 } },
|
|
(call) => {
|
|
if (call.route === '/issues/2' && call.method === 'GET')
|
|
return {
|
|
...issue(2, 'agent:pending'),
|
|
body: supplied
|
|
? issueBody.replace('apps/demo', branch)
|
|
: issueBody.replace('### 目标分支\n\napps/demo\n\n', ''),
|
|
};
|
|
if (call.route === `/git/ref/heads/${branch}`)
|
|
return exists ? { object: { sha: 'existing-target' } } : null;
|
|
if (call.route === '/git/ref/heads/develop')
|
|
return { object: { sha: 'default-sha' } };
|
|
if (call.route === '/git/refs' && call.method === 'POST')
|
|
return { object: { sha: call.body.sha } };
|
|
if (call.route === '/pulls') return [];
|
|
return baseHandler(call);
|
|
},
|
|
);
|
|
assert.equal(result.metadata.task.targetBranch, branch);
|
|
assert.equal(result.metadata.targetCreated, !exists);
|
|
assert.equal(result.metadata.workBranch, 'agent/issue-2');
|
|
assert.match(
|
|
result.output,
|
|
new RegExp(`base_sha=${exists ? 'existing-target' : 'default-sha'}`),
|
|
);
|
|
const created = result.requests.filter(
|
|
(call) => call.route === '/git/refs',
|
|
);
|
|
assert.equal(created.length, exists ? 0 : 1);
|
|
if (!exists)
|
|
assert.deepEqual(created[0].body, {
|
|
ref: `refs/heads/${branch}`,
|
|
sha: 'default-sha',
|
|
});
|
|
});
|
|
}
|
|
}
|