test(#3090): assert which value, not which type
Nine assertions drove a real fault through a real seam and then checked only that the call did not throw, or that a result was a string, a boolean, an array. Each passed whether the code was right or wrong. #3050 is the canonical instance of the shape: its counter-test asserted effectiveRoot was a string and never which root, so a silent misroute passed it. All nine now assert the exact verdict, derived from the production branch each one reaches and traced back to source rather than taken from the survey. One was worse than a weak assertion. The test targeting resolveWorktreeLinkage's main_worktree path used createTempGitProject, which always seeds .planning/ — so the reason was always has_local_planning and the git-dir comparison the test appears to exercise was unreachable from its own fixture. It was not asserting loosely, it was pointed at the wrong path. The fixture now builds a git project without .planning (projectDoc had to be disabled too, since it defaults to git and would have re-seeded it), and the test reaches the branch it names. Another had no reason assertion anywhere in the file while its four siblings all pinned theirs — the odd one out rather than a convention. The last is mine. The parity guard shipped in #3077 checked typeof and doesNotThrow across four ExecGitFn seams, and that PR described it as failing "the moment any site re-grows its own shape". It could not: a site returning a different value of the same type passed it. All four benign-passthrough outputs are derivable exact values, so it now asserts them and the claim is true. Test names that promised more than their assertions established are corrected to match what they prove. Refs #3057 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,31 +144,58 @@ describe('A. execGit normalization (#3071)', () => {
|
||||
const sharedStub = makeFaultyGit();
|
||||
|
||||
// worktree-safety.cts:33 — via resolveWorktreeContext, the exported
|
||||
// entry point that threads deps.execGit.
|
||||
// entry point that threads deps.execGit. Derived: existsSync:()=>false
|
||||
// skips the has_local_planning shortcut, so resolveWorktreeLinkage runs;
|
||||
// the shared stub's benign passthrough (exitCode:0, empty stdout) for
|
||||
// both --git-dir and --git-common-dir makes them resolve to the SAME
|
||||
// path (tmpDir), which is the main_worktree branch
|
||||
// (src/worktree-safety.cts:196-200) — a `typeof === 'string'` check
|
||||
// would still pass if a shape regrowth returned e.g. reason:'not_a_git_repo'.
|
||||
const wsResult = worktreeSafety.resolveWorktreeContext(tmpDir, {
|
||||
execGit: sharedStub,
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.strictEqual(typeof wsResult.effectiveRoot, 'string');
|
||||
assert.strictEqual(typeof wsResult.mode, 'string');
|
||||
assert.strictEqual(typeof wsResult.reason, 'string');
|
||||
assert.deepStrictEqual(wsResult, {
|
||||
effectiveRoot: tmpDir,
|
||||
mode: 'current_directory',
|
||||
reason: 'main_worktree',
|
||||
});
|
||||
|
||||
// git-base-branch.cts:32 — trySymbolicRef takes execGit directly as its
|
||||
// second positional argument (no deps wrapper).
|
||||
assert.doesNotThrow(() => trySymbolicRef(tmpDir, sharedStub));
|
||||
// second positional argument (no deps wrapper). Derived: the stub
|
||||
// answers `git symbolic-ref ...` with exitCode:0 but empty stdout, and
|
||||
// trySymbolicRef treats an empty stdout as "unset" regardless of exit
|
||||
// code (src/git-base-branch.cts:105) — it must return null, not merely
|
||||
// return without throwing.
|
||||
assert.strictEqual(trySymbolicRef(tmpDir, sharedStub), null);
|
||||
|
||||
// worktree-base-ref.cts:88 — evaluateWorktreeBaseDegrade threads
|
||||
// deps.execGit.
|
||||
// deps.execGit. Derived: `git rev-parse HEAD` answers exitCode:0 with
|
||||
// empty stdout, which is the explicit exit0-empty-stdout branch
|
||||
// (src/worktree-base-ref.cts:401-403) — shouldDegrade:false,
|
||||
// reason:'no-head', headAbsenceVerified:false (NOT the exit-128
|
||||
// "definitive no-head" case, which would report headAbsenceVerified:true).
|
||||
// A `typeof shouldDegrade === 'boolean'` check would still pass on a
|
||||
// fail-closed flip to shouldDegrade:true.
|
||||
const wbrResult = evaluateWorktreeBaseDegrade({ execGit: sharedStub, cwd: tmpDir });
|
||||
assert.strictEqual(typeof wbrResult.shouldDegrade, 'boolean');
|
||||
assert.strictEqual(typeof wbrResult.reason, 'string');
|
||||
assert.deepStrictEqual(wbrResult, {
|
||||
shouldDegrade: false,
|
||||
reason: 'no-head',
|
||||
message: null,
|
||||
headSha: null,
|
||||
forkRef: null,
|
||||
forkSha: null,
|
||||
headAbsenceVerified: false,
|
||||
});
|
||||
|
||||
// verification.cts:226 — defaultPhaseCleanCommitTimesMs takes execGitFn
|
||||
// directly as its third positional argument, typed `= typeof execGit`.
|
||||
assert.doesNotThrow(() => {
|
||||
const map = defaultPhaseCleanCommitTimesMs(tmpDir, ['a.md', 'b.md'], sharedStub);
|
||||
assert.ok(map instanceof Map);
|
||||
});
|
||||
// Derived: `git log ...` answers exitCode:0 with empty stdout, which
|
||||
// trips the early `logRes.stdout.length === 0` return (empty Map) at
|
||||
// src/verification.cts:247 — `map instanceof Map` alone would also pass
|
||||
// for a non-empty map.
|
||||
const map = defaultPhaseCleanCommitTimesMs(tmpDir, ['a.md', 'b.md'], sharedStub);
|
||||
assert.deepStrictEqual(map, new Map());
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const childProcess = require('node:child_process');
|
||||
const fc = require('fast-check');
|
||||
const { createTempGitProject, createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { createFixture } = require('./fixtures/index.cjs');
|
||||
const { makeFaultyGit } = require('./helpers/faulty-deps.cjs');
|
||||
|
||||
const WORKTREE_SAFETY_PATH = path.join(
|
||||
@@ -127,8 +128,8 @@ describe('resolveWorktreeContext', () => {
|
||||
assert.strictEqual(context.mode, 'current_directory');
|
||||
});
|
||||
|
||||
// Counter-test: timeout returns object with effectiveRoot (Contract 6)
|
||||
test('returns valid result on timeout, not throw', () => {
|
||||
// Counter-test: timeout returns the canonical degraded shape, not just an object (Contract 6)
|
||||
test('returns effectiveRoot=cwd, mode=current_directory, reason=git_timed_out on timeout, not throw', () => {
|
||||
let threw = false;
|
||||
let result;
|
||||
try {
|
||||
@@ -137,11 +138,11 @@ describe('resolveWorktreeContext', () => {
|
||||
threw = true;
|
||||
}
|
||||
assert.strictEqual(threw, false, 'must not throw on timeout');
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
assert.ok(
|
||||
typeof result.effectiveRoot === 'string',
|
||||
'must return effectiveRoot string even on timeout'
|
||||
);
|
||||
assert.deepStrictEqual(result, {
|
||||
effectiveRoot: '/tmp',
|
||||
mode: 'current_directory',
|
||||
reason: 'git_timed_out',
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3050 DEFECT 2: timeout must be distinguishable from not_git_repo ─────
|
||||
@@ -439,7 +440,7 @@ describe('planWorktreePrune', () => {
|
||||
});
|
||||
|
||||
// Counter-test: timeout path (Contract 6)
|
||||
test('returns action=skip when execGit times out', () => {
|
||||
test('returns action=skip, reason=git_timed_out when execGit times out', () => {
|
||||
let threw = false;
|
||||
let result;
|
||||
try {
|
||||
@@ -450,9 +451,10 @@ describe('planWorktreePrune', () => {
|
||||
assert.strictEqual(threw, false, 'must not throw on timeout');
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
assert.strictEqual(result.action, 'skip');
|
||||
assert.ok(
|
||||
typeof result.reason === 'string' && result.reason.length > 0,
|
||||
'must return a non-empty reason when git times out'
|
||||
assert.strictEqual(
|
||||
result.reason,
|
||||
'git_timed_out',
|
||||
'must surface the specific git_timed_out reason, not a generic non-empty string'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -524,11 +526,15 @@ describe('executeWorktreePrunePlan', () => {
|
||||
});
|
||||
|
||||
// Counter-test: timeout path (Contract 6)
|
||||
test('returns ok:false when plan is skip (timeout path)', () => {
|
||||
test('returns {ok:false, action:skip, reason:git_timed_out, pruned:[]} when plan is skip (timeout path)', () => {
|
||||
const plan = planWorktreePrune('/tmp', {}, { execGit: makeTimeoutStub() });
|
||||
const result = executeWorktreePrunePlan(plan, { execGit: makeTimeoutStub() });
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
assert.strictEqual(result.ok, false, 'must return ok:false on timeout');
|
||||
assert.deepStrictEqual(result, {
|
||||
ok: false,
|
||||
action: 'skip',
|
||||
reason: 'git_timed_out',
|
||||
pruned: [],
|
||||
});
|
||||
});
|
||||
|
||||
// AC4 strict: timedOut must be surfaced as a first-class field
|
||||
@@ -578,7 +584,7 @@ describe('listLinkedWorktreePaths', () => {
|
||||
});
|
||||
|
||||
// Counter-test: failure path (Contract 6)
|
||||
test('returns ok:false on timeout, not throw', () => {
|
||||
test('returns ok:false, reason:git_timed_out on timeout, not throw', () => {
|
||||
let threw = false;
|
||||
let result;
|
||||
try {
|
||||
@@ -588,9 +594,10 @@ describe('listLinkedWorktreePaths', () => {
|
||||
}
|
||||
assert.strictEqual(threw, false, 'must not throw on timeout');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.ok(
|
||||
typeof result.reason === 'string' && result.reason.length > 0,
|
||||
'must return non-empty reason on timeout'
|
||||
assert.strictEqual(
|
||||
result.reason,
|
||||
'git_timed_out',
|
||||
'must surface the specific git_timed_out reason, not a generic non-empty string'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -641,8 +648,11 @@ describe('inspectWorktreeHealth', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// Counter-test: timeout path (Contract 6)
|
||||
test('returns ok:false when git times out', () => {
|
||||
// Counter-test: timeout path (Contract 6). This function's own reason on
|
||||
// timeout is not pinned by any sibling test in this file (unlike
|
||||
// planWorktreePrune/listLinkedWorktreePaths/snapshotWorktreeInventory,
|
||||
// which each have a dedicated "reason is git_timed_out" test) — pin it here.
|
||||
test('returns {ok:false, reason:git_timed_out, findings:[]} when git times out', () => {
|
||||
let threw = false;
|
||||
let result;
|
||||
try {
|
||||
@@ -651,13 +661,16 @@ describe('inspectWorktreeHealth', () => {
|
||||
threw = true;
|
||||
}
|
||||
assert.strictEqual(threw, false, 'must not throw on timeout');
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.deepStrictEqual(result, {
|
||||
ok: false,
|
||||
reason: 'git_timed_out',
|
||||
findings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('findings is empty array (not undefined) on timeout', () => {
|
||||
test('findings is an empty array, not undefined, on timeout', () => {
|
||||
const result = inspectWorktreeHealth('/tmp', {}, { execGit: makeTimeoutStub() });
|
||||
assert.strictEqual(Array.isArray(result.findings), true, 'findings must be an array even when ok:false');
|
||||
assert.deepStrictEqual(result.findings, [], 'findings must be [] (not undefined) even when ok:false');
|
||||
});
|
||||
|
||||
// Counter-test (B5, #3057): a statSync throw must surface as its own
|
||||
@@ -734,7 +747,7 @@ describe('snapshotWorktreeInventory', () => {
|
||||
});
|
||||
|
||||
// Counter-test: timeout path (Contract 6)
|
||||
test('returns ok:false with reason on timeout, not throw', () => {
|
||||
test('returns ok:false, reason:git_timed_out on timeout, not throw', () => {
|
||||
let threw = false;
|
||||
let result;
|
||||
try {
|
||||
@@ -745,9 +758,10 @@ describe('snapshotWorktreeInventory', () => {
|
||||
assert.strictEqual(threw, false, 'must not throw on timeout');
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.ok(
|
||||
typeof result.reason === 'string' && result.reason.length > 0,
|
||||
'must return non-empty reason on timeout'
|
||||
assert.strictEqual(
|
||||
result.reason,
|
||||
'git_timed_out',
|
||||
'must surface the specific git_timed_out reason, not a generic non-empty string'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3536,15 +3550,20 @@ describe('worktree-safety: resolveWorktreeRoot and pruneOrphanedWorktrees reloca
|
||||
describe('worktree-safety: resolveWorktreeRoot behaviour', () => {
|
||||
const worktreeSafety = require(WORKTREE_SAFETY_PATH);
|
||||
|
||||
test('resolveWorktreeRoot(createTempGitProject()) returns {root, reason} with a non-empty root', (t) => {
|
||||
const dir = createTempGitProject('gsd-wt-root-');
|
||||
// NOTE: createTempGitProject() always seeds .planning/ (createFixture's
|
||||
// planning:true default), which makes resolveWorktreeContext short-circuit
|
||||
// on the has_local_planning branch (src/worktree-safety.cts:203-216) BEFORE
|
||||
// ever calling git — so a fixture built with it can never reach the
|
||||
// git-based main_worktree path this test's name is about. Use a git fixture
|
||||
// WITHOUT .planning/ (and without the projectDoc PROJECT.md, which — since
|
||||
// projectDoc defaults to `git` in createFixture — would otherwise silently
|
||||
// recreate the .planning/ directory it's writing into) so resolveWorktreeRoot
|
||||
// actually reaches resolveWorktreeLinkage's real git rev-parse comparison.
|
||||
test('resolveWorktreeRoot(git repo with no local .planning) reaches the git-based main_worktree path, returns {root: dir, reason: main_worktree}', (t) => {
|
||||
const dir = createFixture({ prefix: 'gsd-wt-root-', planning: false, git: true, projectDoc: false });
|
||||
t.after(() => cleanup(dir));
|
||||
const result = worktreeSafety.resolveWorktreeRoot(dir);
|
||||
assert.ok(result && typeof result === 'object', 'must return an object, not a bare string');
|
||||
assert.ok(typeof result.root === 'string' && result.root.length > 0,
|
||||
`Expected non-empty root string, got: ${JSON.stringify(result)}`);
|
||||
assert.ok(typeof result.reason === 'string' && result.reason.length > 0,
|
||||
`Expected non-empty reason string, got: ${JSON.stringify(result)}`);
|
||||
assert.deepStrictEqual(result, { root: dir, reason: 'main_worktree' });
|
||||
});
|
||||
|
||||
test('resolveWorktreeRoot propagates git_timed_out via the injected execGit seam (#3050)', () => {
|
||||
|
||||
Reference in New Issue
Block a user