Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 258 additions & 13 deletions packages/spec/scripts/build-schemas-check-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1516,25 +1516,35 @@ describe('build-schemas.ts — the drift notice names the direction it measured
//
// Truncation moves TWO things here, and the second was a surprise worth
// writing down: `merge-base HEAD origin/main` itself fails once the walk is
// cut, so `resolveSurfaceBase` falls back to origin/main's TIP as the
// baseline (it says so — "no merge base is walkable here, so this run
// anchors on the origin/main TIP …", the line #6359 reworded to name what
// that anchor then MISJUDGES).
// The pair being compared is therefore anchor-at-`tip` vs baseline-at-
// `mainTip`, not the fork point at all. And the ancestry between them is
// exactly what a grafted history cannot answer: `mainTip` is its own shallow
// root, so walking down from it to reach `tip` is the walk that was cut, and
// the reverse is a plain negative. Neither probe yields a usable answer.
// cut, so `resolveSurfaceBase` has to anchor the baseline somewhere else.
// The pair being compared is therefore the committed anchor against THAT
// baseline, not against the fork point — and the ancestry between them is
// exactly what a grafted history cannot answer, in either direction.
//
// The old line printed "trails the baseline at <mainTip> by 1 key(s)" here,
// The old line printed "trails the baseline at <rev> by 1 key(s)" here,
// which happens to be TRUE of the untruncated history — and that is the
// point: it was never measured, it was assumed, and one fixture over the
// same assumption printed the exact opposite of the truth. Declining is the
// same disposition #5370 already took for the write.
//
// #6452 re-based this fixture without changing its subject. The baseline a
// shallow run resolves is no longer origin/main's TIP (that is what made
// main's additions read as this branch's deletions), so the fixture now
// says on MAIN which upstream rev the anchor names — `older` — instead of
// inheriting whichever anchor an earlier case left behind. And BOTH fetched
// revs are grafted, which is what a checkout that fetched two commits at
// `--depth=1` actually looks like: with `tip` a shallow root too, neither
// ancestry probe can answer and the notice must still decline.
seedSurfaceBase(older, (k) => k.filter((x) => x !== AHEAD_KEY && x !== LANDED_KEY));
git('add', 'authorable-surface.base.json');
git('commit', '-q', '-m', 'fixture: main records its anchor at the older baseline');
const mainHead = git('rev-parse', 'HEAD');
git('update-ref', 'refs/remotes/origin/main', mainHead);

git('checkout', '-q', '-B', 'issue-5847-shallow', older);
seedSurface((s) => s);
const anchorAtTip = commitAnchor(tip, (k) => k.filter((x) => x !== LANDED_KEY));
fs.writeFileSync(shallowFile(), `${mainTip}\n`);
fs.writeFileSync(shallowFile(), `${mainHead}\n${tip}\n`);
expect(git('rev-parse', '--is-shallow-repository')).toBe('true');

const { status, output } = run([]);
Expand All @@ -1544,8 +1554,8 @@ describe('build-schemas.ts — the drift notice names the direction it measured
expect(git('status', '--porcelain', '-uno')).toBe('');
expect(output).toContain('differs from the baseline this build resolved');
expect(output).toContain(
`${tip.slice(0, 12)}, that baseline is at ${mainTip.slice(0, 12)}, and they differ by ` +
`1 key(s) only that baseline has`,
`${tip.slice(0, 12)}, that baseline is at ${older.slice(0, 12)}, and they differ by ` +
`1 key(s) only the anchor has`,
);
expect(output).toContain(
'shallow checkout — a "not an ancestor" answer is not usable about a truncated history',
Expand Down Expand Up @@ -1612,6 +1622,241 @@ describe('build-schemas.ts — the drift notice names the direction it measured
);
});

// ─────────────────────────────────────────────────────────────────────────────
// #6452 — a truncated history moves the ANCHOR, never the verdict.
//
// `merge-base HEAD origin/main` cannot answer in a shallow checkout, and the old
// fallback anchored on origin/main's TIP. Under a tip anchor "main added a key
// after this branch forked" and "this branch deleted a key" are the SAME fact,
// so the gate reported the first as the second: #6359 measured PR #6356 — which
// touched no packages/spec file at all — being told it had deleted
// `ui/BulkActionDef:requiredPermissions`, a key main had just added. Nothing
// guarded that path (the calling block carries no `if (CHECK)`) and its verdict
// is `process.exit(1)`, so it is every `gen:schema` in a shallow job, not one
// gate in one mode.
//
// The two obvious dispositions were both refused before this one was chosen:
// reporting "unverified" instead of adjudicating is the #4650 bypass in every
// shallow job at once (`resolveSurfaceBase`'s own doc comment says so), and
// erroring on the CI configuration paints that whole set of jobs red. So the
// anchor moves and the verdict does not: the gate still runs, and a key that
// existed at the anchored rev and is gone now is still caught.
//
// What these cases pin, and why each one can go red:
//
// 1. the false red is gone — same tree, same truncation, main's addition is
// no longer this branch's deletion;
// 2. the gate did NOT weaken — a real deletion is still red, and it names the
// deleted key rather than main's addition;
// 3. the baseline's keys come from GIT at that commit, never from the anchor
// FILE. This is the acceptance criterion that cannot be assumed: if the
// resolution took the file's own `keys`, `verifyCommittedSurfaceBase` would
// hit its `rev === resolved.rev` fast path and compare the anchor against
// itself, so a line shed from it would pass. The fixture sheds one;
// 4. an anchor rev nothing upstream vouches for is not used — a PR can point
// `baseRev` at one of its OWN commits (a `--depth=1` fetch resolves any sha
// the remote advertises), and a truncated history cannot refute it by
// walking, so the rev is accepted only when origin/main's own copy of the
// anchor names it (or reachability is demonstrated outright);
// 5. with no upstream anchor at all the run keeps the tip and SAYS so, naming
// `fetch-depth: 0`. That residual false red is the honest degradation, and
// it is pinned so it stays loud rather than becoming a silent skip.
describe('build-schemas.ts — a shallow checkout re-anchors the deletion gate, it does not accuse (#6452)', () => {
/** Only in the baseline at origin/main's TIP: what main added after this branch forked. */
const MAIN_ADDED_KEY = 'data/Object:zzAddedOnMainAfterTheFork6452';
/** In the baseline at the ANCHORED rev too, so its absence is a real deletion. */
const BRANCH_DELETED_KEY = 'data/Object:zzDeletedByThisBranch6452';
/** A live key this build really emits — shed from the anchor FILE by case 3. */
const SHED_KEY = 'data/Object:label';

const shallowFile = (): string => path.join(sandbox, '.git', 'shallow');

/** `git()` throws on a non-zero exit, which is what the fixture guards expect. */
const mergeBaseFails = (rev: string): boolean =>
spawnSync('git', ['merge-base', 'HEAD', rev], { cwd: sandbox }).status !== 0;

beforeAll(() => {
expect(pristineSurface, `${SHED_KEY} is no longer in the baseline — pick another live key`).toContain(
SHED_KEY,
);
});

/**
* The upstream ladder every case forks from: a fork point, main's own anchor
* committed ON MAIN at it, then a main that moves ahead and ADDS a key.
*
* Committing the anchor on main is what makes these fixtures model CI rather
* than a laboratory: origin/main's copy of that file is the only statement
* about which rev the anchor names that a PR cannot rewrite, and it is exactly
* what a `--depth=1` fetch of main still carries.
*/
function seedUpstream(baseKeys: (keys: string[]) => string[]): {
forkBase: string;
anchored: string;
mainTip: string;
} {
seedManifest((s) => s);
const forkBase = seedBase(baseKeys);
seedSurfaceBase(forkBase, baseKeys);
git('add', 'authorable-surface.base.json');
git('commit', '-q', '-m', 'fixture: main anchors at the fork point');
const anchored = git('rev-parse', 'HEAD');
git('update-ref', 'refs/remotes/origin/main', anchored);
const mainTip = seedBase((s) => [...baseKeys(s), MAIN_ADDED_KEY].sort());
return { forkBase, anchored, mainTip };
}

/** Fork at an upstream commit and truncate history the way CI's checkout does. */
function forkBranch(name: string, at: string, mainTip: string): void {
git('checkout', '-q', '-B', name, at);
// The worktree carries what this build emits, so every case below is judged
// on its baseline rather than on artifact staleness.
seedSurface((s) => s);
fs.writeFileSync(shallowFile(), `${mainTip}\n`);
expect(git('rev-parse', '--is-shallow-repository')).toBe('true');
expect(mergeBaseFails(mainTip), 'the fixture is not truncated — merge-base still answers').toBe(true);
}

afterEach(() => {
fs.rmSync(shallowFile(), { force: true });
git('checkout', '-q', '-f', 'main');
// Hand `main` back current and CLEAN, so the describes after this one start
// from a tree with no fixture of ours in it — the surface first, then an
// anchor that names the commit just made, so what is left behind is authentic
// by construction rather than by luck (one case here removes main's anchor
// outright, and the next describe reads whatever this leaves).
seedSurface((s) => s);
git('add', AUTHORABLE_SURFACE_DIR_NAME);
git('commit', '-q', '--allow-empty', '-m', 'fixture: restore the pristine surface on main');
seedSurfaceBase(git('rev-parse', 'HEAD'), (k) => k);
git('add', 'authorable-surface.base.json');
git('commit', '-q', '-m', 'fixture: restore a current anchor on main');
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
});

it(
'no false red: a key main added after the fork is not reported as this branch deleting it',
{ timeout: SPAWN_TIMEOUT_MS },
() => {
const { forkBase, anchored, mainTip } = seedUpstream((s) => s);
forkBranch('issue-6452-no-false-red', anchored, mainTip);

const { status, output } = run(['--check']);

// The anchor moved, and the line says which rev and on whose authority.
expect(output).toContain(`anchors on ${forkBase.slice(0, 12)} rather than on`);
expect(output).toContain("origin/main's own authorable-surface.base.json names the same commit");
expect(output).not.toContain('no merge base is walkable here');
// The defect itself: under the tip anchor this run exited 1 naming
// MAIN_ADDED_KEY as an unproven deletion.
expect(output).not.toContain('deleted without proof');
expect(output).not.toContain(MAIN_ADDED_KEY);
expect(status).toBe(0);
},
);

it(
'the gate does not weaken: a genuine deletion is still red under the same truncation',
{ timeout: SPAWN_TIMEOUT_MS },
() => {
const { forkBase, anchored, mainTip } = seedUpstream((s) => [...s, BRANCH_DELETED_KEY].sort());
forkBranch('issue-6452-real-deletion', anchored, mainTip);

const { status, output } = run(['--check']);

expect(status).toBe(1);
expect(output).toContain(`anchors on ${forkBase.slice(0, 12)} rather than on`);
expect(output).toContain('1 authorable baseline line(s) were deleted without proof (#4650)');
expect(output).toContain(BRANCH_DELETED_KEY);
// Exactly one, and the right one: main's addition is not in the verdict.
expect(output).not.toContain(MAIN_ADDED_KEY);
},
);

it(
'the baseline keys come from git at that commit, never from the anchor file (no self-verification)',
{ timeout: SPAWN_TIMEOUT_MS },
() => {
const { forkBase, anchored, mainTip } = seedUpstream((s) => s);
forkBranch('issue-6452-shed-anchor-key', anchored, mainTip);
// The anchor sheds a line the commit it NAMES really carries — the #4650
// attack moved one file over, and the shape the shortcut would bless.
seedSurfaceBase(forkBase, (k) => k.filter((x) => x !== SHED_KEY));

const { status, output } = run(['--check']);

// THE pin. Resolve the baseline from the anchor file's own `keys` and this
// comparison becomes file-against-file: it passes, the run exits 0, and the
// shed line is gone from the baseline for good. Reading the keys out of git
// at `baseRev` is the only thing that makes it red.
expect(status).toBe(1);
expect(output).toContain('is not the baseline it claims to be (#4650, #5235)');
expect(output).toContain(`- ${SHED_KEY} (at ${forkBase.slice(0, 12)}, absent here)`);
},
);

it(
'an anchor rev nothing upstream vouches for is not used — the rev origin/main records is',
{ timeout: SPAWN_TIMEOUT_MS },
() => {
const { forkBase, anchored, mainTip } = seedUpstream((s) => [...s, BRANCH_DELETED_KEY].sort());
git('checkout', '-q', '-B', 'issue-6452-unvouched', anchored);
// The branch deletes the key, commits it, and then points `baseRev` at its
// OWN commit — authentic against itself (its keys ARE that commit's
// surface), upstream against nothing. A truncated history cannot refute it
// by walking, which is why the rev has to be vouched for rather than merely
// checked.
seedSurface((s) => s);
git('add', AUTHORABLE_SURFACE_DIR_NAME);
git('commit', '-q', '-m', 'fixture: the branch deletes a baseline key');
const branchOwn = git('rev-parse', 'HEAD');
seedSurfaceBase(branchOwn, (k) => k);
git('add', 'authorable-surface.base.json');
git('commit', '-q', '-m', 'fixture: the branch anchors on its own commit');
fs.writeFileSync(shallowFile(), `${mainTip}\n`);
expect(mergeBaseFails(mainTip)).toBe(true);

const { status, output } = run(['--check']);

expect(output).toContain(`names ${branchOwn.slice(0, 12)}, which nothing here can`);
expect(output).toContain(`anchors on ${forkBase.slice(0, 12)} rather than on`);
// …and the deletion the forged anchor was hiding is still adjudicated.
expect(status).toBe(1);
expect(output).toContain('deleted without proof (#4650)');
expect(output).toContain(BRANCH_DELETED_KEY);
},
);

it(
'with no upstream anchor at all it keeps the tip and says so, naming fetch-depth: 0',
{ timeout: SPAWN_TIMEOUT_MS },
() => {
// The honest degradation, pinned so it stays LOUD. A main whose tree carries
// no anchor cannot vouch for anything, so this run has only the tip — and
// the tip anchor is the defect. It reports the residual false red instead of
// waiving the check, because a diagnosable false red beats a silent bypass.
seedManifest((s) => s);
const forkBase = seedBase((s) => s);
seedSurfaceBase(forkBase, (k) => k);
git('add', 'authorable-surface.base.json');
git('commit', '-q', '-m', 'fixture: anchor at the fork point');
const anchored = git('rev-parse', 'HEAD');
git('rm', '-q', 'authorable-surface.base.json');
const mainTip = seedBase((s) => [...s, MAIN_ADDED_KEY].sort());
forkBranch('issue-6452-no-upstream-anchor', anchored, mainTip);

const { status, output } = run(['--check']);

expect(output).toContain('no merge base is walkable here, and no upstream anchor was usable');
expect(output).toContain(`anchors on the origin/main TIP ${mainTip.slice(0, 12)} instead`);
expect(output).toContain('fetch-depth: 0');
expect(status).toBe(1);
expect(output).toContain('deleted without proof');
expect(output).toContain(MAIN_ADDED_KEY);
},
);
});

// ─────────────────────────────────────────────────────────────────────────────
// #5371 — the output clean is scoped to THIS generator's artifacts.
//
Expand Down
Loading
Loading