mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 03:00:26 +01:00
* Load the Capy crew skills only on Capy machines (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Add changelog entry for Capy-only crew skills (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Move the Capy-only crew tooling into crew/ and run its tests only when needed (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Test that the Capy launch file puts crew/bin on PATH Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Count any ~/.capy as a Capy machine, so a Mac running Capy keeps /captain Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Fix the tests and fixtures that broke when crew tooling moved into crew/ Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Plan every crew test when a pull request's merge-base can't be read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Add a test that checks each crew-only module left the root tree for crew/ Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Run crew tests on pull requests that change bin/ commands or workflows they drive Review-Finding: p1/specialist-batched#1 reader=testing Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep running every crew test on every pull request; the skip comes in a later change Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep copied installs and older release bisects working after the crew/ move Setup now copies crew/lib into every host's runtime root beside bin and lib, so Windows installs (which copy instead of linking) can still start gstack-ship-receipt, gstack-pregate and the other commands that import it. The release bisect runs the train-gate script from wherever the checked-out commit has it, so a bisect over commits from before the move still gets a verdict. Review-Finding: p1/outside-voice#1 reader=outside-voice Review-Finding: p1/outside-voice#2 reader=outside-voice Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Say in the changelog that Windows installs keep working after the crew/ move Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Point the harness guide's merge-queue rows at crew/lib/ where those modules moved Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Point the plain-title test at the wave module's new home in crew/ (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
104 lines
6.6 KiB
TypeScript
104 lines
6.6 KiB
TypeScript
/**
|
|
* GST-1176 (HARN-7, harness-evals): a release_tool that renders content, not just the version, writes release
|
|
* outputs whose hunks are more than version rewrites. The tree receipt classified them `changed`, so a stamp
|
|
* over an unstamped merge was never same-modulo-stamps. With --allow-repo-commands the receipt accepts a release
|
|
* output whose bytes equal release_tool's rerun on the current head at its version, the Version Gate's rule.
|
|
*/
|
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { loadPolicyFromHead } from '../lib/ship-policy';
|
|
import { stampMismatch } from '../crew/lib/ship-queue-train-stamp';
|
|
import { BASE_POLICY, cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
|
|
|
const repos: FixtureRepo[] = [];
|
|
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
|
|
|
const FOLD = "const fs=require('fs');const v=fs.readFileSync('VERSION','utf8').trim();const items=fs.readdirSync('todos.d').filter(f=>f.endsWith('.md')).sort().map(f=>fs.readFileSync('todos.d/'+f,'utf8'));fs.writeFileSync('TODOS.md','# TODOS v'+v+'\\n'+items.join(''));\n";
|
|
const POLICY = { ...BASE_POLICY, release_tool: 'node fold.js', release_outputs: ['TODOS.md'] };
|
|
|
|
/** HARN-7's shape: M is the unstamped merge of a PR that adds only a todos.d/ file; S stamps it with the tool's output. */
|
|
function harn7(policy: Record<string, unknown> = POLICY): { repo: FixtureRepo; merged: string; stamped: string } {
|
|
const repo = makeFixtureRepo({ prefix: 'tree-receipt-release-tool-', policy });
|
|
repos.push(repo);
|
|
const fold = () => expect(spawnSync('node', ['fold.js'], { cwd: repo.work, timeout: 30_000 }).status).toBe(0);
|
|
fs.mkdirSync(path.join(repo.work, 'todos.d'));
|
|
fs.writeFileSync(path.join(repo.work, 'fold.js'), FOLD);
|
|
fs.writeFileSync(path.join(repo.work, 'todos.d', 'base.md'), '- base todo\n');
|
|
fold();
|
|
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'todos fold');
|
|
fs.writeFileSync(path.join(repo.work, 'todos.d', 'new.md'), '- new todo\n');
|
|
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'merge: a PR that adds only a todos.d file');
|
|
const merged = repo.git('rev-parse', 'HEAD');
|
|
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.4.0\n');
|
|
fold();
|
|
repo.git('commit', '-qam', 'stamp 1.2.4.0');
|
|
return { repo, merged, stamped: repo.git('rev-parse', 'HEAD') };
|
|
}
|
|
|
|
const receipt = (repo: FixtureRepo, args: string[]) => runBin(repo, 'gstack-tree-receipt', ['--base', 'main', ...args]);
|
|
|
|
describe('tree receipt: a release output release_tool regenerates (GST-1176)', () => {
|
|
test('a rendered release output is a stamp when release_tool reruns to the same bytes, and only with --allow-repo-commands', () => {
|
|
const { repo, merged, stamped } = harn7();
|
|
expect(repo.git('show', `${stamped}:TODOS.md`)).toBe('# TODOS v1.2.4.0\n- base todo\n- new todo');
|
|
expect(receipt(repo, ['--gated', merged, '--current', stamped]).out).toMatch(/^TREE: changed \(TODOS\.md\)$/m);
|
|
const r = receipt(repo, ['--gated', merged, '--current', stamped, '--allow-repo-commands']);
|
|
expect(r.out).toMatch(/^ stamp VERSION \(version-file\)$/m);
|
|
expect(r.out).toMatch(/^ stamp TODOS\.md \(release_tool\)$/m);
|
|
expect(r.out).toMatch(/^TREE: same-modulo-stamps$/m);
|
|
});
|
|
|
|
test('a release output the tool would not write stays changed, with the regeneration named', () => {
|
|
const { repo, merged } = harn7();
|
|
fs.appendFileSync(path.join(repo.work, 'TODOS.md'), '- a hand-written line\n');
|
|
repo.git('commit', '-qam', 'hand edit');
|
|
const j = JSON.parse(receipt(repo, ['--gated', merged, '--current', 'HEAD', '--allow-repo-commands', '--json']).out);
|
|
expect(j.tree).toBe('changed');
|
|
const todos = j.files.find((f: { path: string }) => f.path === 'TODOS.md');
|
|
expect(todos).toMatchObject({ verdict: 'changed', rule: 'release_outputs' });
|
|
expect(todos.residual).toContain('release_tool');
|
|
});
|
|
|
|
test('a release output the tool adds for the first time is a stamp when the rerun writes the same bytes', () => {
|
|
const repo = makeFixtureRepo({ prefix: 'tree-receipt-release-tool-', policy: POLICY });
|
|
repos.push(repo);
|
|
fs.mkdirSync(path.join(repo.work, 'todos.d'));
|
|
fs.writeFileSync(path.join(repo.work, 'fold.js'), FOLD);
|
|
fs.writeFileSync(path.join(repo.work, 'todos.d', 'new.md'), '- new todo\n');
|
|
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'merge: the first todos.d file, TODOS.md not rendered yet');
|
|
const merged = repo.git('rev-parse', 'HEAD');
|
|
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.4.0\n');
|
|
expect(spawnSync('node', ['fold.js'], { cwd: repo.work, timeout: 30_000 }).status).toBe(0);
|
|
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'stamp 1.2.4.0');
|
|
expect(receipt(repo, ['--gated', merged, '--current', 'HEAD']).out).toMatch(/^ changed TODOS\.md \(path: added\)$/m);
|
|
const r = receipt(repo, ['--gated', merged, '--current', 'HEAD', '--allow-repo-commands']);
|
|
expect(r.out).toMatch(/^ stamp TODOS\.md \(release_tool\)$/m);
|
|
expect(r.out).toMatch(/^TREE: same-modulo-stamps$/m);
|
|
});
|
|
|
|
test('a release_tool that fails leaves the release output changed, with its exit named', () => {
|
|
const { repo, merged, stamped } = harn7({ ...POLICY, release_tool: 'node fold.js && exit 3' });
|
|
const j = JSON.parse(receipt(repo, ['--gated', merged, '--current', stamped, '--allow-repo-commands', '--json']).out);
|
|
expect(j.tree).toBe('changed');
|
|
const todos = j.files.find((f: { path: string }) => f.path === 'TODOS.md');
|
|
expect(todos).toMatchObject({ verdict: 'changed', rule: 'release_outputs' });
|
|
expect(todos.residual).toContain('exited 3');
|
|
});
|
|
|
|
test('a stamped train prefix over the merge passes stampMismatch only when the train runs repo commands', () => {
|
|
const { repo, merged, stamped } = harn7();
|
|
const opts = { repoRoot: repo.work, base: 'main', policy: loadPolicyFromHead(repo.work).policy, policyLabel: 'head', baseSha: merged, slots: [], plain: [], now: new Date() };
|
|
const plainTree = repo.git('rev-parse', `${merged}^{tree}`);
|
|
const prior = Object.fromEntries(Object.keys(repo.env).map(k => [k, process.env[k]]));
|
|
Object.assign(process.env, repo.env);
|
|
try {
|
|
expect(stampMismatch({ ...opts, allowRepoCommands: true }, merged, stamped, plainTree)).toBeNull();
|
|
expect(stampMismatch({ ...opts, allowRepoCommands: false }, merged, stamped, plainTree)).toContain('TODOS.md');
|
|
} finally {
|
|
for (const [k, v] of Object.entries(prior)) if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
}
|
|
});
|
|
});
|