diff --git a/scripts/resolve-verification-path.sh b/scripts/resolve-verification-path.sh index 2855dc2a..36a84246 100755 --- a/scripts/resolve-verification-path.sh +++ b/scripts/resolve-verification-path.sh @@ -81,6 +81,38 @@ phase_level_path() { fi fi + # Per-plan single-plan-phase fallback (issue #653): when no phase-level, plain, + # or wave-level artifact exists on disk but the writer produced exactly one + # per-plan {NN}-{MM}-VERIFICATION.md (the single-plan-phase case), adopt it so + # the QA gate does not see the phase as unverified and force a spurious re-run. + # Only a single match is adopted — multiple per-plan files are ambiguous and + # fall through to the canonical phase-level name (the integration artifact). + if [ -n "$phase_prefix" ]; then + # Match per-plan names with a >=2-digit, all-numeric MM component to mirror + # the writer's `printf %02d` plan numbering (resolve-artifact-path.sh): so + # 3-digit plan numbers (100+) are matched, but non-canonical names such as + # `{NN}-01a-VERIFICATION.md` or `{NN}-01-extra-VERIFICATION.md` are not. + # A nullglob array gives an exact entry count with no `ls` parsing and no + # newline-in-filename ambiguity; the glob's `*` is greedy, so a strict regex + # re-filter rejects its over-matches before the single-match decision. + local -a plan_files=() _plan_matches=() + local _nullglob_restore _pf _pf_base + _nullglob_restore=$(shopt -p nullglob 2>/dev/null || true) + shopt -s nullglob + _plan_matches=( "$PHASE_DIR/${phase_prefix}-"[0-9][0-9]*"-VERIFICATION.md" ) + eval "$_nullglob_restore" 2>/dev/null || true + for _pf in "${_plan_matches[@]}"; do + _pf_base=$(basename "$_pf") + if [[ "$_pf_base" =~ ^${phase_prefix}-[0-9][0-9]+-VERIFICATION\.md$ ]]; then + plan_files+=("$_pf") + fi + done + if [ "${#plan_files[@]}" -eq 1 ]; then + printf '%s\n' "${plan_files[0]}" + return 0 + fi + fi + if [ -n "$canonical_name" ]; then echo "$PHASE_DIR/$canonical_name" return 0 diff --git a/tests/qa-result-gate.bats b/tests/qa-result-gate.bats index 240addda..8f8df79b 100644 --- a/tests/qa-result-gate.bats +++ b/tests/qa-result-gate.bats @@ -997,6 +997,33 @@ VERIF [[ "$output" == *"qa_gate_writer=write-verification.sh"* ]] } +@test "single-plan phase with only a per-plan {NN}-{MM}-VERIFICATION.md → PROCEED_TO_UAT (issue #653)" { + # Regression: a passing single-plan phase whose verification artifact was + # written with the per-plan name must not be read as missing and forced to + # re-run. Before the fix the gate auto-resolved the (absent) phase-level + # 01-VERIFICATION.md and reported qa_gate_writer=missing → QA_RERUN_REQUIRED. + : > "$PHASE_DIR/01-01-PLAN.md" + { + echo "---" + echo "phase: 01" + echo "result: PASS" + echo "passed: 33" + echo "failed: 0" + echo "total: 33" + echo "plans_verified: [01-01]" + echo "writer: write-verification.sh" + echo "---" + } > "$PHASE_DIR/01-01-VERIFICATION.md" + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" == *"qa_gate_routing=PROCEED_TO_UAT"* ]] + # Confirm the per-plan artifact was actually read (not a missing-file false pass) + [[ "$output" == *"qa_gate_writer=write-verification.sh"* ]] + [[ "$output" == *"qa_gate_result=PASS"* ]] +} + @test "brownfield fallback to plain VERIFICATION.md" { # Create a phase dir WITHOUT numbered prefix BROWNFIELD_DIR="$TEST_DIR/legacy-phase" diff --git a/tests/resolve-verification-path.bats b/tests/resolve-verification-path.bats index 02959676..4923e0ae 100644 --- a/tests/resolve-verification-path.bats +++ b/tests/resolve-verification-path.bats @@ -307,4 +307,124 @@ EOF [ "$status" -eq 0 ] [ -z "$output" ] -} \ No newline at end of file +} + +# Issue #653: single-plan phase where the writer produced a per-plan +# {NN}-{MM}-VERIFICATION.md must resolve to that artifact, not a synthetic +# phase-level name the gate would read as missing. +@test "resolve-verification-path phase adopts the sole per-plan verification (issue #653 single-plan phase)" { + cat > "$PHASE_DIR/01-01-VERIFICATION.md" <<'EOF' +--- +result: PASS +plans_verified: [01-01] +--- +EOF + + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-01-VERIFICATION.md" ] +} + +@test "resolve-verification-path phase does NOT adopt per-plan files when more than one exists (ambiguous -> phase-level)" { + cat > "$PHASE_DIR/01-01-VERIFICATION.md" <<'EOF' +--- +result: PASS +--- +EOF + cat > "$PHASE_DIR/01-02-VERIFICATION.md" <<'EOF' +--- +result: PASS +--- +EOF + + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-VERIFICATION.md" ] +} + +@test "resolve-verification-path phase prefers an on-disk phase-level artifact over a per-plan one" { + cat > "$PHASE_DIR/01-VERIFICATION.md" <<'EOF' +--- +result: PASS +--- +EOF + cat > "$PHASE_DIR/01-01-VERIFICATION.md" <<'EOF' +--- +result: FAIL +--- +EOF + + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-VERIFICATION.md" ] +} + +@test "resolve-verification-path phase prefers wave files over a per-plan one" { + cat > "$PHASE_DIR/01-VERIFICATION-wave1.md" <<'EOF' +--- +result: PASS +--- +EOF + cat > "$PHASE_DIR/01-01-VERIFICATION.md" <<'EOF' +--- +result: FAIL +--- +EOF + + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-VERIFICATION-wave1.md" ] +} + +# Regression guard: a numbered phase with NO verification artifact of any kind +# must fall through the per-plan block and return the synthetic phase-level +# name (the pre-existing behavior the #653 block must not disturb). +@test "resolve-verification-path phase returns the synthetic phase-level name when no artifact exists" { + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-VERIFICATION.md" ] +} + +# Issue #653 (round 1 hardening): the writer's `printf %02d` plan numbering +# yields 3-digit names for the 100th+ plan, so the per-plan fallback must adopt +# a sole {NN}-{MMM}-VERIFICATION.md, not just 2-digit MM. +@test "resolve-verification-path phase adopts a sole 3-digit per-plan verification (100+ plan numbers)" { + cat > "$PHASE_DIR/01-100-VERIFICATION.md" <<'EOF' +--- +result: PASS +plans_verified: [01-100] +--- +EOF + + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-100-VERIFICATION.md" ] +} + +# Issue #653 (QA round 2): the per-plan glob is greedy; a strict regex re-filter +# must reject non-canonical names whose MM segment is not purely numeric (e.g. a +# trailing letter or an extra `-segment-`), so they are NOT mistaken for the sole +# per-plan artifact. Such names fall through to the canonical phase-level name. +@test "resolve-verification-path phase does NOT adopt a non-canonical per-plan name (over-match guard)" { + cat > "$PHASE_DIR/01-01a-VERIFICATION.md" <<'EOF' +--- +result: PASS +--- +EOF + cat > "$PHASE_DIR/01-01-extra-VERIFICATION.md" <<'EOF' +--- +result: PASS +--- +EOF + + run bash "$SCRIPTS_DIR/resolve-verification-path.sh" phase "$PHASE_DIR" + + [ "$status" -eq 0 ] + [ "$output" = "$PHASE_DIR/01-VERIFICATION.md" ] +}