fix(#3518): pin uat_path to the phase's own UAT artifact via the shared phase-pinned resolver (#3525)
* fix(#3518): pin uat_path to the phase's own UAT artifact via the shared phase-pinned resolver Both uat_path projectors in src/init.cts picked the phase's UAT file with a bare .find() over unsorted readdir order — no phase-membership check, no ordering — so a stray cross-phase 04-UAT.md in phase 03's directory could become phase 03's uat_path, filesystem-dependently (creation order on APFS, hash order on ext4/XFS): two machines on the same commit could emit different uat_path values for the same phase. Route both sites through a new resolveUatFile in src/verification.cts, the UAT counterpart of #3357/#3492's resolveVerificationFile, sharing the exact selection rule via one extracted core (resolvePhaseArtifactFile): the phase's own <token>-UAT.md always wins; otherwise the alphabetically-first dashed candidate (deterministic everywhere); a bare UAT.md only via allowBare when no dashed candidate exists. resolveVerificationFile now delegates to the same core — behavior byte-identical. Guarded by: two end-to-end repro tests in tests/init.test.cjs (plan-phase and phase-op, red on the pre-fix readdir pick), resolveUatFile contract anchors and a src/-wide call-site guard in tests/verification-status.test.cjs. * chore(#3518): add changeset fragment for PR #3525 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -143,6 +143,31 @@ describe('init commands', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// #3518: init plan-phase's uat_path projector must resolve via the shared
|
||||
// resolveUatFile resolver (phase-pinned, deterministic) instead of a
|
||||
// hand-rolled `.find()` over unsorted readdir() order. A stray cross-phase
|
||||
// UAT artifact must never become THIS phase's uat_path, on any filesystem.
|
||||
// The stray is listed first in the fixture (creation-order filesystems) AND
|
||||
// sorts before the phase's own file (lexicographic-order filesystems), so
|
||||
// the pre-fix readdir pick loses on both ordering families.
|
||||
test('#3518: init plan-phase uat_path is phase-pinned — a stray cross-phase -UAT.md must not win', () => {
|
||||
seedPhase(tmpDir, '03-api', {
|
||||
'02-UAT.md': '# Stray cross-phase UAT artifact (belongs to phase 02)',
|
||||
'03-UAT.md': '# UAT',
|
||||
});
|
||||
writePlanningDocs(tmpDir);
|
||||
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(
|
||||
output.uat_path,
|
||||
absPlanningPath(tmpDir, 'phases', '03-api', '03-UAT.md'),
|
||||
'the phase\'s own 03-UAT.md must win over the stray cross-phase 02-UAT.md',
|
||||
);
|
||||
});
|
||||
|
||||
// #2056: normalizePhaseName() strips ANY [A-Z][A-Z0-9_]*- prefix as a project
|
||||
// code, so a foreign-prefixed workstream/task id like "MEM-01" collapsed to
|
||||
// "01" and resolved to the unrelated numeric Phase 01. init plan-phase must
|
||||
@@ -406,6 +431,29 @@ describe('init commands', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// #3518: init phase-op's uat_path projector — the second of the two
|
||||
// single-pick UAT sites in src/init.cts — same phase-pinned contract as
|
||||
// the plan-phase test above. Stray created first AND sorting first, so the
|
||||
// pre-fix readdir-order pick deterministically chose it on both ordering
|
||||
// families.
|
||||
test('#3518: init phase-op uat_path is phase-pinned — a stray cross-phase -UAT.md must not win', () => {
|
||||
seedPhase(tmpDir, '03-api', {
|
||||
'02-UAT.md': '# Stray cross-phase UAT artifact (belongs to phase 02)',
|
||||
'03-UAT.md': '# UAT',
|
||||
});
|
||||
writePlanningDocs(tmpDir);
|
||||
|
||||
const result = runGsdTools('init phase-op 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(
|
||||
output.uat_path,
|
||||
absPlanningPath(tmpDir, 'phases', '03-api', '03-UAT.md'),
|
||||
'the phase\'s own 03-UAT.md must win over the stray cross-phase 02-UAT.md',
|
||||
);
|
||||
});
|
||||
|
||||
test('init plan-phase detects has_reviews and reviews_path when REVIEWS.md exists', () => {
|
||||
seedPhase(tmpDir, '03-api', {
|
||||
'03-REVIEWS.md': '# Cross-AI Reviews',
|
||||
|
||||
Reference in New Issue
Block a user