enhance(#4014): an unreadable directory must not report as an empty one (#4163)

* test(#4014): add failing-first coverage for unreadable-vs-empty directory scope (epic #3473 B4)

* fix(#4014): an unreadable directory must not report as an empty one (epic #3473 B4)

* test(#4014): update hardcoded generateSlugInternal closing-brace line after import shift

src/core-utils.cts's new #4014 import block shifted every subsequent line by
6, moving generateSlugInternal's real closing brace from line 193 to 199.
tests/slug-derivation-drift-guard.test.cjs's MAJOR-1 fixture hardcodes that
line number to plant a synthetic violation immediately after the function's
real body; the guard script itself locates the boundary dynamically via
brace-matching and needed no change.

* docs(#4014): document the unreadable-directory scope signal and add changeset

* docs(#4014): backfill changeset PR number to #4163

* test(#4014): kill pre-existing core-utils.cjs mutation-score gap, unrelated to this issue's diff

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-02 08:12:57 -04:00
committed by GitHub
parent 2131fe13f3
commit 77dcdda534
14 changed files with 693 additions and 93 deletions

View File

@@ -4312,6 +4312,53 @@ describe('#3885 (ADR-3473 §8.5): countPhasePlansAndSummaries distinguishes unre
`ENOENT must be treated as genuinely absent, not reported as an error; got: ${phase.context_read_error}`,
);
});
// ─── #4014 (epic #3473 B4-unreadable) matrix rows 7-9 ───────────────────
//
// `countPhasePlansAndSummaries` (not exported — driven through
// cmdRoadmapAnalyze, the same style as T61/T62/T64 above) gains a
// `context_scope` field on its result, surfaced on `AnalyzePhase` as
// `context_scope` — the typed SCOPE-enum sibling of the existing
// `context_read_error` string field.
test('#4014 matrix row 7: readable phase dir with CONTEXT.md reports has_context:true, context_scope:complete', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, 'CONTEXT.md'), '# context\n');
const output = runAnalyzeCapturingStdout(tmpDir);
const phase = findPhase3(output);
assert.strictEqual(phase.has_context, true);
assert.strictEqual(phase.context_scope, 'complete');
assert.strictEqual(phase.context_read_error ?? null, null);
});
test('#4014 matrix row 8: unreadable phase dir reports context_scope:unreadable, distinct from a genuinely empty phase dir', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '---\nwave: 1\n---\n## Task 1\n');
const restore = injectReaddirFailure(phaseDir, 'EACCES');
let output;
try {
output = runAnalyzeCapturingStdout(tmpDir);
} finally {
restore();
}
const phase = findPhase3(output);
assert.strictEqual(phase.has_context, false);
assert.strictEqual(phase.context_scope, 'unreadable',
`an unreadable phase directory must report context_scope 'unreadable', distinct from a genuinely empty one; got: ${phase.context_scope}`);
});
test('#4014 matrix row 9: genuinely absent phase dir reports context_scope:complete (boundary — not unreadable)', () => {
// No phase directory created at all — matches T64's ENOENT-shaped absence.
const output = runAnalyzeCapturingStdout(tmpDir);
const phase = findPhase3(output);
assert.strictEqual(phase.has_context, false);
assert.strictEqual(phase.context_scope, 'complete',
`a genuinely absent phase directory must report context_scope 'complete', not 'unreadable'; got: ${phase.context_scope}`);
});
});
// ═══════════════════════════════════════════════════════════════════════════