mirror of
https://gh-proxy.org/https://github.com/klinsmaya/portal-template-default.git
synced 2026-11-10 18:17:16 +08:00
108 lines
7.4 KiB
JavaScript
108 lines
7.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { preparePresetIssue } from '../issue-presets.mjs';
|
|
import { SOURCE_BASELINE_DEFAULT, isSourceBaselineRef, isSharedTaskBase } from '../source-baseline-ref.mjs';
|
|
import { pinInitialBase } from '../task-base.mjs';
|
|
import { assertSafeChangedPaths } from '../factory-lib.mjs';
|
|
import { captureBaseline } from '../baseline-record.mjs';
|
|
import { sourceDescriptor } from '../source-candidate.mjs';
|
|
import { exactCreatorVersion, recordCreator } from '../template-creator.mjs';
|
|
|
|
const branch = `factory-baseline/source-${'a'.repeat(12)}-123-1`;
|
|
const human = { login: 'owner', type: 'User' }, bot = { login: 'github-actions[bot]', type: 'Bot' };
|
|
function fixture(selected = branch) {
|
|
const source = { number: 1, title: 'A small test', user: human, author_association: 'OWNER', labels: ['factory:preset'], html_url: 'https://github.com/owner/repo/issues/1',
|
|
body: '### 目标分支\n\nold\n\n### 任务类型\n\n创建新系统\n\n### 业务需求\n\n计数器\n\n### 验收要求\n\n0 → 1' };
|
|
const issue = { number: 2, title: 'rebuild', body: `### 预置案例\n\n#1 - A small test\n\n### 测试基线分支\n\n${selected}`, user: human, labels: [] };
|
|
const comments = [];
|
|
const client = {
|
|
repository: 'owner/repo',
|
|
getIssue: async n => structuredClone(n === 1 ? source : issue),
|
|
getRepository: async () => ({ default_branch: 'develop' }),
|
|
getRef: async value => value === branch ? { object: { sha: 'b'.repeat(40) } } : null,
|
|
async addComment(n, body) { assert.equal(n, 2); const comment = { id: comments.length + 100, user: bot, body }; comments.push(comment); return structuredClone(comment); },
|
|
async request(method, route, options = {}) {
|
|
if (method === 'GET' && route.endsWith('/comments')) return route === '/issues/1/comments' ? [] : structuredClone(comments);
|
|
if (method === 'PATCH' && route === '/issues/2') { Object.assign(issue, options.body); return structuredClone(issue); }
|
|
throw new Error(`Unexpected ${method} ${route}`);
|
|
},
|
|
};
|
|
return { source, issue, client, comments };
|
|
}
|
|
test('preset rebuilds start from the default branch and ignore a legacy baseline field and the source branch', async () => {
|
|
for (const selection of ['', '_No response_', branch, 'unknown']) {
|
|
const f = fixture(selection), result = await preparePresetIssue(f.client, f.issue);
|
|
assert.equal(result.task.targetBranch, 'develop');
|
|
assert.equal(f.source.body.includes('\nold\n'), true);
|
|
}
|
|
});
|
|
test('captured default branch and base SHA remain fixed after the default branch advances', async () => {
|
|
const f = fixture(''); const one = await preparePresetIssue(f.client, f.issue);
|
|
const original = 'b'.repeat(40);
|
|
assert.equal(await pinInitialBase(f.client, 2, 'develop', original), original);
|
|
f.client.getRepository = async () => ({ default_branch: 'new-default' });
|
|
const two = await preparePresetIssue(f.client, f.issue);
|
|
assert.equal(two.task.targetBranch, 'develop'); assert.equal(two.preset.inputHash, one.preset.inputHash);
|
|
assert.equal(await pinInitialBase(f.client, 2, 'develop', 'c'.repeat(40)), original);
|
|
});
|
|
test('only exact source baselines share independent-task behavior and descriptor is protected', () => {
|
|
assert.ok(isSourceBaselineRef(branch)); assert.ok(isSharedTaskBase(branch, 'develop'));
|
|
assert.ok(!isSourceBaselineRef('factory-baseline/source-latest'));
|
|
assert.ok(!isSharedTaskBase('apps/crm', 'develop'));
|
|
assert.throws(() => assertSafeChangedPaths(['factory-source.json']));
|
|
});
|
|
test('baseline records actual creator and source descriptor without guessing source from published versions', t => {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'selected-baseline-')); t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
writeFileSync(path.join(root, 'package.json'), '{}');
|
|
const template = path.join(root, 'factory-template.json'); writeFileSync(template, JSON.stringify({ templateVersion: '1.0.0-beta.43' }));
|
|
recordCreator(template, '0.1.0-beta.19');
|
|
assert.equal(captureBaseline(root).creatorVersion, '0.1.0-beta.19'); assert.equal(captureBaseline(root).source, null);
|
|
const value = sourceDescriptor({ repository: process.env.GITHUB_REPOSITORY || 'owner/repo', sourceSha: 'a'.repeat(40), runId: 123, attempt: 1 }, Buffer.from('safe bytes'));
|
|
writeFileSync(path.join(root, 'factory-source.json'), JSON.stringify(value));
|
|
assert.equal(captureBaseline(root).source.sha, value.sourceSha);
|
|
assert.throws(() => captureBaseline(root, { sourceSha: 'b'.repeat(40) }), /conflicts/);
|
|
assert.throws(() => exactCreatorVersion('latest')); assert.throws(() => exactCreatorVersion('1.0.0\nINJECT=1'));
|
|
});
|
|
test('all three clean runners restore the selected snapshot before installing and preserve final archive gates', () => {
|
|
const workflow = readFileSync(new URL('../../workflows/code-agent-task.yml', import.meta.url), 'utf8');
|
|
// verify-final and preview-build-failed restore it through the
|
|
// restore-task-toolchain composite action.
|
|
const toolchain = readFileSync(new URL('../../actions/restore-task-toolchain/action.yml', import.meta.url), 'utf8');
|
|
assert.equal((workflow.match(/source-snapshot.mjs restore workspace/g) ?? []).length, 1);
|
|
assert.equal((toolchain.match(/source-snapshot.mjs restore workspace/g) ?? []).length, 1);
|
|
for (const [name, next] of [['agent', 'verify-final'], ['verify-final', 'publish'], ['preview-build-failed', 'report-failure']]) {
|
|
const job = workflow.split(` ${name}:
|
|
`)[1].split(` ${next}:
|
|
`)[0];
|
|
const restore = name === 'agent'
|
|
? job.indexOf('source-snapshot.mjs restore workspace')
|
|
: job.indexOf('uses: ./factory-actions/.github/actions/restore-task-toolchain');
|
|
assert.ok(restore >= 0, `${name} restores its pinned source baseline`);
|
|
assert.ok(job.indexOf('pnpm install --frozen-lockfile') > restore, `${name} restores before installing`);
|
|
}
|
|
assert.match(workflow, /Capture the actual installed package and Skill baseline/);
|
|
assert.match(workflow, /FACTORY_BUILD_ARCHIVE: '1'/);
|
|
assert.doesNotMatch(workflow, /scripts\/utils\/pack-dist.mjs/);
|
|
const history = readFileSync(new URL('../agent-history.mjs', import.meta.url), 'utf8');
|
|
assert.match(history, /baseline/);
|
|
});
|
|
|
|
test('the source baseline workflow verifies SOURCE_BASELINE_DEFAULT when no SHA is requested', () => {
|
|
const workflow = readFileSync(new URL('../../workflows/source-baseline.yml', import.meta.url), 'utf8');
|
|
assert.match(SOURCE_BASELINE_DEFAULT, /^[a-f0-9]{40}$/u);
|
|
// A workflow_dispatch input default cannot read a file, so it repeats the constant.
|
|
const input = workflow.split(' source_sha:\n')[1].split(/\n {6}\w/)[0];
|
|
assert.match(input, new RegExp(`^ {8}default: ${SOURCE_BASELINE_DEFAULT}$`, 'm'));
|
|
assert.match(input, /source-baseline-ref\.mjs/);
|
|
// The pull request fallback imports it instead of repeating it.
|
|
const pin = workflow.split('- name: Validate the explicit source pin\n')[1].split(/\n {6}- (?=name:|uses:)/)[0];
|
|
assert.match(pin, /REQUESTED_SHA: \$\{\{ inputs\.source_sha \}\}\n/);
|
|
assert.match(pin, /import \{SOURCE_BASELINE_DEFAULT\} from '\.\/control\/\.github\/scripts\/source-baseline-ref\.mjs';/);
|
|
assert.match(pin, /const sha=process\.env\.REQUESTED_SHA\|\|SOURCE_BASELINE_DEFAULT;/);
|
|
// Action pins are @<sha>; the input default is the only bare 40-hex value.
|
|
assert.deepEqual(workflow.match(/(?<!@)\b[a-f0-9]{40}\b/g), [SOURCE_BASELINE_DEFAULT]);
|
|
});
|