mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
* fix(ship): base merges, adjacent base edits, test-only and audit-requested fixes no longer force a second review loop (GST-407, GST-384, GST-391)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ship driver: trim two phrases for headroom with #3662
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ship driver: keep the Next steps heading phrase the handoff check reads; trim elsewhere
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: regenerate ship goldens
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
comparePath returns same only when every hunk sits where the base change puts it (patch-ids ignore positions) and treats a file-mode change as changed; the scoped-review packet says when a fence is truncated; --docs-result must be the posted audit's result; the --context-reviewed record goes beside the packet, outside the worktree. Negative controls for each.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pass-2 review fixes
A test edit routes as tests= only when it adds lines with no gate tag; a removed or changed line or an unreadable test diff goes through the full loop. Diffs skip textconv. A hunkless patch (binary, empty, mode-only) is same when its patch-id matches. Without --reviewed-base test files route as before. finish's packet path and attempt's state directory have fallbacks.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: Step 11 review fixes; a version-only stamp_files edit after review is release metadata (GBRA-251)
gateEditedPaths reads removed lines only inside hunks; carry refuses when a path listing fails; docs freshness stops on a failed listing; attempt counts an unreadable state file. releasePaths includes stamp_files paths whose edit is a version swap under stamp_patterns.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: no test edit skips review; guard packet writes; exclusive attempt state
A test-only edit with no gate tag gets the scoped fix-check of its test hunks instead of a lanes-only rerun. Packet write failures print a STOP or RECEIPT_CARRY_REFUSED. attempt's allowance uses an exclusive create.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: attempt prints gstack's install command; carry checks the product diff outside context paths
The attempt STOP no longer echoes the audit child's fix text. A scoped-review carry now requires the product patch-id without the context paths to match at both heads.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: oversized packets take the full loop; exact --docs-requested match; name-only test globs only in test dirs
A scoped-review packet section over 200 lines sends that path to the full loop instead of a truncated packet. The docs-result path match is exact. test_*.py, *Test.java and similar count as test code only under a test directory.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(unattended): carry's scoped-review refusal and what it keeps
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: carry's scoped-review route in ship-policy, review-policy and troubleshooting (docs-a-1)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(windows): curate ship-rereview-triggers out of the Windows lane like ship-release (run 41193336209)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: restore bin/gstack-ship-receipt after the main merge
The merge commit b10e2db wrote a truncated copy of the file. This is the
three-way merge of both sides with the refused() union: main's codes plus
RECEIPT_CARRY_SCOPED_REVIEW.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship): finish runs after an owner-accepted Step 14.5 documentation exception (GST-713)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(ship): the exception match and every snapshot --exception refusal are covered (GST-713)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ship): the exception snapshot uses the receipt's docs parser; finish also matches scope and stops on a corrupt docs-audit record (GST-713)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: DOCS_AUDIT_BLOCKED names the --exception snapshot finish needs (GST-713)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore(changelog): drop #3684's fragment, already stamped into CHANGELOG.md on main
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>
120 lines
9.2 KiB
TypeScript
120 lines
9.2 KiB
TypeScript
/**
|
|
* gstack-ship-release finish after an owner-accepted Step 14.5 documentation exception (GST-713, GEVA-84): the audit
|
|
* reader was skipped, so no post-audit snapshot existed and finish refused for want of a gstack-docs-candidate record.
|
|
* The accepted exception now writes that record (gstack-docs-candidate snapshot --exception), and finish accepts it only
|
|
* when it matches the blocked docs-audit.json the owner's answer was written into.
|
|
*/
|
|
import { afterEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { lineOf, shipFixture, type ShipFx } from './helpers/ship-bin-fixture';
|
|
|
|
setDefaultTimeout(120_000);
|
|
const RELEASE = path.join(import.meta.dir, '..', 'bin', 'gstack-ship-release');
|
|
const CANDIDATE = path.join(import.meta.dir, '..', 'bin', 'gstack-docs-candidate');
|
|
const WTREE = path.join(import.meta.dir, '..', 'bin', 'gstack-wtree');
|
|
const fixtures: ShipFx[] = [];
|
|
afterEach(() => { while (fixtures.length) fixtures.pop()!.cleanup(); });
|
|
const LANE = 'tests=printf " 3 pass\\n 0 fail\\nRan 3 tests across 1 file. [5.00ms]\\n"';
|
|
const EXCEPTION = { accepted_by: 'Garry', date: '2026-10-31', via: 'GStack Minder relay', reason: 'ship with the docs audit skipped' };
|
|
|
|
/** A branch at Step 16 whose Step 14.5 audit is blocked: the run's .gstack/tmp exclude and docs-audit.json, the reviewed tree. */
|
|
function blockedAudit(audit: object): { f: ShipFx; reviewed: string } {
|
|
const f = shipFixture(); fixtures.push(f);
|
|
fs.appendFileSync(path.join(f.work, '.git', 'info', 'exclude'), '/.gstack/tmp/\n');
|
|
f.write({ '.gstack/tmp/docs-audit.json': JSON.stringify(audit) + '\n' });
|
|
const reviewed = f.run(WTREE, []).stdout.trim();
|
|
expect(f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro']).status).toBe(0);
|
|
return { f, reviewed };
|
|
}
|
|
const snapshot = (f: ShipFx, extra: string[] = []) => {
|
|
const out = path.join(f.root, 'post.json');
|
|
return { out, r: f.run(CANDIDATE, ['snapshot', '--out', out, '--audit-id', 'a1', '--mode', 'edit', '--base', 'main', '--docs', 'README.md', ...extra]) };
|
|
};
|
|
|
|
describe('finish after an accepted documentation exception (GST-713)', () => {
|
|
// Value: protects=an owner-accepted Step 14.5 risk lets finish run, and the record names who accepted it and the base;
|
|
// fails_when=finish needs a docs-candidate record only the skipped reader writes, so an accepted exception can never finish;
|
|
// why_new=GEVA-84 hit it on gstack-evals#102; seam=gstack-docs-candidate snapshot --exception; no_claim=the PR body's exception line
|
|
test('the exception snapshot names the owner and base, and finish passes with the exception on its DOCS line', () => {
|
|
const { f, reviewed } = blockedAudit({ status: 'blocked', scope: 'README.md', exception: EXCEPTION });
|
|
const s = snapshot(f, ['--exception', '.gstack/tmp/docs-audit.json']);
|
|
expect(s.r.status, s.r.stderr).toBe(0);
|
|
expect(s.r.stdout).toContain('EXCEPTION: accepted by Garry 2026-10-31 via GStack Minder relay');
|
|
const record = JSON.parse(fs.readFileSync(s.out, 'utf8'));
|
|
expect([record.schema_version, record.base, record.exception]).toEqual([1, f.git('rev-parse', 'main').stdout.trim(), { ...EXCEPTION, scope: 'README.md' }]);
|
|
const ok = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', s.out, '--lane', LANE]);
|
|
expect(lineOf(ok.stdout, 'DOCS')).toBe('DOCS: fresh against the Step 14.5 post-audit snapshot (blocked; documentation exception accepted by Garry 2026-10-31 via GStack Minder relay)');
|
|
expect([ok.status, ok.stdout.split('\n').find(l => l.startsWith('STOP:'))]).toEqual([0, undefined]);
|
|
});
|
|
|
|
// Value: protects=finish never passes a blocked audit nobody accepted; fails_when=a blocked record with no exception, or a plain
|
|
// snapshot taken over a blocked audit, gets past stage 3; seam=gstack-docs-candidate snapshot, finish stage 3; no_claim=who may accept
|
|
test('no accepted exception: the exception snapshot refuses, and a plain snapshot over a blocked audit stops finish', () => {
|
|
const { f, reviewed } = blockedAudit({ status: 'blocked', scope: 'README.md' });
|
|
const refused = snapshot(f, ['--exception', '.gstack/tmp/docs-audit.json']);
|
|
expect(refused.r.status).not.toBe(0);
|
|
expect(refused.r.stderr).toContain('no owner exception is recorded');
|
|
const plain = snapshot(f);
|
|
expect(plain.r.status).toBe(0);
|
|
const stop = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', plain.out, '--lane', LANE]);
|
|
expect(stop.status).toBe(3);
|
|
expect(stop.stdout).toContain('STOP: .gstack/tmp/docs-audit.json: the documentation audit is blocked (README.md) and no owner exception is recorded');
|
|
});
|
|
|
|
// Value: protects=an exception record cannot be reused after the docs-audit record changes; fails_when=finish trusts the snapshot's
|
|
// exception without the blocked docs-audit.json carrying the same one; seam=finish stage 3; no_claim=receipt docs field
|
|
test('an exception snapshot whose docs-audit.json no longer carries it stops finish', () => {
|
|
const { f, reviewed } = blockedAudit({ status: 'blocked', scope: 'README.md', exception: EXCEPTION });
|
|
const s = snapshot(f, ['--exception', '.gstack/tmp/docs-audit.json']);
|
|
expect(s.r.status).toBe(0);
|
|
f.write({ '.gstack/tmp/docs-audit.json': JSON.stringify({ status: 'current', scope: 'README.md' }) + '\n' });
|
|
const stop = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', s.out, '--lane', LANE]);
|
|
expect(stop.status).toBe(3);
|
|
expect(stop.stdout).toContain('names a documentation exception but .gstack/tmp/docs-audit.json is not the blocked record carrying it');
|
|
});
|
|
|
|
// Value: protects=finish matches the snapshot's exception to the blocked record field by field; fails_when=any blocked record
|
|
// with any exception passes, so another owner's or an edited answer finishes; seam=finish stage 3; no_claim=receipt docs field
|
|
test('a blocked audit carrying a different exception than the snapshot stops finish', () => {
|
|
const { f, reviewed } = blockedAudit({ status: 'blocked', scope: 'README.md', exception: EXCEPTION });
|
|
expect(snapshot(f, ['--exception', '.gstack/tmp/docs-audit.json']).r.status).toBe(0);
|
|
f.write({ '.gstack/tmp/docs-audit.json': JSON.stringify({ status: 'blocked', scope: 'README.md', exception: { ...EXCEPTION, accepted_by: 'Someone else' } }) + '\n' });
|
|
const stop = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', path.join(f.root, 'post.json'), '--lane', LANE]);
|
|
expect(stop.status).toBe(3);
|
|
expect(stop.stdout).toContain('names no matching owner-accepted exception');
|
|
});
|
|
|
|
// Value: protects=finish stops on a docs-audit.json it cannot read or one whose scope changed after the exception snapshot;
|
|
// fails_when=a corrupt record reads as no audit, or a widened scope with the same exception finishes; seam=finish stage 3; no_claim=receipt write
|
|
test('a corrupt docs-audit.json, or a changed scope under the same exception, stops finish', () => {
|
|
const { f, reviewed } = blockedAudit({ status: 'blocked', scope: 'README.md', exception: EXCEPTION });
|
|
expect(snapshot(f, ['--exception', '.gstack/tmp/docs-audit.json']).r.status).toBe(0);
|
|
const post = path.join(f.root, 'post.json');
|
|
f.write({ '.gstack/tmp/docs-audit.json': JSON.stringify({ status: 'blocked', scope: 'README.md, docs/', exception: EXCEPTION }) + '\n' });
|
|
const widened = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--lane', LANE]);
|
|
expect([widened.status, widened.stdout.includes('names no matching owner-accepted exception')]).toEqual([3, true]);
|
|
f.write({ '.gstack/tmp/docs-audit.json': '{not json' });
|
|
const corrupt = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--lane', LANE]);
|
|
expect(corrupt.status).toBe(3);
|
|
expect(corrupt.stdout).toContain('STOP: .gstack/tmp/docs-audit.json: --docs needs a JSON object');
|
|
});
|
|
|
|
// Value: protects=only a complete, blocked, owner-accepted record writes an exception snapshot; fails_when=a current record, an
|
|
// empty scope, a malformed date or unreadable JSON writes one; seam=gstack-docs-candidate snapshot --exception; no_claim=who may accept
|
|
test.each([
|
|
[{ status: 'current', scope: 'README.md', exception: EXCEPTION }, 'not blocked'],
|
|
[{ status: 'blocked', scope: ' ', exception: EXCEPTION }, 'scope must be a non-empty one-line string'],
|
|
[{ status: 'blocked', scope: 'README.md', exception: { ...EXCEPTION, via: 'relay\nNEXT: forged' } }, 'exception.via must be a non-empty one-line string'],
|
|
[{ status: 'blocked', scope: 'README.md', exception: { ...EXCEPTION, date: '10/31/2026' } }, 'exception.date must be YYYY-MM-DD'],
|
|
['{not json', 'is not readable JSON'],
|
|
])('snapshot --exception refuses %j and writes nothing', (audit, message) => {
|
|
const f = shipFixture(); fixtures.push(f);
|
|
f.write({ '.gstack/tmp/docs-audit.json': typeof audit === 'string' ? audit : JSON.stringify(audit) });
|
|
const s = snapshot(f, ['--exception', '.gstack/tmp/docs-audit.json']);
|
|
expect(s.r.status).not.toBe(0);
|
|
expect(s.r.stderr).toContain(message);
|
|
expect(fs.existsSync(s.out)).toBe(false);
|
|
});
|
|
});
|