From 44cc86b690e95b5684bcbd645b6ae06fdcd4f477 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 11:22:25 -0400 Subject: [PATCH 1/8] [feat] Teach QA scripts to defer version naming via "Unreleased" placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mid-cycle SemVer pivots (e.g., 1.1.0 → 2.0.0) force renaming the QA YAML, editing headers, and chasing references across consumer scripts. Extending the `[Unreleased]` convention from CHANGELOG to the QA tooling lets the version stay deferred until finalize-release locks it in. Both generators now branch on `nextTargetVersion == "Unreleased"`: filenames drop the `v` prefix and headers render without it, while SemVer values keep the existing `v`-prefixed form for forward compatibility with the future finalize step. The QA test plan generator also stops no-op-ing when the output file exists so a header refresh can be a separate, minimal-diff commit. Consumer auto-discovery in resolve-qa-labels.js and the verify-qa-scripts.sh glob are loosened so the new unreleased.yaml is picked up. New BATS coverage exercises both filename branches, the in-place refresh, the no-clobber early-exit on the testing-instructions side, and the existing error paths. Benefits: - No more mid-cycle file renames when a version target shifts - Symmetric behavior between generate-qa-test-plan and generate-release-testing-instructions - Forward-compatible: SemVer paths still work for finalize-release - BATS coverage prevents regressions on either branch --- .../scripts/generate-qa-test-plan.sh | 18 +- .../generate-release-testing-instructions.sh | 33 ++- .../scripts/resolve-qa-labels.js | 2 +- .../scripts/verify-qa-scripts.sh | 6 +- tests/shell/generate-qa-test-plan.bats | 215 ++++++++++++++++++ ...generate-release-testing-instructions.bats | 152 +++++++++++++ tests/shell/resolve-qa-labels.bats | 14 ++ tests/shell/validate-qa-coverage.bats | 23 ++ 8 files changed, 442 insertions(+), 21 deletions(-) create mode 100644 tests/shell/generate-qa-test-plan.bats create mode 100644 tests/shell/generate-release-testing-instructions.bats diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh index 12651c86..b80451a8 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh @@ -33,13 +33,17 @@ if [[ -z "$PUBLISHED_VERSION" ]]; then fi COMMIT=$(git -C "$REPO_ROOT" rev-parse --short HEAD) -BASE_NAME="qa-test-cases-v${NEXT_VERSION}" -OUTPUT_FILE="$QA_DIR/${BASE_NAME}.yaml" -if [[ -f "$OUTPUT_FILE" ]]; then - echo "$OUTPUT_FILE already exists — nothing to generate" - exit 0 +# Version-aware filename + label. "Unreleased" is the placeholder used during +# trunk-based development before finalize-release locks in a SemVer. +if [[ "$NEXT_VERSION" == "Unreleased" ]]; then + NEXT_LABEL="Unreleased" + BASE_NAME="qa-test-cases-unreleased" +else + NEXT_LABEL="v${NEXT_VERSION}" + BASE_NAME="qa-test-cases-v${NEXT_VERSION}" fi +OUTPUT_FILE="$QA_DIR/${BASE_NAME}.yaml" # Suffix sort fix: unsuffixed files (v1.1.0.yaml) sort AFTER suffixed files # (v1.1.0-001.yaml) because '.' > '-' in ASCII. Normalize by appending -000 @@ -61,10 +65,10 @@ if [[ -z "$PREVIOUS_YAML" ]]; then exit 1 fi -HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → v${NEXT_VERSION} +HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → ${NEXT_LABEL} # # Scope: Changes accumulated between the vscode-extension-v${PUBLISHED_VERSION} release tag and the current -# main branch tip, targeting v${NEXT_VERSION}. Created at commit ${COMMIT}. +# main branch tip, targeting ${NEXT_LABEL}. Created at commit ${COMMIT}. # # Source of truth for this QA cycle. Run \`pnpm generate:qa-issue -- qa/$(basename "$OUTPUT_FILE")\` # to create the corresponding GitHub issue tracker (parent issue + per-section sub-issues). diff --git a/packages/rangelink-vscode-extension/scripts/generate-release-testing-instructions.sh b/packages/rangelink-vscode-extension/scripts/generate-release-testing-instructions.sh index 61744e9c..d44c866b 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-release-testing-instructions.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-release-testing-instructions.sh @@ -42,7 +42,21 @@ elif ! gh auth status &>/dev/null; then WARNINGS+=" ${YELLOW}Warning: gh CLI not authenticated — Phase 2 (GitHub QA Issues) requires auth${NC}\n" fi -echo -e "${GREEN}Generating release testing instructions for v${NEXT_VERSION}${NC}" +# Version-aware filename + label. "Unreleased" is the placeholder used during +# trunk-based development before finalize-release locks in a SemVer. +if [[ "$NEXT_VERSION" == "Unreleased" ]]; then + NEXT_LABEL="Unreleased" + BASE_NAME="release-testing-instructions-unreleased" + QA_YAML_FILENAME="qa-test-cases-unreleased.yaml" + QA_CHECKLIST_SLUG="unreleased" +else + NEXT_LABEL="v${NEXT_VERSION}" + BASE_NAME="release-testing-instructions-v${NEXT_VERSION}" + QA_YAML_FILENAME="qa-test-cases-v${NEXT_VERSION}.yaml" + QA_CHECKLIST_SLUG="v${NEXT_VERSION}" +fi + +echo -e "${GREEN}Generating release testing instructions for ${NEXT_LABEL}${NC}" if [[ -n "$WARNINGS" ]]; then echo -e "\n${YELLOW}Prerequisites warnings (non-blocking):${NC}" @@ -51,7 +65,6 @@ fi # --- Output file --- -BASE_NAME="release-testing-instructions-v${NEXT_VERSION}" OUTPUT_FILE="$QA_DIR/${BASE_NAME}.md" if [[ -f "$OUTPUT_FILE" ]]; then @@ -62,10 +75,10 @@ fi # --- Generate markdown --- cat > "$OUTPUT_FILE" <.md\`. +The generated checklist is at \`qa/output/qa-checklist-${QA_CHECKLIST_SLUG}-.md\`. Each TC is annotated with its automation status and reason. Cursor and Ubuntu TCs are grouped into their own sections with the required run commands inline. --- @@ -212,7 +225,7 @@ When all checks pass, generate the publishing instructions: pnpm generate:publish-instructions:vscode-extension \`\`\` -This validates the release environment and creates a version-specific publishing guide at \`publishing-instructions/publish-vscode-extension-v${NEXT_VERSION}.md\`. +This validates the release environment and creates a version-specific publishing guide at \`publishing-instructions/publish-vscode-extension-${NEXT_LABEL}.md\`. EOF # Format with prettier diff --git a/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js b/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js index d8a39fff..db24c676 100755 --- a/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js +++ b/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js @@ -96,7 +96,7 @@ if (!yamlPath) { try { files = fs .readdirSync(qaDir) - .filter((f) => f.startsWith('qa-test-cases-v') && f.endsWith('.yaml')); + .filter((f) => f.startsWith('qa-test-cases-') && f.endsWith('.yaml')); } catch (err) { process.stderr.write(`Error: Cannot read QA directory ${qaDir}: ${err.message}\n`); process.exit(1); diff --git a/packages/rangelink-vscode-extension/scripts/verify-qa-scripts.sh b/packages/rangelink-vscode-extension/scripts/verify-qa-scripts.sh index c93ccfdb..3c5a03c3 100755 --- a/packages/rangelink-vscode-extension/scripts/verify-qa-scripts.sh +++ b/packages/rangelink-vscode-extension/scripts/verify-qa-scripts.sh @@ -71,13 +71,13 @@ echo " Using committed YAML: $(basename "$LATEST_YAML")" echo "" run_check "validate-qa-coverage" ./scripts/validate-qa-coverage.sh "$LATEST_YAML" -check_artifact "validate-qa-coverage" "output/qa-coverage-report-v*.txt" +check_artifact "validate-qa-coverage" "output/qa-coverage-report-*.txt" run_check "generate-release-testing-instructions" ./scripts/generate-release-testing-instructions.sh -check_artifact "generate-release-testing-instructions" "release-testing-instructions-v*.md" +check_artifact "generate-release-testing-instructions" "release-testing-instructions-*.md" run_check "generate-qa-test-plan" ./scripts/generate-qa-test-plan.sh -check_artifact "generate-qa-test-plan" "qa-test-cases-v*.yaml" +check_artifact "generate-qa-test-plan" "qa-test-cases-*.yaml" # dry-run produces no artifact — exit code is sufficient run_check "generate-qa-issue (dry-run)" bash -c 'echo "y" | ./scripts/generate-qa-issue.sh --dry-run' diff --git a/tests/shell/generate-qa-test-plan.bats b/tests/shell/generate-qa-test-plan.bats new file mode 100644 index 00000000..c34c0aad --- /dev/null +++ b/tests/shell/generate-qa-test-plan.bats @@ -0,0 +1,215 @@ +#!/usr/bin/env bats + +load test_helper + +REAL_SCRIPT="$PROJECT_ROOT/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh" + +# ── Fixture scaffolding ──────────────────────────────────────────────────────── +# +# The script derives SCRIPT_DIR/PACKAGE_DIR/REPO_ROOT/PACKAGE_JSON/QA_DIR from +# $0 + git. We copy it into a temp tree so $0 resolves inside the fixture and +# stub git so the script does not depend on the real repo state. + +setup_fixture() { + FIXTURE_ROOT="$TEST_TEMP_DIR" + mkdir -p "$FIXTURE_ROOT/scripts" + mkdir -p "$FIXTURE_ROOT/qa" + + cp "$REAL_SCRIPT" "$FIXTURE_ROOT/scripts/generate-qa-test-plan.sh" + SCRIPT="$FIXTURE_ROOT/scripts/generate-qa-test-plan.sh" + + stub_dir + make_stub "git" < "$FIXTURE_ROOT/package.json" + } + + write_yaml() { + cat > "$FIXTURE_ROOT/qa/$1" + } +} + +# Minimal valid yaml body — header comments stripped before test_cases:. +minimal_previous_yaml() { + cat <<'EOF' +# Old header that will be stripped +# Another comment line +test_cases: + - id: foo-001 + feature: 'Foo' + scenario: 'Test scenario' + automated: true +EOF +} + +# ── Filename: Unreleased vs SemVer ───────────────────────────────────────────── + +@test "Unreleased nextTargetVersion produces qa-test-cases-unreleased.yaml" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + write_yaml "qa-test-cases-v1.0.0.yaml" < <(minimal_previous_yaml) + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ -f "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" ]] +} + +@test "SemVer nextTargetVersion produces v-prefixed yaml (forward compat with finalize)" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "2.0.0" +} +EOF + write_yaml "qa-test-cases-v1.0.0.yaml" < <(minimal_previous_yaml) + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ -f "$FIXTURE_ROOT/qa/qa-test-cases-v2.0.0.yaml" ]] +} + +# ── Header rendering ─────────────────────────────────────────────────────────── + +@test "Unreleased: header reads 'v → Unreleased' with no v prefix on right" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + write_yaml "qa-test-cases-v1.0.0.yaml" < <(minimal_previous_yaml) + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local header + header=$(head -1 "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml") + [[ "$header" == "# RangeLink QA Test Cases — v1.0.0 → Unreleased" ]] + # Negative assertion: no vUnreleased anywhere in the header. + ! grep -q "vUnreleased" "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" +} + +@test "SemVer: header keeps the v prefix on the right" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "2.0.0" +} +EOF + write_yaml "qa-test-cases-v1.0.0.yaml" < <(minimal_previous_yaml) + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local header + header=$(head -1 "$FIXTURE_ROOT/qa/qa-test-cases-v2.0.0.yaml") + [[ "$header" == "# RangeLink QA Test Cases — v1.0.0 → v2.0.0" ]] +} + +# ── Idempotent refresh-in-place (early-exit dropped) ─────────────────────────── + +@test "re-running on existing unreleased.yaml refreshes header without losing body" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + # Pre-existing unreleased.yaml acts as PREVIOUS_YAML for itself. + write_yaml "qa-test-cases-unreleased.yaml" <<'EOF' +# Stale header +test_cases: + - id: foo-001 + feature: 'Foo' + scenario: 'Existing body' + automated: true +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + # Header refreshed. + local header + header=$(head -1 "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml") + [[ "$header" == "# RangeLink QA Test Cases — v1.0.0 → Unreleased" ]] + # Body preserved verbatim. + grep -q "scenario: 'Existing body'" "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" +} + +@test "re-run is idempotent: second invocation produces identical file" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + write_yaml "qa-test-cases-v1.0.0.yaml" < <(minimal_previous_yaml) + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local checksum_after_first + checksum_after_first=$(shasum "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" | cut -d' ' -f1) + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local checksum_after_second + checksum_after_second=$(shasum "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" | cut -d' ' -f1) + + [[ "$checksum_after_first" == "$checksum_after_second" ]] +} + +# ── Error paths ──────────────────────────────────────────────────────────────── + +@test "missing nextTargetVersion still errors" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 1 ]] + [[ "$output" =~ "nextTargetVersion not set" ]] +} + +@test "missing version still errors" { + setup_fixture + write_package_json <<'EOF' +{ + "nextTargetVersion": "Unreleased" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 1 ]] + [[ "$output" =~ "version not set" ]] +} + +@test "no previous YAML still errors" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 1 ]] + [[ "$output" =~ "no previous QA YAML found" ]] +} diff --git a/tests/shell/generate-release-testing-instructions.bats b/tests/shell/generate-release-testing-instructions.bats new file mode 100644 index 00000000..58e107c5 --- /dev/null +++ b/tests/shell/generate-release-testing-instructions.bats @@ -0,0 +1,152 @@ +#!/usr/bin/env bats + +load test_helper + +REAL_SCRIPT="$PROJECT_ROOT/packages/rangelink-vscode-extension/scripts/generate-release-testing-instructions.sh" + +setup_fixture() { + FIXTURE_ROOT="$TEST_TEMP_DIR" + mkdir -p "$FIXTURE_ROOT/scripts" + mkdir -p "$FIXTURE_ROOT/qa" + + cp "$REAL_SCRIPT" "$FIXTURE_ROOT/scripts/generate-release-testing-instructions.sh" + SCRIPT="$FIXTURE_ROOT/scripts/generate-release-testing-instructions.sh" + + stub_dir + make_stub "git" < "$FIXTURE_ROOT/package.json" + } +} + +# ── Output filename: Unreleased vs SemVer ────────────────────────────────────── + +@test "Unreleased nextTargetVersion produces release-testing-instructions-unreleased.md" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ -f "$FIXTURE_ROOT/qa/release-testing-instructions-unreleased.md" ]] + [[ ! -f "$FIXTURE_ROOT/qa/release-testing-instructions-vUnreleased.md" ]] +} + +@test "SemVer nextTargetVersion produces v-prefixed markdown (forward compat with finalize)" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "2.0.0" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ -f "$FIXTURE_ROOT/qa/release-testing-instructions-v2.0.0.md" ]] +} + +# ── Internal yaml reference ──────────────────────────────────────────────────── + +@test "Unreleased: emitted markdown references qa-test-cases-unreleased.yaml" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local out="$FIXTURE_ROOT/qa/release-testing-instructions-unreleased.md" + grep -q "qa/qa-test-cases-unreleased.yaml" "$out" + # Negative assertion: no vUnreleased in the yaml reference. + ! grep -q "qa-test-cases-vUnreleased.yaml" "$out" +} + +@test "SemVer: emitted markdown references qa-test-cases-v.yaml" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "2.0.0" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local out="$FIXTURE_ROOT/qa/release-testing-instructions-v2.0.0.md" + grep -q "qa/qa-test-cases-v2.0.0.yaml" "$out" +} + +# ── Header rendering ─────────────────────────────────────────────────────────── + +@test "Unreleased: title and scope render with no v prefix on right side" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local out="$FIXTURE_ROOT/qa/release-testing-instructions-unreleased.md" + grep -q "RangeLink VS Code Extension Unreleased" "$out" + grep -q "Changes from v1.0.0 → Unreleased" "$out" + ! grep -q "vUnreleased" "$out" +} + +# ── Error paths ──────────────────────────────────────────────────────────────── + +@test "missing nextTargetVersion still errors" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0" +} +EOF + + run "$SCRIPT" + [[ "$status" -eq 1 ]] + [[ "$output" =~ "nextTargetVersion not set" ]] +} + +# ── Early-exit (kept for this script per A006 scope) ─────────────────────────── + +@test "file-exists early-exit still applies (no clobber)" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + echo "preexisting content" > "$FIXTURE_ROOT/qa/release-testing-instructions-unreleased.md" + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "already exists" ]] + # File unchanged. + local content + content=$(cat "$FIXTURE_ROOT/qa/release-testing-instructions-unreleased.md") + [[ "$content" == "preexisting content" ]] +} diff --git a/tests/shell/resolve-qa-labels.bats b/tests/shell/resolve-qa-labels.bats index 6aa825b9..64bc9d29 100644 --- a/tests/shell/resolve-qa-labels.bats +++ b/tests/shell/resolve-qa-labels.bats @@ -209,6 +209,20 @@ EOF [[ "$output" == "fifth-001" ]] } +@test "resolve-qa-labels: auto-discovery picks qa-test-cases-unreleased.yaml when it is the only file" { + setup_fixture + write_yaml "qa-test-cases-unreleased.yaml" <<'EOF' +test_cases: + - id: unreleased-001 + feature: Unreleased + scenario: Single file + automated: true +EOF + run node "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ "$output" == "unreleased-001" ]] +} + # ════════════════════════════════════════════════════════════════════ # YAML parsing # ════════════════════════════════════════════════════════════════════ diff --git a/tests/shell/validate-qa-coverage.bats b/tests/shell/validate-qa-coverage.bats index 90f1eaa2..8c53f1fd 100644 --- a/tests/shell/validate-qa-coverage.bats +++ b/tests/shell/validate-qa-coverage.bats @@ -247,6 +247,29 @@ EOF [[ -n "$found" ]] } +@test "report filename uses 'unreleased' slug when input is qa-test-cases-unreleased.yaml" { + setup_fixture + write_yaml "qa-test-cases-unreleased.yaml" <<'EOF' +test_cases: + - id: foo-001 + feature: 'Foo' + scenario: 'Test' + automated: true +EOF + write_test_file "suite.test.ts" <<< "test('foo-001: does things', () => {});" + export STUB_AUTOMATED_IDS="foo-001" + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + local found + found=$(find "$FIXTURE_ROOT/qa/output" -name "qa-coverage-report-unreleased-*" -type f 2>/dev/null | head -1) + [[ -n "$found" ]] + # Negative assertion: no v-prefixed report. + local v_prefixed + v_prefixed=$(find "$FIXTURE_ROOT/qa/output" -name "qa-coverage-report-vunreleased-*" -type f 2>/dev/null | head -1) + [[ -z "$v_prefixed" ]] +} + @test "report contains header with QA YAML and test paths" { setup_fixture write_yaml "qa-test-cases-v1.0.0.yaml" <<'EOF' From 9d846007ae465753cc461372106bc0ed70a7ebdf Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 11:23:13 -0400 Subject: [PATCH 2/8] Renamed the file --- .../{qa-test-cases-v1.1.0.yaml => qa-test-cases-unreleased.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/rangelink-vscode-extension/qa/{qa-test-cases-v1.1.0.yaml => qa-test-cases-unreleased.yaml} (100%) diff --git a/packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0.yaml b/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml similarity index 100% rename from packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0.yaml rename to packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml From 806d53537782ac25c8eac13fa9deb7c75a0151b1 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 11:30:33 -0400 Subject: [PATCH 3/8] Remove commit from the output so there is less SCM noise --- .../scripts/generate-qa-test-plan.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh index b80451a8..b62b8adc 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh @@ -32,8 +32,6 @@ if [[ -z "$PUBLISHED_VERSION" ]]; then exit 1 fi -COMMIT=$(git -C "$REPO_ROOT" rev-parse --short HEAD) - # Version-aware filename + label. "Unreleased" is the placeholder used during # trunk-based development before finalize-release locks in a SemVer. if [[ "$NEXT_VERSION" == "Unreleased" ]]; then @@ -68,7 +66,7 @@ fi HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → ${NEXT_LABEL} # # Scope: Changes accumulated between the vscode-extension-v${PUBLISHED_VERSION} release tag and the current -# main branch tip, targeting ${NEXT_LABEL}. Created at commit ${COMMIT}. +# main branch tip, targeting ${NEXT_LABEL}. # # Source of truth for this QA cycle. Run \`pnpm generate:qa-issue -- qa/$(basename "$OUTPUT_FILE")\` # to create the corresponding GitHub issue tracker (parent issue + per-section sub-issues). From eb610bc91183b9feb3f22bff5868c6ad527af143 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 11:55:45 -0400 Subject: [PATCH 4/8] [chore] Set nextTargetVersion to "Unreleased" and refresh QA YAML header Activates the deferred-version pattern by flipping nextTargetVersion from a SemVer placeholder to the literal "Unreleased". The QA YAML header is regenerated in the same commit so the file accurately reflects the new state and the stale self-reference (qa-test-cases-v1.1.0-003.yaml) is corrected. Body content unchanged. Cleaned up the header template at the same time --- .../scripts/generate-qa-test-plan.sh | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh index b62b8adc..9916e3d3 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh @@ -9,8 +9,8 @@ set -euo pipefail # Reads nextTargetVersion from package.json to name the output file. # Reads version (last published) to document the scope in the header. # -# Filename: qa-test-cases-v.yaml -# If the file already exists, exits successfully (no-op). +# Filename: qa-test-cases-v.yaml, or qa-test-cases-unreleased.yaml when nextTargetVersion is "Unreleased". +# Always regenerates: header is freshly emitted, body is carried forward from the highest-sorted existing yaml. # # Requires: jq @@ -73,8 +73,8 @@ HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → ${NEXT_LABEL} # # Schema: # id: Unique test case identifier (-NNN) -# feature: Feature area / CHANGELOG section -# scenario: One-line description of the specific scenario being tested +# feature: Feature area +# scenario: One-line description of what is tested # labels: Optional tags (e.g., cursor, ubuntu, requires-extensions) # preconditions: Required setup steps — only on \`automated: false\` entries # steps: Ordered test actions — only on \`automated: false\` entries @@ -88,11 +88,7 @@ HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → ${NEXT_LABEL} # non_automatable_reason: Required when \`automated: false\`. Why this TC cannot be # automated (even assisted): # platform-specific — requires Win/Linux; CI runs on macOS -# ide-specific — requires Cursor IDE; not in VS Code host -# -# For \`automated: true\` and \`automated: assisted\` entries the integration test in -# src/__integration-tests__/suite/ is the canonical source of setup and actions — -# \`preconditions:\` and \`steps:\` are omitted to prevent duplication and drift." +# ide-specific — requires Cursor IDE; not in VS Code host" BODY=$(sed '1,/^test_cases:/{ /^#/d; }' "$PREVIOUS_YAML") From 410ba5f91f18862ab5cd635a586c55ff2e35056e Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 12:11:23 -0400 Subject: [PATCH 5/8] [docs] Update QA workflow docs to reflect Unreleased deferred-version pattern CLAUDE.md QA002, TESTING.md, qa-suggest SKILL.md, and plan-integration-test agent all referenced the old version-based filename convention (qa-test-cases-v1.1.0.yaml). Updated to describe qa-test-cases-unreleased.yaml as the trunk-based development filename, with version locking deferred to finalize-release. --- .claude/agents/plan-integration-test.md | 2 +- .claude/skills/qa-suggest/SKILL.md | 14 +++++++------- CLAUDE.md | 8 ++++---- packages/rangelink-vscode-extension/TESTING.md | 8 ++++---- packages/rangelink-vscode-extension/package.json | 2 +- .../qa/qa-test-cases-unreleased.yaml | 13 +++++-------- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/.claude/agents/plan-integration-test.md b/.claude/agents/plan-integration-test.md index f53994da..16e3e8c8 100644 --- a/.claude/agents/plan-integration-test.md +++ b/.claude/agents/plan-integration-test.md @@ -26,7 +26,7 @@ A complete test template with: ### Step 1: Read the QA YAML -Find the TC in `packages/rangelink-vscode-extension/qa/qa-test-cases-v1.1.0*.yaml` (use the latest file). Extract: +Find the TC in `packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml` (or `qa-test-cases-v.yaml` for a released version — use the latest file). Extract: - `scenario` — what the test verifies - `preconditions` — what setup is needed diff --git a/.claude/skills/qa-suggest/SKILL.md b/.claude/skills/qa-suggest/SKILL.md index 7c33077c..2425be1f 100644 --- a/.claude/skills/qa-suggest/SKILL.md +++ b/.claude/skills/qa-suggest/SKILL.md @@ -21,10 +21,10 @@ Read packages/rangelink-vscode-extension/package.json Extract: -- `nextTargetVersion` — the upcoming release version (e.g., `1.1.0`) +- `nextTargetVersion` — the upcoming release version (`"Unreleased"` during trunk-based development, or a SemVer like `"1.1.0"` once locked in) - `version` — the last published version (e.g., `1.0.0`) -**If `nextTargetVersion` is not set**, STOP: "Set `nextTargetVersion` in `packages/rangelink-vscode-extension/package.json` before running `/qa-suggest`." +**If `nextTargetVersion` is not set**, STOP: "Set `nextTargetVersion` in `packages/rangelink-vscode-extension/package.json` (e.g., `"Unreleased"`) before running `/qa-suggest`." ## Step 2: Locate QA YAMLs @@ -34,10 +34,10 @@ Find the current cycle's YAML and the previous version's YAML: Glob(pattern="packages/rangelink-vscode-extension/qa/qa-test-cases-*.yaml") ``` -- **Current YAML**: the file matching `qa-test-cases-v.yaml` (e.g., `qa-test-cases-v1.1.0.yaml` — a single file, no suffix) -- **Previous YAML**: the most recent file that does NOT contain `v` — this is the baseline for diffing +- **Current YAML**: `qa-test-cases-unreleased.yaml` during trunk-based development, or `qa-test-cases-v.yaml` once the version is locked in +- **Previous YAML**: the most recent released version's YAML (e.g., `qa-test-cases-v1.0.0.yaml`) — this is the baseline for diffing -**If the current YAML doesn't exist**, STOP: "No QA YAML found for v``. Run `pnpm generate:qa-test-plan` first." +**If the current YAML doesn't exist**, STOP: "No QA YAML found. Run `pnpm generate:qa-test-plan` first." Read both YAML files in parallel. @@ -144,7 +144,7 @@ Create a scratchpad file for the report. Use the `/scratchpad` conventions: 1. Determine the issue context from the current git branch (e.g., `issues/382` → issue ID `382`) 2. Find the next available sequence number in `.claude-work/issues//scratchpads/` -3. Write the scratchpad to `.claude-work/issues//scratchpads/NNNN-qa-suggest-v.txt` +3. Write the scratchpad to `.claude-work/issues//scratchpads/NNNN-qa-suggest.txt` If no issue context can be determined, use `.claude-work/scratchpads/` instead. @@ -153,7 +153,7 @@ The scratchpad should contain these sections in order: ### Header ```text -# QA Suggest — v → v +# QA Suggest — v → Unreleased ## What to do next diff --git a/CLAUDE.md b/CLAUDE.md index ff21b398..d76c12ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -605,11 +605,11 @@ QA YAML is a single file per release cycle - Edit `qa-test-cases-v.yaml` in place during the current release cycle — add TCs, update automated status, adjust preconditions - When moving to a new version (e.g., v1.1.0 → v1.2.0), use `pnpm generate:qa-test-plan:vscode-extension` to create the new file from the previous one - Edit a YAML file from a previous release cycle (e.g., don't touch v1.0.0.yaml after v1.1.0 work starts) + During trunk-based development, edit `qa-test-cases-unreleased.yaml` — add TCs, update automated status, adjust preconditions. The version is deferred until `finalize-release` locks it in. + When starting a new release cycle, use `pnpm generate:qa-test-plan:vscode-extension` to create a fresh `qa-test-cases-unreleased.yaml` carrying forward all TCs from the previous version's YAML + Edit a YAML file from a past release (e.g., don't touch v1.0.0.yaml after it has shipped) Fixing typos or updating `automated` status (`true`/`assisted`/`false`) in the current file is always allowed - Each version has exactly one QA file. Suffix files (v1.1.0-001.yaml, -002.yaml, etc.) were journal snapshots from an older workflow and are no longer created. + Each version has exactly one QA file. The `Unreleased` placeholder defers version naming until release time so the filename need not change mid-cycle. diff --git a/packages/rangelink-vscode-extension/TESTING.md b/packages/rangelink-vscode-extension/TESTING.md index 936467a8..b80f9ca3 100644 --- a/packages/rangelink-vscode-extension/TESTING.md +++ b/packages/rangelink-vscode-extension/TESTING.md @@ -37,7 +37,7 @@ All `test:release*` commands accept `--label ` (include TCs with QA YAML la ```mermaid flowchart TD Z[generate:release-testing-instructions] -.->|generates guide| A - A[Set nextTargetVersion] --> B[generate:qa-test-plan] + A[nextTargetVersion: Unreleased] --> B[generate:qa-test-plan] B --> C[/qa-suggest in Claude Code/] C --> D[Review + append new TCs] D --> E[Commit YAML] @@ -275,12 +275,12 @@ The QA test plan is a version-scoped YAML file that tracks both automated and ma ### File location and naming ```text -qa/qa-test-cases-v.yaml +qa/qa-test-cases-unreleased.yaml ``` -Example: `qa/qa-test-cases-v1.1.0.yaml` +During trunk-based development the file is `qa/qa-test-cases-unreleased.yaml`. At release time `finalize-release` renames it to `qa/qa-test-cases-v.yaml`. -The version is the target release (`nextTargetVersion` from `package.json`). It is embedded in the filename and parsed automatically by the `generate-qa-issue` script — no extra flags needed. One file per release — Git tracks history across versions. +The filename mirrors `nextTargetVersion` from `package.json` (`"Unreleased"` during development). It is parsed automatically by the `generate-qa-issue` script — no extra flags needed. One file per release — Git tracks history across versions. New QA YAML files are created by `pnpm generate:qa-test-plan`. The script carries forward all TCs from the most recent YAML, resets `status:` fields to `pending`, and preserves `automated:` flags. diff --git a/packages/rangelink-vscode-extension/package.json b/packages/rangelink-vscode-extension/package.json index ea71882c..d176bb44 100644 --- a/packages/rangelink-vscode-extension/package.json +++ b/packages/rangelink-vscode-extension/package.json @@ -982,6 +982,6 @@ "vscode": "^1.49.0" }, "icon": "icon.png", - "nextTargetVersion": "1.1.0", + "nextTargetVersion": "Unreleased", "pricing": "Free" } diff --git a/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml b/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml index fd560a33..6c979d80 100644 --- a/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml +++ b/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml @@ -1,15 +1,15 @@ -# RangeLink QA Test Cases — v1.0.0 → v1.1.0 +# RangeLink QA Test Cases — v1.0.0 → Unreleased # # Scope: Changes accumulated between the vscode-extension-v1.0.0 release tag and the current -# main branch tip, targeting v1.1.0. +# main branch tip, targeting Unreleased. # -# Source of truth for this QA cycle. Run `pnpm generate:qa-issue -- qa/qa-test-cases-v1.1.0-003.yaml` +# Source of truth for this QA cycle. Run `pnpm generate:qa-issue -- qa/qa-test-cases-unreleased.yaml` # to create the corresponding GitHub issue tracker (parent issue + per-section sub-issues). # # Schema: # id: Unique test case identifier (-NNN) # feature: Feature area -# scenario: One-line description of the specific scenario being tested +# scenario: One-line description of what is tested # labels: Optional tags (e.g., cursor, ubuntu, requires-extensions) # preconditions: Required setup steps — only on `automated: false` entries # steps: Ordered test actions — only on `automated: false` entries @@ -24,10 +24,7 @@ # automated (even assisted): # platform-specific — requires Win/Linux; CI runs on macOS # ide-specific — requires Cursor IDE; not in VS Code host -# -# For `automated: true` and `automated: assisted` entries the integration test in -# src/__integration-tests__/suite/ is the canonical source of setup and actions — -# `preconditions:` and `steps:` are omitted to prevent duplication and drift. + test_cases: # --------------------------------------------------------------------------- From a8f8c2eca68638f19fc97482ede0f217773353c0 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 12:22:39 -0400 Subject: [PATCH 6/8] [fix] Fix blank-line accumulation in generate-qa-test-plan The old sed range expression preserved blank lines between the header and body, which combined with the separator echo to add one blank line per regeneration. Dropping the file-exists early-exit in commit 1 surfaced this latent bug. Replaced with a simpler sed -n range that skips directly from test_cases: to EOF. Also updated a stale usage example in generate-qa-issue.sh. --- .../rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml | 1 - .../rangelink-vscode-extension/scripts/generate-qa-issue.sh | 2 +- .../rangelink-vscode-extension/scripts/generate-qa-test-plan.sh | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml b/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml index 6c979d80..bbf3eda0 100644 --- a/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml +++ b/packages/rangelink-vscode-extension/qa/qa-test-cases-unreleased.yaml @@ -25,7 +25,6 @@ # platform-specific — requires Win/Linux; CI runs on macOS # ide-specific — requires Cursor IDE; not in VS Code host - test_cases: # --------------------------------------------------------------------------- # Section 1 — R-M Status Bar Menu diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh index 492e180e..be85d9f0 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh @@ -2,7 +2,7 @@ set -euo pipefail # Usage: ./scripts/generate-qa-issue.sh [--dry-run] [--local] [yaml-file] -# Example: ./scripts/generate-qa-issue.sh qa/qa-test-cases-v1.1.0.yaml +# Example: ./scripts/generate-qa-issue.sh qa/qa-test-cases-unreleased.yaml # ./scripts/generate-qa-issue.sh --local # # Creates a single GitHub issue with checkboxes grouped by TC ID prefix (feature domain). diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh index 9916e3d3..ee8815da 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh @@ -90,7 +90,7 @@ HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → ${NEXT_LABEL} # platform-specific — requires Win/Linux; CI runs on macOS # ide-specific — requires Cursor IDE; not in VS Code host" -BODY=$(sed '1,/^test_cases:/{ /^#/d; }' "$PREVIOUS_YAML") +BODY=$(sed -n '/^test_cases:/,$p' "$PREVIOUS_YAML") { echo "$HEADER" From 383feb588b4a70d1527e78a3ba7aacbea45c9f9e Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 13:10:27 -0400 Subject: [PATCH 7/8] [PR feedback] Prefer qa-test-cases-unreleased.yaml in file discovery sorts Both generate-qa-test-plan.sh and resolve-qa-labels.js sort candidate files with ASCII ordering where 'unreleased' (u=117) sorts before 'v' (v=118), causing versioned files to win discovery when both coexist. This would clobber in-progress unreleased edits once a versioned file exists (post-finalize). Fixed by short-circuiting: prefer the existing target file in the shell script, and return the unreleased file immediately in the JS auto-discovery. Ref: https://github.com/couimet/rangeLink/pull/605#pullrequestreview-4365656672 --- .../scripts/generate-qa-test-plan.sh | 43 +++++++++++-------- .../scripts/resolve-qa-labels.js | 32 ++++++++------ tests/shell/generate-qa-test-plan.bats | 27 ++++++++++++ tests/shell/resolve-qa-labels.bats | 21 +++++++++ 4 files changed, 93 insertions(+), 30 deletions(-) diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh index ee8815da..7ef4470e 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh @@ -43,24 +43,31 @@ else fi OUTPUT_FILE="$QA_DIR/${BASE_NAME}.yaml" -# Suffix sort fix: unsuffixed files (v1.1.0.yaml) sort AFTER suffixed files -# (v1.1.0-001.yaml) because '.' > '-' in ASCII. Normalize by appending -000 -# to unsuffixed names for sorting purposes, then pick the highest. -PREVIOUS_YAML=$( - for f in "$QA_DIR"/qa-test-cases-*.yaml; do - [[ -e "$f" ]] || continue - name=$(basename "$f") - base="${name%.yaml}" - if [[ "$base" =~ -[0-9]{3}$ ]]; then - printf '%s\t%s\n' "$base" "$f" - else - printf '%s-000\t%s\n' "$base" "$f" - fi - done | sort -t$'\t' -k1,1 | tail -1 | cut -f2 -) -if [[ -z "$PREVIOUS_YAML" ]]; then - echo "Error: no previous QA YAML found in $QA_DIR" >&2 - exit 1 +# Prefer the existing target file as the carry-forward source so in-progress +# unreleased edits aren't clobbered by a versioned file that would win the +# ASCII sort ('u' < 'v'). +if [[ -f "$OUTPUT_FILE" ]]; then + PREVIOUS_YAML="$OUTPUT_FILE" +else + # Suffix sort fix: unsuffixed files (v1.1.0.yaml) sort AFTER suffixed files + # (v1.1.0-001.yaml) because '.' > '-' in ASCII. Normalize by appending -000 + # to unsuffixed names for sorting purposes, then pick the highest. + PREVIOUS_YAML=$( + for f in "$QA_DIR"/qa-test-cases-*.yaml; do + [[ -e "$f" ]] || continue + name=$(basename "$f") + base="${name%.yaml}" + if [[ "$base" =~ -[0-9]{3}$ ]]; then + printf '%s\t%s\n' "$base" "$f" + else + printf '%s-000\t%s\n' "$base" "$f" + fi + done | sort -t$'\t' -k1,1 | tail -1 | cut -f2 + ) + if [[ -z "$PREVIOUS_YAML" ]]; then + echo "Error: no previous QA YAML found in $QA_DIR" >&2 + exit 1 + fi fi HEADER="# RangeLink QA Test Cases — v${PUBLISHED_VERSION} → ${NEXT_LABEL} diff --git a/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js b/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js index db24c676..7a6c4bdc 100755 --- a/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js +++ b/packages/rangelink-vscode-extension/scripts/resolve-qa-labels.js @@ -107,18 +107,26 @@ if (!yamlPath) { process.exit(1); } - files.sort((a, b) => { - const va = a.match(/v(\d+\.\d+\.\d+)/)?.[1] ?? ''; - const vb = b.match(/v(\d+\.\d+\.\d+)/)?.[1] ?? ''; - if (va !== vb) return va.localeCompare(vb, undefined, { numeric: true }); - const suffixA = a.match(/v\d+\.\d+\.\d+-(.+)\.yaml$/)?.[1] ?? ''; - const suffixB = b.match(/v\d+\.\d+\.\d+-(.+)\.yaml$/)?.[1] ?? ''; - if (!suffixA && suffixB) return 1; - if (suffixA && !suffixB) return -1; - return suffixA.localeCompare(suffixB); - }); - - yamlPath = path.join(qaDir, files[files.length - 1]); + // Prefer the current development file when it exists; otherwise fall + // back to the version sort (which doesn't match 'unreleased', causing + // it to lose to any versioned file). + const unreleased = files.find((f) => f === 'qa-test-cases-unreleased.yaml'); + if (unreleased) { + yamlPath = path.join(qaDir, unreleased); + } else { + files.sort((a, b) => { + const va = a.match(/v(\d+\.\d+\.\d+)/)?.[1] ?? ''; + const vb = b.match(/v(\d+\.\d+\.\d+)/)?.[1] ?? ''; + if (va !== vb) return va.localeCompare(vb, undefined, { numeric: true }); + const suffixA = a.match(/v\d+\.\d+\.\d+-(.+)\.yaml$/)?.[1] ?? ''; + const suffixB = b.match(/v\d+\.\d+\.\d+-(.+)\.yaml$/)?.[1] ?? ''; + if (!suffixA && suffixB) return 1; + if (suffixA && !suffixB) return -1; + return suffixA.localeCompare(suffixB); + }); + + yamlPath = path.join(qaDir, files[files.length - 1]); + } } // ── Parse YAML ──────────────────────────────────────────────────────────────── diff --git a/tests/shell/generate-qa-test-plan.bats b/tests/shell/generate-qa-test-plan.bats index c34c0aad..cc29c028 100644 --- a/tests/shell/generate-qa-test-plan.bats +++ b/tests/shell/generate-qa-test-plan.bats @@ -172,6 +172,33 @@ EOF [[ "$checksum_after_first" == "$checksum_after_second" ]] } +@test "re-running prefers existing unreleased.yaml when both unreleased and versioned files exist" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.0.0", + "nextTargetVersion": "Unreleased" +} +EOF + write_yaml "qa-test-cases-v1.0.0.yaml" <<'EOF' +test_cases: + - id: versioned-001 + scenario: 'from versioned' + automated: true +EOF + write_yaml "qa-test-cases-unreleased.yaml" <<'EOF' +test_cases: + - id: unreleased-001 + scenario: 'from unreleased' + automated: true +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + grep -q "scenario: 'from unreleased'" "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" + ! grep -q "scenario: 'from versioned'" "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" +} + # ── Error paths ──────────────────────────────────────────────────────────────── @test "missing nextTargetVersion still errors" { diff --git a/tests/shell/resolve-qa-labels.bats b/tests/shell/resolve-qa-labels.bats index 64bc9d29..ba2cc9dd 100644 --- a/tests/shell/resolve-qa-labels.bats +++ b/tests/shell/resolve-qa-labels.bats @@ -223,6 +223,27 @@ EOF [[ "$output" == "unreleased-001" ]] } +@test "resolve-qa-labels: auto-discovery prefers unreleased.yaml when both unreleased and versioned files exist" { + setup_fixture + write_yaml "qa-test-cases-v1.2.3.yaml" <<'EOF' +test_cases: + - id: versioned-001 + feature: Versioned + scenario: From versioned file + automated: true +EOF + write_yaml "qa-test-cases-unreleased.yaml" <<'EOF' +test_cases: + - id: unreleased-001 + feature: Unreleased + scenario: From unreleased file + automated: true +EOF + run node "$SCRIPT" + [[ "$status" -eq 0 ]] + [[ "$output" == "unreleased-001" ]] +} + # ════════════════════════════════════════════════════════════════════ # YAML parsing # ════════════════════════════════════════════════════════════════════ From 4eede3ad3779fda1c282a6abbb156f6dc20d52a5 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Tue, 26 May 2026 17:53:38 -0400 Subject: [PATCH 8/8] [PR feedback] Fix SemVer sort and qa-suggest SKILL.md conditional templates The fallback YAML selection used lexicographic sort, which ranked v1.9.0 above v1.10.0 because 9 > 1 at offset 3. Replaced with numeric sort by MAJOR, MINOR, PATCH. The qa-suggest SKILL.md now conditionally formats the scratchpad header and filename based on whether nextTargetVersion is "Unreleased" or a locked SemVer. Benefits: - Correct carry-forward source when version components cross the 10 boundary - Scratchpad naming reflects actual version state during locked-version QA cycles - Regression test prevents backsliding on numeric sort Ref: https://github.com/couimet/rangeLink/pull/605#pullrequestreview-4367400230 --- .claude/skills/qa-suggest/SKILL.md | 16 +++++++++-- .../scripts/generate-qa-test-plan.sh | 20 ++++++++------ tests/shell/generate-qa-test-plan.bats | 27 +++++++++++++++++++ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.claude/skills/qa-suggest/SKILL.md b/.claude/skills/qa-suggest/SKILL.md index 2425be1f..8b449649 100644 --- a/.claude/skills/qa-suggest/SKILL.md +++ b/.claude/skills/qa-suggest/SKILL.md @@ -144,7 +144,9 @@ Create a scratchpad file for the report. Use the `/scratchpad` conventions: 1. Determine the issue context from the current git branch (e.g., `issues/382` → issue ID `382`) 2. Find the next available sequence number in `.claude-work/issues//scratchpads/` -3. Write the scratchpad to `.claude-work/issues//scratchpads/NNNN-qa-suggest.txt` +3. Write the scratchpad. Choose the filename based on `nextTargetVersion`: + - If `nextTargetVersion` is `"Unreleased"`: `.claude-work/issues//scratchpads/NNNN-qa-suggest.txt` + - If `nextTargetVersion` is a locked SemVer (e.g., `"2.0.0"`): `.claude-work/issues//scratchpads/NNNN-qa-suggest-v.txt` If no issue context can be determined, use `.claude-work/scratchpads/` instead. @@ -152,15 +154,25 @@ The scratchpad should contain these sections in order: ### Header +If `nextTargetVersion` is `"Unreleased"`, use this header: + ```text # QA Suggest — v → Unreleased +``` + +If `nextTargetVersion` is a locked SemVer (e.g., `"2.0.0"`), use this header instead: + +```text +# QA Suggest — v → v +``` + +Then continue with the shared body: ## What to do next 1. Review the suggested TCs below — edit descriptions, remove irrelevant ones 2. Copy the YAML block at the bottom into the QA file at the appropriate section 3. Verify the IDs don't collide with existing entries -``` ### Change Summary diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh index 7ef4470e..ee670801 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-test-plan.sh @@ -49,20 +49,24 @@ OUTPUT_FILE="$QA_DIR/${BASE_NAME}.yaml" if [[ -f "$OUTPUT_FILE" ]]; then PREVIOUS_YAML="$OUTPUT_FILE" else - # Suffix sort fix: unsuffixed files (v1.1.0.yaml) sort AFTER suffixed files - # (v1.1.0-001.yaml) because '.' > '-' in ASCII. Normalize by appending -000 - # to unsuffixed names for sorting purposes, then pick the highest. + # Pick the highest-SemVer versioned YAML (or unreleased as last resort). + # Sort by MAJOR, MINOR, PATCH numerically so v1.10.0 > v1.9.0. Unsuffixed + # files get -000 for tie-breaking so the primary file sorts before backups. PREVIOUS_YAML=$( for f in "$QA_DIR"/qa-test-cases-*.yaml; do [[ -e "$f" ]] || continue name=$(basename "$f") base="${name%.yaml}" - if [[ "$base" =~ -[0-9]{3}$ ]]; then - printf '%s\t%s\n' "$base" "$f" - else - printf '%s-000\t%s\n' "$base" "$f" + if [[ "$base" =~ ^qa-test-cases-v([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9]{3})?$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + suff="${BASH_REMATCH[4]:-"-000"}" + printf '%d\t%d\t%d\t%s\t%s\n' "$major" "$minor" "$patch" "$suff" "$f" + elif [[ "$base" == *unreleased* ]]; then + printf '0\t0\t0\t-000\t%s\n' "$f" fi - done | sort -t$'\t' -k1,1 | tail -1 | cut -f2 + done | sort -t$'\t' -k1,1n -k2,2n -k3,3n -k4,4 | tail -1 | cut -f5 ) if [[ -z "$PREVIOUS_YAML" ]]; then echo "Error: no previous QA YAML found in $QA_DIR" >&2 diff --git a/tests/shell/generate-qa-test-plan.bats b/tests/shell/generate-qa-test-plan.bats index cc29c028..9e136913 100644 --- a/tests/shell/generate-qa-test-plan.bats +++ b/tests/shell/generate-qa-test-plan.bats @@ -199,6 +199,33 @@ EOF ! grep -q "scenario: 'from versioned'" "$FIXTURE_ROOT/qa/qa-test-cases-unreleased.yaml" } +@test "fallback: numeric SemVer sort picks v1.10.0 over v1.9.0 when output file is new" { + setup_fixture + write_package_json <<'EOF' +{ + "version": "1.10.0", + "nextTargetVersion": "2.0.0" +} +EOF + write_yaml "qa-test-cases-v1.9.0.yaml" <<'EOF' +test_cases: + - id: older-001 + scenario: 'from older version' + automated: true +EOF + write_yaml "qa-test-cases-v1.10.0.yaml" <<'EOF' +test_cases: + - id: newer-001 + scenario: 'from newer version' + automated: true +EOF + + run "$SCRIPT" + [[ "$status" -eq 0 ]] + grep -q "scenario: 'from newer version'" "$FIXTURE_ROOT/qa/qa-test-cases-v2.0.0.yaml" + ! grep -q "scenario: 'from older version'" "$FIXTURE_ROOT/qa/qa-test-cases-v2.0.0.yaml" +} + # ── Error paths ──────────────────────────────────────────────────────────────── @test "missing nextTargetVersion still errors" {