mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 18:50:18 +01:00
Count a granted follow-up review whose range starts after a docs-only commit (GST-2294) (#4278)
* Count a fix-check whose range starts after a docs-only commit (GST-2294) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Name the follow-up review rule in plain words in the changelog (GST-2294) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Bridge a docs-only gap only through paths the router itself treats as release metadata (GST-2294) 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>
This commit is contained in:
1 parent
6afe390244
commit
d5f7ab5f20
5 files changed
+210
-9
No files matched your search
@@ -0,0 +1,4 @@
|
||||
---
|
||||
important: blocker
|
||||
---
|
||||
- **/ship now accepts a granted follow-up review that starts after a docs-only commit.** When the docs commit /ship writes after its review landed before the owner granted one more scoped review of a fix, that review's range started at the docs commit, and `gstack-ship-release finish` refused it with `FIX_CHECK: not counted (range_base <sha> does not hold the reviewed tree)`, so the reviewed fix counted as unreviewed code. finish now accepts the range when every commit between the reviewed commit and the range start changes only docs or release metadata, and prints a `FIX_CHECK_DOCS_BRIDGE:` line naming those commits. The docs still get their usual recheck. A code or test commit in between, a merge, a range that starts before the reviewed commit, and a range that starts on another branch still refuse.
|
||||
@@ -732,7 +732,8 @@ through earlier rows whose commit is the next row's `range_base`
|
||||
is a completed clean read on committed content (freshness `CURRENT` against its own
|
||||
tree; a fix-check that stopped on findings the owner has since ruled notes reads
|
||||
clean, [ruled notes](ship-policy.md#ruled-notes)) whose commit is on the branch, the oldest link's `range_base` is a branch commit
|
||||
holding the reviewed tree, and for each link an approved `review_loop_grant` in
|
||||
holding the reviewed tree (or following its commit by docs and release metadata
|
||||
only, [docs bridge](ship-policy.md#fix-check-docs-bridge)), and for each link an approved `review_loop_grant` in
|
||||
`gstack-approvals` names a stop head inside its `range_base..commit`, each code or test path inside a link's `range_base..commit`
|
||||
that is unchanged since, or whose change against the base is still the one the
|
||||
fix-check read after a base merge also edited it (GST-1195), routes as
|
||||
|
||||
@@ -1913,6 +1913,20 @@ reason (`range_base <sha12> does not hold the reviewed tree`); a link that is
|
||||
stale or ungranted refuses with its reason and `; fix-check at <sha12>, link <k>
|
||||
of <n>`. Either way nothing in the chain counts.
|
||||
|
||||
<a id="fix-check-docs-bridge"></a>One gap is not a gap: the oldest link's `range_base` may follow the reviewed
|
||||
tree's commit by commits that change only authored docs or release metadata,
|
||||
such as the Step 14.5 docs commit (harness-evals#361, GST-2294). finish walks
|
||||
`range_base`'s first parents back to the nearest commit holding the reviewed tree
|
||||
and accepts the range when every commit after it is a single-parent commit whose
|
||||
every path it routes as docs or release metadata, printing
|
||||
`FIX_CHECK_DOCS_BRIDGE: range_base <sha12> follows the reviewed commit <sha12> by
|
||||
docs or release metadata only (<commits>)`. Those docs still take the docs route
|
||||
and its recheck. A code or test commit in between, a merge, a `range_base` the
|
||||
reviewed commit descends from, and a `range_base` on another branch still refuse.
|
||||
Pass `--reviewed-tree` as the tree the bound review read, never the fix-check's
|
||||
own tree: with the fix-check's tree, `range_base` sits before the "reviewed"
|
||||
commit and finish refuses (#4267, #4270).
|
||||
|
||||
<a id="test-code"></a>
|
||||
<a id="uid-sidecar"></a>
|
||||
## `test_code` and Godot UID sidecars after review
|
||||
|
||||
+34
-6
@@ -249,6 +249,29 @@ function patchBase(cwd: string, baseRef: string, commit: string, parent: string
|
||||
}
|
||||
|
||||
type FixLink = { row: Record<string, any>; commit: string; from: string };
|
||||
type BridgeOpts = { reviewed: string; baseRef: string; metadata: (p: string) => boolean; bookkeeping: readonly string[]; testCode: readonly string[] };
|
||||
|
||||
/**
|
||||
* A range_base that follows the reviewed tree's commit by docs and release metadata alone (GST-2294, harness-evals#361: the
|
||||
* fix-check range started at the Step 14.5 docs commit). Walks range_base's first parents off the base back to the nearest commit
|
||||
* holding the reviewed tree; every commit after it, range_base included, must be a single-parent commit whose every path finish
|
||||
* routes as release metadata (route's own per-path rule, so baseRelease on a landed stack parent) or authored docs, so the paths
|
||||
* outside the fix-check's read keep the docs route and its recheck.
|
||||
* A merge, a code or test path, or no reviewed commit behind range_base returns null.
|
||||
*/
|
||||
function docsBridge(cwd: string, from: string, o: BridgeOpts): { reviewedAt: string; commits: string[] } | null {
|
||||
const log = git(cwd, ['log', '--first-parent', '--format=%H %T %P', from, '--not', o.baseRef]);
|
||||
if (log.status !== 0) return null;
|
||||
const docsOnly = (p: string) => o.metadata(p) || (!isTestCode(p, o.testCode) && classifyPath(p, cwd) === 'docs' && !matchesAny(o.bookkeeping, p));
|
||||
const commits: string[] = [];
|
||||
for (const [commit, tree, ...parents] of log.stdout.trim().split('\n').filter(Boolean).map(l => l.split(' '))) {
|
||||
if (tree === o.reviewed) return commits.length ? { reviewedAt: commit!, commits: commits.reverse() } : null;
|
||||
const paths = parents.length === 1 ? touchedPaths(cwd, parents[0]!, commit!) : null;
|
||||
if (!paths || ![...paths].every(docsOnly)) return null;
|
||||
commits.push(commit!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The granted fix-checks finish counts (GST-1130, GST-1627): the newest review-fix-check row and, while its range_base does
|
||||
@@ -257,13 +280,14 @@ type FixLink = { row: Record<string, any>; commit: string; from: string };
|
||||
* head inside its range_base..commit, its own commit included (the post-review flow grants each fix-check at the commit it
|
||||
* reads, GST-1676); a grant counts for one link only, assigned newest link first, so one grant naming the boundary or two
|
||||
* links' commits covers one, and the older link needs a grant of its own.
|
||||
* A range_base no earlier row ends at is a gap, and the chain does not count. A link before the newest may, instead of a clean
|
||||
* A range_base no earlier row ends at is a gap, and the chain does not count, unless docs and release metadata alone sit between
|
||||
* the reviewed tree's commit and it (docsBridge, GST-2294). A link before the newest may, instead of a clean
|
||||
* read, be a finished read bound to its own committed content that found issues (logged converged:false, GST-1973) when its
|
||||
* findings account for its counts and each open or blocking one names a file it read: it counts no path itself, and a path its
|
||||
* findings name counts only through a clean link after it, and a finding no later clean link's range touched refuses the chain. A path counts as fix_checked when a clean link's range touched it and it is unchanged since that link,
|
||||
* or its patch against the base is still the one the link read.
|
||||
*/
|
||||
function fixChecked(cwd: string, o: { reviewed: string; now: string; baseRef: string; parent?: string; currentBase?: string; candidates: string[] }, r: Report): { counted: boolean; paths: string[] } | null {
|
||||
function fixChecked(cwd: string, o: BridgeOpts & { now: string; parent?: string; currentBase?: string; candidates: string[] }, r: Report): { counted: boolean; paths: string[] } | null {
|
||||
const rows: Record<string, any>[] = bin(cwd, 'gstack-review-read', [], 60_000).stdout.split('\n---CONFIG---')[0].split('\n')
|
||||
.flatMap(l => { try { const v = JSON.parse(l); return v?.skill === 'review-fix-check' ? [v] : []; } catch { return []; } });
|
||||
const commitOf = (c: unknown) => typeof c === 'string' && /^[0-9a-f]{7,64}$/.test(c) ? git(cwd, ['rev-parse', '--verify', '--quiet', `${c}^{commit}`]).stdout.trim() : '';
|
||||
@@ -271,13 +295,15 @@ function fixChecked(cwd: string, o: { reviewed: string; now: string; baseRef: st
|
||||
const onBranch = (c: string) => !!c && ancestor(c, 'HEAD');
|
||||
const commits = rows.map(row => commitOf(row.commit_full));
|
||||
const chain: FixLink[] = [];
|
||||
let gap = '';
|
||||
let gap = '', bridged = '';
|
||||
for (let i = rows.length - 1; i >= 0;) {
|
||||
const link = { row: rows[i]!, commit: commits[i]!, from: commitOf(rows[i]!.range_base) };
|
||||
chain.unshift(link);
|
||||
if (!onBranch(link.from) || git(cwd, ['rev-parse', `${link.from}^{tree}`]).stdout.trim() === o.reviewed) break;
|
||||
i = commits.slice(0, i).lastIndexOf(link.from);
|
||||
if (i < 0) gap = `range_base ${link.from.slice(0, 12)} does not hold the reviewed tree`;
|
||||
const bridge = i < 0 ? docsBridge(cwd, link.from, o) : null;
|
||||
if (bridge) bridged = `FIX_CHECK_DOCS_BRIDGE: range_base ${link.from.slice(0, 12)} follows the reviewed commit ${bridge.reviewedAt.slice(0, 12)} by docs or release metadata only (${bridge.commits.map(c => c.slice(0, 12)).join(',')})`;
|
||||
else if (i < 0) gap = `range_base ${link.from.slice(0, 12)} does not hold the reviewed tree`;
|
||||
}
|
||||
const inRange = (l: FixLink, c: string) => !!l.from && !!l.commit && ancestor(l.from, c) && ancestor(c, l.commit);
|
||||
const rulings = readRuledNotes(cwd).map(n => ({ ...n, links: chain.filter(l => inRange(l, n.head)) }));
|
||||
@@ -325,6 +351,7 @@ function fixChecked(cwd: string, o: { reviewed: string; now: string; baseRef: st
|
||||
const rulable = (l: FixLink) => (Array.isArray(l.row.findings) ? l.row.findings : []).filter((f: any) => !!f && !f.gate && RULABLE.has(f.action) && holdsOpen(f) && typeof f.fingerprint === 'string' && !ruledOn(l).has(f.fingerprint)).map((f: any) => f.fingerprint as string);
|
||||
for (const l of notCounted ? chain : []) if (rulable(l).length) r.add(`FIX_CHECK_OPEN: ${rulable(l).join(',')} on the fix-check at ${l.commit.slice(0, 12)}: the owner rules each one a disclosed note with gstack-approvals rule --finding <fingerprint> --head ${l.commit.slice(0, 12)} --reason '<why>' --words '<owner\'s exact words>' --by '<owner>'; a review-loop exception accepts edits after a fix-check, never its findings (docs/troubleshooting.md#fix-check-open)`);
|
||||
if (!chain.length || notCounted) return { counted: false, paths: [] };
|
||||
if (bridged) r.add(bridged);
|
||||
const read: ((p: string) => boolean)[] = [];
|
||||
for (const [k, l] of chain.entries()) {
|
||||
if (!clean.has(l)) continue;
|
||||
@@ -361,10 +388,11 @@ function route(cwd: string, base: string, reviewed: string, reviewedBase: Review
|
||||
shipped = b;
|
||||
}
|
||||
const by: Routed = { behavior: [], docs: [], metadata: [], tests: [], base: [], context: [], bounded: [], mechanical: [], accepted: [], fix_checked: [], sidecar: [] };
|
||||
const isMetadata = (p: string) => parent && shipped.has(p) ? opts.baseRelease!.includes(p) || harnessRepin(cwd, sides!.currentBase, now, p) : release.includes(p) || harnessRepin(cwd, reviewed, now, p);
|
||||
const onlyShipped = new Set(parent ? [...shipped].filter(p => !changedSet.has(p)) : []);
|
||||
for (const p of [...changedSet, ...onlyShipped].sort()) {
|
||||
const verdict = sides ? (!touched.has(p) || (parent && !shipped.has(p) && baseHolds(cwd, sides, p)) ? 'same' : comparePath(cwd, sides, p)) : 'changed';
|
||||
const metadata = parent && shipped.has(p) ? opts.baseRelease!.includes(p) || harnessRepin(cwd, sides!.currentBase, now, p) : release.includes(p) || harnessRepin(cwd, reviewed, now, p);
|
||||
const metadata = isMetadata(p);
|
||||
by[verdict === 'same' ? 'base' : metadata ? 'metadata' : sides && isTestCode(p, opts.testCode) ? 'tests' : classifyPath(p, cwd) === 'docs' && !matchesAny(opts.bookkeeping, p) ? 'docs' : verdict === 'context' ? 'context' : 'behavior'].push(p);
|
||||
}
|
||||
const shippedTests = by.tests.filter(p => onlyShipped.has(p));
|
||||
@@ -384,7 +412,7 @@ function route(cwd: string, base: string, reviewed: string, reviewedBase: Review
|
||||
by.behavior.push(...packetBody.over);
|
||||
for (const k of ['context', 'tests', 'bounded'] as const) by[k] = by[k].filter(p => !packetBody.over.includes(p));
|
||||
}
|
||||
const fix = fixChecked(cwd, { reviewed, now, baseRef, parent, currentBase: sides?.currentBase, candidates: [...by.behavior, ...by.tests, ...by.context, ...by.bounded] }, r);
|
||||
const fix = fixChecked(cwd, { reviewed, now, baseRef, parent, currentBase: sides?.currentBase, metadata: isMetadata, bookkeeping: opts.bookkeeping, testCode: opts.testCode, candidates: [...by.behavior, ...by.tests, ...by.context, ...by.bounded] }, r);
|
||||
if (!fix) return null;
|
||||
const counted = fix.counted;
|
||||
by.fix_checked = fix.paths;
|
||||
|
||||
@@ -60,10 +60,10 @@ function link(f: ShipFx, from: string, row: Record<string, unknown>, o: { grant?
|
||||
expect(logged.status, logged.stderr).toBe(0);
|
||||
return { commit, grant };
|
||||
}
|
||||
function finish(f: ShipFx, reviewed: string, reviewedBase: string) {
|
||||
function finish(f: ShipFx, reviewed: string, reviewedBase: string, extra: string[] = []) {
|
||||
const post = path.join(f.root, 'post.json');
|
||||
expect(f.run(path.join(BIN, 'gstack-docs-candidate'), ['snapshot', '--out', post, '--audit-id', 'a1', '--mode', 'edit', '--base', 'main', '--docs', 'README.md']).status).toBe(0);
|
||||
return f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--reviewed-base', reviewedBase, '--lane', LANE]);
|
||||
return f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--reviewed-base', reviewedBase, '--lane', LANE, ...extra]);
|
||||
}
|
||||
|
||||
describe('finish counts a fix-check chain whose earlier links found issues a later clean link read (GST-1973)', () => {
|
||||
@@ -240,3 +240,157 @@ describe('finish still refuses fix-check chains that do not read the fixes (GST-
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('finish counts a fix-check whose range starts after docs-only commits (GST-2294)', () => {
|
||||
const README = { 'README.md': '# Fx\n\nHow to run the canary.\n' };
|
||||
const notHeld = (c: string) => `FIX_CHECK: not counted (range_base ${at(c)} does not hold the reviewed tree)`;
|
||||
|
||||
// Value: protects=harness-evals#361's shape counts: the reviewed commit, then the Step 14.5 docs commit, then the granted
|
||||
// fix-check whose range starts at that docs commit; fails_when=finish refuses the range_base because its tree is not the
|
||||
// reviewed tree and no earlier fix-check ends at it, so src/x.ts routes as behavior; seam=finish's chain walk
|
||||
test.each([
|
||||
['one docs commit', [README]],
|
||||
['a docs commit and a changelog stamp', [README, { 'CHANGELOG.md': '# Changelog\n\n## [Unreleased]\n\n- pending\n- canary docs\n' }]],
|
||||
])('a range_base after %s counts', (_name, commits) => {
|
||||
const { f, t, reviewed, reviewedBase } = reviewedShape();
|
||||
const between = (commits as Record<string, string>[]).map((files, k) => edit(f, files, `docs commit ${k + 1}`));
|
||||
const from = between[between.length - 1]!;
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n', 'test/x.test.ts': TEST_V2 }, 'fix the round-2 blocker');
|
||||
const c = link(f, from, CLEAN);
|
||||
const r = finish(f, reviewed, reviewedBase, ['--docs-rechecked']);
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK_DOCS_BRIDGE'), r.stdout).toBe(`FIX_CHECK_DOCS_BRIDGE: range_base ${at(from)} follows the reviewed commit ${at(t)} by docs or release metadata only (${between.map(at).join(',')})`);
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK'), r.stdout).toBe(`FIX_CHECK: reviewed src/x.ts,test/x.test.ts (fix-check at ${at(c.commit)}, range from ${at(from)}, grant ${c.grant})`);
|
||||
expect([r.status, lineOf(r.stdout, 'ROUTE')], r.stdout).toEqual([0, expect.stringMatching(/ behavior=0 .* fix_checked=2$/)]);
|
||||
});
|
||||
|
||||
// Value: protects=only docs and release metadata bridge the gap: a code or test commit, alone or after a docs commit, between
|
||||
// the reviewed commit and range_base is a read no fix-check made; fails_when=finish accepts any range_base that descends from
|
||||
// the reviewed commit; seam=finish's chain walk
|
||||
test.each([
|
||||
['a code commit', [{ 'src/y.ts': 'export const y = 2;\n' }]],
|
||||
['a test commit', [{ 'test/x.test.ts': TEST_V2 }]],
|
||||
['a docs commit, then a code commit', [README, { 'src/y.ts': 'export const y = 2;\n' }]],
|
||||
])('a range_base after %s still refuses', (_name, commits) => {
|
||||
const { f, reviewed, reviewedBase } = reviewedShape();
|
||||
const between = (commits as Record<string, string>[]).map((files, k) => edit(f, files, `unread commit ${k + 1}`));
|
||||
const from = between[between.length - 1]!;
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n' }, 'fix the round-2 blocker');
|
||||
link(f, from, CLEAN);
|
||||
const r = finish(f, reviewed, reviewedBase, ['--docs-rechecked']);
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK_DOCS_BRIDGE'), r.stdout).toBe('');
|
||||
expect([r.status, lineOf(r.stdout, 'FIX_CHECK')], r.stdout).toEqual([3, notHeld(from)]);
|
||||
});
|
||||
|
||||
// Value: protects=the bridge runs only forward from the reviewed commit: a docs-only commit before it (so the reviewed commit
|
||||
// descends from range_base, as when --reviewed-tree names the fix-check's own tree) is no bridge; fails_when=finish accepts a
|
||||
// docs-only diff between range_base and the reviewed commit in either direction; seam=finish's chain walk
|
||||
test('a docs commit before the reviewed commit refuses: range_base is not a descendant of it', () => {
|
||||
const { f } = reviewedShape();
|
||||
const before = edit(f, README, 'docs before review');
|
||||
edit(f, { 'src/y.ts': 'export const y = 2;\n' }, 'more reviewed code');
|
||||
const reviewed = f.run(path.join(BIN, 'gstack-wtree'), []).stdout.trim();
|
||||
const reviewedBase = f.git('merge-base', 'origin/main', 'HEAD').stdout.trim();
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n' }, 'fix the round-2 blocker');
|
||||
link(f, before, CLEAN);
|
||||
const r = finish(f, reviewed, reviewedBase, ['--docs-rechecked']);
|
||||
expect([r.status, lineOf(r.stdout, 'FIX_CHECK')], r.stdout).toEqual([3, notHeld(before)]);
|
||||
});
|
||||
|
||||
// Value: protects=a base merge between the reviewed commit and range_base is no bridge, even when the base change is docs;
|
||||
// fails_when=finish reads a merge commit's first-parent diff as the branch's own docs edit; seam=finish's chain walk
|
||||
test('a merge commit between the reviewed commit and range_base refuses', () => {
|
||||
const { f, reviewed, reviewedBase } = reviewedShape();
|
||||
f.advanceBase({ 'docs/base.md': '# Base docs\n' });
|
||||
expect(f.git('fetch', '-q', 'origin').status).toBe(0);
|
||||
expect(f.git('merge', '-q', '--no-edit', 'origin/main').status).toBe(0);
|
||||
const merge = headOf(f);
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n' }, 'fix the round-2 blocker');
|
||||
link(f, merge, CLEAN);
|
||||
const r = finish(f, reviewed, reviewedBase, ['--docs-rechecked']);
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK'), r.stdout).toBe(notHeld(merge));
|
||||
expect(r.status, r.stdout).toBe(3);
|
||||
});
|
||||
|
||||
// Value: protects=a range_base on another branch still refuses, docs-only or not; fails_when=the bridge looks up the reviewed
|
||||
// commit from a range_base HEAD does not contain; seam=finish's chain walk
|
||||
test('a docs-only range_base on another branch refuses', () => {
|
||||
const { f, reviewed, reviewedBase } = reviewedShape();
|
||||
expect(f.git('checkout', '-qb', 'side').status).toBe(0);
|
||||
const side = edit(f, README, 'docs on a side branch');
|
||||
expect(f.git('checkout', '-q', 'feat').status).toBe(0);
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n' }, 'fix the round-2 blocker');
|
||||
link(f, side, CLEAN);
|
||||
const r = finish(f, reviewed, reviewedBase);
|
||||
expect([r.status, lineOf(r.stdout, 'FIX_CHECK')], r.stdout).toEqual([3, 'FIX_CHECK: not counted (the row names no range_base commit on this branch)']);
|
||||
});
|
||||
|
||||
// Value: protects=a range_base whose tree is the reviewed tree counts with no bridge line (an empty commit after review);
|
||||
// fails_when=the bridge replaces the equal-tree check; seam=finish's chain walk
|
||||
test('a range_base whose tree equals the reviewed tree counts', () => {
|
||||
const { f, reviewed, reviewedBase } = reviewedShape();
|
||||
expect(f.git('commit', '-q', '--allow-empty', '-m', 'an empty commit after review').status).toBe(0);
|
||||
const same = headOf(f);
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n', 'test/x.test.ts': TEST_V2 }, 'fix the round-2 blocker');
|
||||
const c = link(f, same, CLEAN);
|
||||
const r = finish(f, reviewed, reviewedBase);
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK_DOCS_BRIDGE'), r.stdout).toBe('');
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK'), r.stdout).toBe(`FIX_CHECK: reviewed src/x.ts,test/x.test.ts (fix-check at ${at(c.commit)}, range from ${at(same)}, grant ${c.grant})`);
|
||||
expect(r.status, r.stdout).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the docs bridge uses the router\'s own metadata rule on a landed stack parent (GST-2294 audit)', () => {
|
||||
const POLICY_SRC = ['export const allow = false;', ...Array.from({ length: 28 }, (_, k) => `export const l${k} = ${k};`), 'export const label = "old";', ''].join('\n');
|
||||
/** A child branch reviewed after its stack parent P, with src/policy.ts optionally declared under key; P then lands on main and main merges into the child. */
|
||||
function stackShape(key: string | null, after: (f: ShipFx) => void) {
|
||||
const f = shipFixture({ ...(key ? { '.gstack/ship-policy.json': JSON.stringify({ min_gstack_version: '1.99.0.0', [key]: ['src/policy.ts'] }) } : {}), 'src/policy.ts': POLICY_SRC, 'src/x.ts': 'export const x = 1;\n' }, { 'src/parent.ts': 'export const parent = 1;\n' });
|
||||
ships.push(f);
|
||||
const parent = headOf(f);
|
||||
edit(f, { 'src/x.ts': 'export const x = 2;\n' }, 'child');
|
||||
const reviewed = f.run(path.join(BIN, 'gstack-wtree'), []).stdout.trim();
|
||||
const from = edit(f, { 'src/policy.ts': POLICY_SRC.replace('allow = false', 'allow = true') }, 'unreviewed logic change');
|
||||
after(f);
|
||||
link(f, from, CLEAN);
|
||||
f.advanceBase({ 'src/parent.ts': 'export const parent = 1;\n' });
|
||||
expect(f.git('fetch', '-q', 'origin').status).toBe(0);
|
||||
expect(f.git('merge', '-q', '--no-edit', 'origin/main').status).toBe(0);
|
||||
return { r: finish(f, reviewed, parent, ['--docs-rechecked']), from };
|
||||
}
|
||||
const notHeld = (c: string) => `FIX_CHECK: not counted (range_base ${at(c)} does not hold the reviewed tree)`;
|
||||
|
||||
// Value: protects=a declared stamp or release-output path the router classes as behavior on a landed stack parent never bridges
|
||||
// the gap, whether the fix-check rewrites that file or only appends to it; fails_when=the bridge reads the broad release list and
|
||||
// credits src/policy.ts as fix_checked (finish exits 0, fix_checked=2); seam=finish's chain walk and route's metadata rule
|
||||
test.each([
|
||||
['stamp_paths', 'a rewrite'], ['release_outputs', 'a rewrite'], ['stamp_paths', 'an append'], ['release_outputs', 'an append'],
|
||||
])('%s: a logic change in the gap, then %s in the fix-check range, refuses', (key, kind) => {
|
||||
const { r, from } = stackShape(key, f => edit(f, {
|
||||
'src/policy.ts': kind === 'a rewrite' ? POLICY_SRC.replace('allow = false', 'allow = true').replace('"old"', '"new"') : `${POLICY_SRC.replace('allow = false', 'allow = true')}export const tail = 1;\n`,
|
||||
'src/x.ts': 'export const x = 3;\n',
|
||||
}, 'fix-check reads this range'));
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK_DOCS_BRIDGE'), r.stdout).toBe('');
|
||||
expect([r.status, lineOf(r.stdout, 'FIX_CHECK')], r.stdout).toEqual([3, notHeld(from)]);
|
||||
});
|
||||
|
||||
// Value: protects=the two controls of the append repro: with no append the fix-check never touched src/policy.ts, and with no
|
||||
// declaration the path was never metadata; both refuse; fails_when=either control counts; seam=finish's route
|
||||
test('controls: no append stops on src/policy.ts, and no declaration refuses the bridge', () => {
|
||||
const noAppend = stackShape('stamp_paths', f => edit(f, { 'src/x.ts': 'export const x = 3;\n' }, 'fix-check reads this range'));
|
||||
expect([noAppend.r.status, noAppend.r.stdout.includes('src/policy.ts')], noAppend.r.stdout).toEqual([3, true]);
|
||||
const undeclared = stackShape(null, f => edit(f, { 'src/policy.ts': `${POLICY_SRC.replace('allow = false', 'allow = true')}export const tail = 1;\n`, 'src/x.ts': 'export const x = 3;\n' }, 'fix-check reads this range'));
|
||||
expect([undeclared.r.status, lineOf(undeclared.r.stdout, 'FIX_CHECK')], undeclared.r.stdout).toEqual([3, notHeld(undeclared.from)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the docs bridge line prints only when the fix-check counts (GST-2294 audit)', () => {
|
||||
// Value: protects=FIX_CHECK_DOCS_BRIDGE never sits beside a refused chain; fails_when=the line prints before the grant check
|
||||
// refuses; seam=finish's FIX_CHECK output
|
||||
test('an ungranted fix-check after a docs commit prints no bridge line', () => {
|
||||
const { f, reviewed, reviewedBase } = reviewedShape();
|
||||
const d = edit(f, { 'README.md': '# Fx\n\nHow to run the canary.\n' }, 'docs commit');
|
||||
edit(f, { 'src/x.ts': 'export const x = 3;\n', 'test/x.test.ts': TEST_V2 }, 'fix the round-2 blocker');
|
||||
link(f, d, CLEAN, { grant: false });
|
||||
const r = finish(f, reviewed, reviewedBase, ['--docs-rechecked']);
|
||||
expect([r.status, lineOf(r.stdout, 'FIX_CHECK_DOCS_BRIDGE'), lineOf(r.stdout, 'FIX_CHECK')], r.stdout).toEqual([3, '', 'FIX_CHECK: not counted (no approved review_loop_grant names a stop head inside its range_base..commit)']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user