The catalog half of a shared gap that was previously PINNED rather than fixed. This reader of ~/repos tested `.git` with a directory check, so a plain `git worktree add ~/repos/feature-x` produced a depth-1 sibling that can carry its own STATE.md and was dropped SILENTLY - no warning, no count, indistinguishable from a repo with nothing to say. A submodule fails the same way for the same reason. It was pinned, not fixed, because diverging would break the very same-name-from-both-readers invariant discoverRepos exists to hold. ~/repos answered YES to the gated question and reported their half landed. Verified here rather than taken on report: board.sh's Discovery block tests `-e`, and has done since board.sh was introduced (repo-mailbox 61e224c, first released v0.9.0) - so the released reader has no `-d` era at all, and only this side was ever the outlier. The two move together from here - change one, change the other. THE ACCEPTANCE TRAP, MEASURED RATHER THAN ASSUMED. This changes NO number against real ~/repos. Old and new builders were run back to back against the live tree and diffed: stdout and stderr byte-identical, both exit 0. Directly measured why: 0 directories at depth 1 or 2 currently carry .git as a file. A number standing still is not ambiguous here, it is the ONLY possible outcome, and it is why a fixture is not the best way to test this but the only way. FIXTURE GATED BEFORE BEHAVIOUR. The worktree fixture uses a real `git worktree add`, not a hand-written `.git` file - faking it would assert against our guess at git's on-disk format instead of against git. A separate test asserts the fixture itself: .git exists, is a file, is not a directory. Without it, the day git stops writing worktree .git as a file the behaviour tests would go green while measuring nothing, and a green run cannot distinguish that from success. The gate says which one it was, and says the premise of the -e rule is gone rather than inviting a test tweak. Two behaviour tests: a worktree at depth 1, and one under a polyrepo container at depth 2 - the same rule at both depths board.sh walks. Suite 90/90 across the six files (rollup 31 -> 34). check-versions 11 OK, 0 WARN, 0 ERROR. Real run unchanged at 8 repos, 17 markers, Output B 0 lines, 1 V6 warning, exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSX2v1m7Fz22BKrGJZuUpQ
430 lines
19 KiB
JavaScript
430 lines
19 KiB
JavaScript
// Tests for the state roll-up register builder (build-rollup-register.mjs).
|
|
// Style mirrors check-okf-parity.test.mjs / check-versions.test.mjs: node:test,
|
|
// pure-function core, zero npm deps. The CLI shell (filesystem glob over ~/repos)
|
|
// is NOT unit-tested here — only the pure builder core the contract lives in.
|
|
//
|
|
// Contract source (ratified): ~/.claude/coord/register.md — two outputs, three
|
|
// builder modes, V4 public-safety gate, V5 drift-warn, closed 7-token status vocab.
|
|
// Axes are catalog-owned; this file encodes them as fixtures.
|
|
//
|
|
// The em-dash (U+2014) is written as '—' throughout to keep the source ASCII-clean
|
|
// and to make the V4 "no em-dash in carrier" axis unambiguous.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, statSync, existsSync } from 'node:fs';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import {
|
|
STATUS_VOCAB,
|
|
parseMarkerLine,
|
|
parseCarrierLine,
|
|
validateCarrier,
|
|
computeDrift,
|
|
buildRegister,
|
|
extractMarkers,
|
|
discoverRepos,
|
|
} from './build-rollup-register.mjs';
|
|
|
|
const EMDASH = '—';
|
|
|
|
// --- Status vocabulary: the closed 7-token set (register.md "Status-vokabular") ---
|
|
test('STATUS_VOCAB is exactly the closed 7-token set; "active" is dropped', () => {
|
|
assert.deepEqual(
|
|
[...STATUS_VOCAB].sort(),
|
|
['blocked', 'deferred', 'done', 'in-progress', 'not-applicable', 'partial', 'planned'].sort(),
|
|
);
|
|
assert.ok(!STATUS_VOCAB.includes('active'), '"active" was folded into in-progress and is invalid');
|
|
});
|
|
|
|
// --- parseMarkerLine: Output A source (topic: status — prose) ---
|
|
test('parseMarkerLine: parses "topic: status — prose" from a STATE marker', () => {
|
|
const line = `llm-ingestion-okf: planned ${EMDASH} STEG 3 levert, STEG 4 neste`;
|
|
assert.deepEqual(parseMarkerLine(line), {
|
|
topic: 'llm-ingestion-okf',
|
|
status: 'planned',
|
|
prose: 'STEG 3 levert, STEG 4 neste',
|
|
});
|
|
});
|
|
|
|
test('parseMarkerLine: a status-only marker (no prose) parses with empty prose', () => {
|
|
assert.deepEqual(parseMarkerLine('some-topic: done'), {
|
|
topic: 'some-topic',
|
|
status: 'done',
|
|
prose: '',
|
|
});
|
|
});
|
|
|
|
test('parseMarkerLine: unknown status token -> null (not a marker line)', () => {
|
|
assert.equal(parseMarkerLine(`topic: active ${EMDASH} folded away`), null);
|
|
assert.equal(parseMarkerLine(`# STATE ${EMDASH} title heading`), null);
|
|
assert.equal(parseMarkerLine('just some prose without a marker'), null);
|
|
});
|
|
|
|
// --- parseCarrierLine: Output B source (topic: status ALONE) ---
|
|
test('parseCarrierLine: parses a bare "topic: status" carrier line', () => {
|
|
assert.deepEqual(parseCarrierLine('llm-ingestion-guard: planned'), {
|
|
topic: 'llm-ingestion-guard',
|
|
status: 'planned',
|
|
});
|
|
});
|
|
|
|
test('parseCarrierLine: rejects anything beyond the bare token (prose/em-dash/unknown)', () => {
|
|
assert.equal(parseCarrierLine(`topic: planned ${EMDASH} prose`), null, 'em-dash + prose is not a carrier line');
|
|
assert.equal(parseCarrierLine('topic: planned extra words'), null, 'trailing prose is not a carrier line');
|
|
assert.equal(parseCarrierLine('topic: active'), null, 'unknown token is malformed');
|
|
});
|
|
|
|
// --- V4 public-safety gate: validateCarrier rejects prose / em-dash / unknown ---
|
|
test('V4: a clean carrier (bare topic: token lines) validates ok', () => {
|
|
const text = 'llm-ingestion-guard: planned\nllm-ingestion-okf: not-applicable\n';
|
|
const r = validateCarrier(text);
|
|
assert.equal(r.ok, true);
|
|
assert.deepEqual(r.violations, []);
|
|
assert.deepEqual(r.entries, [
|
|
{ topic: 'llm-ingestion-guard', status: 'planned' },
|
|
{ topic: 'llm-ingestion-okf', status: 'not-applicable' },
|
|
]);
|
|
});
|
|
|
|
test('V4: blank lines are tolerated, not violations', () => {
|
|
const r = validateCarrier('\nllm-ingestion-guard: planned\n\n');
|
|
assert.equal(r.ok, true);
|
|
assert.equal(r.entries.length, 1);
|
|
});
|
|
|
|
test('V4: em-dash in a carrier line is a violation (the core public-safety axis)', () => {
|
|
const r = validateCarrier(`topic: planned ${EMDASH} leaked prose`);
|
|
assert.equal(r.ok, false);
|
|
assert.equal(r.violations.length, 1);
|
|
assert.equal(r.violations[0].reason, 'em-dash');
|
|
assert.equal(r.violations[0].lineNo, 1);
|
|
});
|
|
|
|
test('V4: prose after a valid token (no em-dash) is still a violation', () => {
|
|
const r = validateCarrier('topic: planned some trailing prose');
|
|
assert.equal(r.ok, false);
|
|
assert.equal(r.violations[0].reason, 'prose-after-token');
|
|
});
|
|
|
|
test('V4: an unknown status token is a violation', () => {
|
|
const r = validateCarrier('topic: active');
|
|
assert.equal(r.ok, false);
|
|
assert.equal(r.violations[0].reason, 'unknown-status');
|
|
});
|
|
|
|
test('V4: a structurally malformed line (no "topic: status") is a violation', () => {
|
|
const r = validateCarrier('this is not a marker at all');
|
|
assert.equal(r.ok, false);
|
|
assert.equal(r.violations[0].reason, 'malformed');
|
|
});
|
|
|
|
test('V4: reports every offending line, not just the first', () => {
|
|
const text = `a: planned\nb: active\nc: done ${EMDASH} x\n`;
|
|
const r = validateCarrier(text);
|
|
assert.equal(r.ok, false);
|
|
assert.deepEqual(r.violations.map((v) => v.lineNo), [2, 3]);
|
|
assert.deepEqual(r.violations.map((v) => v.reason), ['unknown-status', 'em-dash']);
|
|
});
|
|
|
|
// --- V5 drift-warn: STATE status != carrier status -> warn, STATE wins ---
|
|
test('V5: agreeing topics produce no drift warning', () => {
|
|
const markers = [{ topic: 't', status: 'planned' }];
|
|
const carrier = [{ topic: 't', status: 'planned' }];
|
|
assert.deepEqual(computeDrift(markers, carrier), []);
|
|
});
|
|
|
|
test('V5: disagreeing topic yields a drift warning carrying both sides', () => {
|
|
const markers = [{ topic: 't', status: 'done' }];
|
|
const carrier = [{ topic: 't', status: 'in-progress' }];
|
|
assert.deepEqual(computeDrift(markers, carrier), [
|
|
{ topic: 't', stateStatus: 'done', carrierStatus: 'in-progress' },
|
|
]);
|
|
});
|
|
|
|
test('V5: a carrier topic absent from STATE is not a drift (only shared topics compared)', () => {
|
|
const markers = [{ topic: 'a', status: 'done' }];
|
|
const carrier = [{ topic: 'b', status: 'planned' }];
|
|
assert.deepEqual(computeDrift(markers, carrier), []);
|
|
});
|
|
|
|
// --- buildRegister: three-mode assembly (register.md "Tre builder-moduser") ---
|
|
function repo(name, markers, carrier, carrierMode) {
|
|
return { name, markers, carrier, carrierMode };
|
|
}
|
|
|
|
test('mode "absent": repo has no carrier -> omitted from Output B, present in Output A', () => {
|
|
const repos = [repo('catalog', [{ topic: 'x', status: 'planned', prose: 'p' }], null, 'absent')];
|
|
const { outputB, outputA } = buildRegister({ repos });
|
|
assert.equal(outputB.length, 0, 'absent repo contributes nothing to public index B');
|
|
assert.equal(outputA.length, 1, 'but A (local) still covers it');
|
|
assert.deepEqual(outputA[0], { repo: 'catalog', topic: 'x', status: 'planned', prose: 'p' });
|
|
});
|
|
|
|
test('mode "committed": a valid carrier projects into Output B (STATE-authoritative status)', () => {
|
|
const repos = [
|
|
repo(
|
|
'guard',
|
|
[{ topic: 'llm-ingestion-guard', status: 'planned', prose: 'B6 detail' }],
|
|
'llm-ingestion-guard: planned\n',
|
|
'committed',
|
|
),
|
|
];
|
|
const { outputB } = buildRegister({ repos });
|
|
assert.deepEqual(outputB, ['llm-ingestion-guard: planned']);
|
|
});
|
|
|
|
test('mode "private": private side-channel carrier stays out of Output B', () => {
|
|
const repos = [
|
|
repo(
|
|
'secret',
|
|
[{ topic: 'z', status: 'in-progress', prose: 'p' }],
|
|
'z: in-progress\n',
|
|
'private',
|
|
),
|
|
];
|
|
const { outputB, outputA } = buildRegister({ repos });
|
|
assert.equal(outputB.length, 0, 'private-mode carrier never reaches the public index');
|
|
assert.equal(outputA.length, 1, 'but it is present in the local A index');
|
|
});
|
|
|
|
test('buildRegister: Output B projects STATE status on drift (STATE wins) and records the drift', () => {
|
|
const repos = [
|
|
repo(
|
|
'r',
|
|
[{ topic: 't', status: 'done', prose: 'finished' }],
|
|
't: in-progress\n', // carrier drifted behind STATE
|
|
'committed',
|
|
),
|
|
];
|
|
const { outputB, drift } = buildRegister({ repos });
|
|
assert.deepEqual(outputB, ['t: done'], 'STATE status wins in the projection, not the stale carrier');
|
|
assert.deepEqual(drift, [{ repo: 'r', topic: 't', stateStatus: 'done', carrierStatus: 'in-progress' }]);
|
|
});
|
|
|
|
test('buildRegister: a V4-violating committed carrier is blocked from B and its violation reported', () => {
|
|
const repos = [
|
|
repo(
|
|
'bad',
|
|
[{ topic: 't', status: 'planned', prose: 'p' }],
|
|
`t: planned ${EMDASH} leaked`,
|
|
'committed',
|
|
),
|
|
];
|
|
const { outputB, violations } = buildRegister({ repos });
|
|
assert.equal(outputB.length, 0, 'a repo whose carrier fails V4 must not pollute the public index');
|
|
assert.equal(violations.length, 1);
|
|
assert.equal(violations[0].repo, 'bad');
|
|
assert.equal(violations[0].reason, 'em-dash');
|
|
});
|
|
|
|
test('buildRegister: Output B is sorted and deterministic across repos', () => {
|
|
const repos = [
|
|
repo('b', [{ topic: 'zeta', status: 'done', prose: 'p' }], 'zeta: done\n', 'committed'),
|
|
repo('a', [{ topic: 'alpha', status: 'planned', prose: 'p' }], 'alpha: planned\n', 'committed'),
|
|
];
|
|
const { outputB } = buildRegister({ repos });
|
|
assert.deepEqual(outputB, ['alpha: planned', 'zeta: done'], 'stable sort so the public index has no spurious diffs');
|
|
});
|
|
|
|
// --- V6 marker-loss gate: a marker ATTEMPT that fails to parse must be reported ---
|
|
// parseMarkerLine returns null for two different facts ("not a marker" / "a marker that
|
|
// failed to parse"), so a malformed marker used to vanish with exit 0. V6 splits them.
|
|
test('V6: a well-formed marker extracts with no loss reported', () => {
|
|
const text = `# STATE\n\nsome-topic: planned ${EMDASH} prose after the em-dash\n`;
|
|
const { markers, losses } = extractMarkers(text);
|
|
assert.equal(markers.length, 1);
|
|
assert.deepEqual(losses, [], 'a parseable marker is not a loss');
|
|
});
|
|
|
|
test('V6: an ASCII "--" separator loses the marker today -> reported with line number', () => {
|
|
// The measured shape: two STATE.md files in ~/repos wrote "--" where the grammar
|
|
// requires the em-dash, and both vanished silently before this gate existed. The
|
|
// fixture is SYNTHETIC on purpose — marker prose is Output-A material (LOCAL-ONLY),
|
|
// so quoting a real one verbatim into this public repo would be the same
|
|
// durability-vs-publicity conflation the builder header warns about.
|
|
const text = '# STATE\n\nsome-topic: done -- prose after an ASCII separator\n';
|
|
const { markers, losses } = extractMarkers(text);
|
|
assert.equal(markers.length, 0, 'the line still does not parse as a marker');
|
|
assert.equal(losses.length, 1, 'but it must no longer disappear silently');
|
|
assert.equal(losses[0].lineNo, 3, 'the operator needs the line number to fix it');
|
|
assert.match(losses[0].line, /some-topic: done/);
|
|
});
|
|
|
|
test('V6: prose and unknown status tokens are NOT losses (no false positives)', () => {
|
|
const text = [
|
|
'just some prose without a marker',
|
|
`topic: active ${EMDASH} folded away, deliberately not a marker`,
|
|
`# STATE ${EMDASH} title heading`,
|
|
'',
|
|
].join('\n');
|
|
const { markers, losses } = extractMarkers(text);
|
|
assert.equal(markers.length, 0);
|
|
assert.deepEqual(losses, [], 'only a KNOWN status token makes a line a marker attempt');
|
|
});
|
|
|
|
test('V6: buildRegister surfaces marker losses per repo, naming the STATE file', () => {
|
|
const repos = [{
|
|
name: 'some-repo',
|
|
statePath: '/repos/some-repo/STATE.md',
|
|
markers: [], // losses-only: the repo must still be reported, not skipped
|
|
losses: [{ lineNo: 78, line: 'some-topic: planned -- prose after an ASCII separator' }],
|
|
carrier: null,
|
|
carrierMode: 'absent',
|
|
}];
|
|
const { markerLosses } = buildRegister({ repos });
|
|
assert.equal(markerLosses.length, 1);
|
|
assert.equal(markerLosses[0].repo, 'some-repo');
|
|
assert.equal(markerLosses[0].statePath, '/repos/some-repo/STATE.md');
|
|
assert.equal(markerLosses[0].lineNo, 78);
|
|
});
|
|
|
|
// --- D1 discovery: depth 1 + depth 2 under polyrepo containers (board.sh's rule) ---
|
|
// Identity is the RAW BASENAME so this builder and board.sh name the same repo the same
|
|
// way (board.sh:100). Mirrors board.sh:63-89: a dir that is itself a git repo contributes
|
|
// itself and is NOT descended into; a non-repo dir containing git repos is a container.
|
|
function tree(spec) {
|
|
const root = mkdtempSync(join(tmpdir(), 'rollup-discovery-'));
|
|
for (const [rel, state] of Object.entries(spec)) {
|
|
const dir = join(root, rel);
|
|
mkdirSync(dir, { recursive: true });
|
|
if (state !== null) {
|
|
mkdirSync(join(dir, '.git'), { recursive: true });
|
|
writeFileSync(join(dir, 'STATE.md'), state);
|
|
}
|
|
}
|
|
return root;
|
|
}
|
|
|
|
const MARKER = `topic-a: planned ${EMDASH} prose\n`;
|
|
|
|
test('D1: a depth-1 git repo with markers is discovered (unchanged behaviour)', () => {
|
|
const root = tree({ 'solo': MARKER });
|
|
const repos = discoverRepos(root);
|
|
assert.deepEqual(repos.map((r) => r.name), ['solo']);
|
|
});
|
|
|
|
test('D1: repos under a polyrepo container (depth 2) are discovered', () => {
|
|
// The measured gap: ktg-plugin-marketplace/{catalog,linkedin-studio,ms-ai-architect}
|
|
// carried 5 markers that the depth-1 glob could not see.
|
|
const root = tree({
|
|
'marketplace/catalog': MARKER,
|
|
'marketplace/linkedin-studio': MARKER,
|
|
'solo': MARKER,
|
|
});
|
|
const repos = discoverRepos(root);
|
|
assert.deepEqual(
|
|
repos.map((r) => r.name).sort(),
|
|
['catalog', 'linkedin-studio', 'solo'],
|
|
'container children contribute; the container itself never does',
|
|
);
|
|
});
|
|
|
|
test('D1: a git repo is NOT descended into (the nested same-name trap stays unreachable)', () => {
|
|
// claude-code-100x/claude-code-100x: the outer IS a git repo, so board.sh's rule never
|
|
// reaches the inner one. Matching that rule makes this collision structurally impossible
|
|
// rather than merely absent today.
|
|
const root = tree({ 'outer': MARKER, 'outer/outer': MARKER });
|
|
const repos = discoverRepos(root);
|
|
assert.deepEqual(repos.map((r) => r.name), ['outer'], 'the inner repo must not be reached');
|
|
});
|
|
|
|
test('D1: a STATE.md in a non-git directory is not a repo', () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'rollup-discovery-'));
|
|
mkdirSync(join(root, 'scratch'), { recursive: true });
|
|
writeFileSync(join(root, 'scratch', 'STATE.md'), MARKER);
|
|
assert.deepEqual(discoverRepos(root), [], 'no .git -> not a repo (board.sh:78)');
|
|
});
|
|
|
|
test('D1: a basename collision fails LOUDLY instead of silently overwriting', () => {
|
|
// Raw basename was chosen so this builder and the coord mailbox name repos alike;
|
|
// measured 0 collisions across 25 state-bearing dirs today, which is a fact, not a
|
|
// guarantee -- so the ambiguity must be an error, never a silent pick-one.
|
|
const root = tree({ 'dup': MARKER, 'container/dup': MARKER });
|
|
assert.throws(
|
|
() => discoverRepos(root),
|
|
(err) => {
|
|
assert.match(err.message, /collision/i);
|
|
assert.match(err.message, /dup/);
|
|
return true;
|
|
},
|
|
'two STATE.md files resolving to one name must stop the build',
|
|
);
|
|
});
|
|
|
|
// --- D1 discovery: git worktrees, where .git is a FILE and not a directory -------------
|
|
// Closes a gap that was previously PINNED rather than fixed: both readers tested `.git`
|
|
// with a directory check, so `git worktree add ~/repos/feature-x` produced a depth-1
|
|
// sibling that can carry its own STATE.md and was dropped SILENTLY here. board.sh's
|
|
// Discovery block tests `-e` (measured: repo-mailbox 61e224c, first released v0.9.0); this
|
|
// is the catalog half. Diverging would break the one invariant the mirroring exists to
|
|
// hold, so the two move together or not at all.
|
|
//
|
|
// This cannot be faked with mkdirSync: a fixture that writes `.git` as a file by hand
|
|
// would assert against our own guess at git's on-disk format rather than against git.
|
|
// Hence a REAL `git worktree add` -- and hence the fixture gate below.
|
|
function gitWorktreeTree() {
|
|
const root = mkdtempSync(join(tmpdir(), 'rollup-worktree-'));
|
|
const main = join(root, 'main');
|
|
mkdirSync(main, { recursive: true });
|
|
const git = (args, cwd) => execFileSync('git', args, { cwd, stdio: 'pipe' });
|
|
git(['init', '-q', '-b', 'main'], main);
|
|
git(['config', 'user.email', 'test@example.invalid'], main);
|
|
git(['config', 'user.name', 'test'], main);
|
|
writeFileSync(join(main, 'seed.txt'), 'seed\n');
|
|
git(['add', 'seed.txt'], main);
|
|
git(['commit', '-q', '-m', 'seed'], main);
|
|
// A worktree needs a commit to attach to, which is why the seed above exists.
|
|
const worktree = join(root, 'feature-x');
|
|
git(['worktree', 'add', '-q', '-b', 'feature-x', worktree], main);
|
|
writeFileSync(join(main, 'STATE.md'), MARKER);
|
|
writeFileSync(join(worktree, 'STATE.md'), MARKER);
|
|
return { root, main, worktree };
|
|
}
|
|
|
|
test('D1 fixture gate: git really does write a worktree .git as a FILE', () => {
|
|
// Gate the fixture BEFORE gating the behaviour. Without this, the day git starts
|
|
// writing worktree `.git` as a directory, the test below would go green while
|
|
// measuring nothing -- passing for the wrong reason is the failure mode that a
|
|
// green run cannot distinguish from success. This assertion says which one it was.
|
|
const { worktree } = gitWorktreeTree();
|
|
const dotGit = join(worktree, '.git');
|
|
assert.ok(existsSync(dotGit), 'a worktree must carry a .git entry at all');
|
|
assert.ok(
|
|
statSync(dotGit).isFile(),
|
|
'git no longer writes a worktree .git as a file -- the premise of the -e rule is gone, '
|
|
+ 'and both this builder and board.sh need re-deciding, not a test tweak',
|
|
);
|
|
assert.ok(!statSync(dotGit).isDirectory(), 'a file and a directory must not both be true');
|
|
});
|
|
|
|
test('D1: a git worktree at depth 1 is discovered (.git is a file, not a directory)', () => {
|
|
const { root } = gitWorktreeTree();
|
|
const repos = discoverRepos(root);
|
|
assert.deepEqual(
|
|
repos.map((r) => r.name).sort(),
|
|
['feature-x', 'main'],
|
|
'the worktree carries its own STATE.md and must not be dropped for having a .git FILE',
|
|
);
|
|
});
|
|
|
|
test('D1: a git worktree under a polyrepo container (depth 2) is discovered', () => {
|
|
// Same rule at the depth board.sh also walks: a container is not itself a repo, so a
|
|
// worktree parked inside one is reached by the same child pass as an ordinary repo.
|
|
const { root, main, worktree } = gitWorktreeTree();
|
|
const outer = mkdtempSync(join(tmpdir(), 'rollup-worktree-outer-'));
|
|
const container = join(outer, 'container');
|
|
mkdirSync(container, { recursive: true });
|
|
execFileSync('git', ['worktree', 'add', '-q', '-b', 'nested-x', join(container, 'nested-x')], {
|
|
cwd: main,
|
|
stdio: 'pipe',
|
|
});
|
|
writeFileSync(join(container, 'nested-x', 'STATE.md'), MARKER);
|
|
assert.ok(statSync(join(container, 'nested-x', '.git')).isFile(), 'fixture gate: .git is a file');
|
|
assert.deepEqual(
|
|
discoverRepos(outer).map((r) => r.name),
|
|
['nested-x'],
|
|
'a container holding only a worktree still contributes that worktree',
|
|
);
|
|
assert.ok(existsSync(join(root, 'main')) && existsSync(worktree), 'fixture roots intact');
|
|
});
|