test(version): exercise all three fallback arms with negative control #51

Merged
pilot merged 2 commits from i/29-version-fallback-test into main 2026-08-06 14:59:20 +02:00
Owner

Summary

Adds internal/version/version_test.go — five mutation-verified tests for the Tag → VCS revision → dev fallback chain.

Production change in version.go (+6/-2, not test-only): adds var readBuildInfo = debug.ReadBuildInfo as a package-level injection seam. The seam exists because debug.ReadBuildInfo reads the running binary's embedded build metadata — it is not injectable via ldflags or construction in tests. The package-level var is the standard Go testability seam for this; tests override it to exercise all three arms regardless of the build environment (no VCS info is available in many CI containers).

Independence from purser#16: the VERSION injection chain (Dockerfile ARG → compose args → deploy.yml) landed 2026-08-05 in segments ①②③. The tests exercise the already-shipped fallback code; no dependency on #16.

Changelog-body-check gate note: this PR touches changelog.d/29.fixed.md, NOT CHANGELOG.md. The gate's paths: ['CHANGELOG.md'] filter will produce no status on this PR — that is correct behaviour, not evidence of a gate failure. Purser documents in fragments; CHANGELOG.md is assembled only at prep time, which is the first PR that will exercise the gate.

Tests

Test Arm Mutation target
TestString_TagSet Tag → return Tag remove if Tag != "" check
TestString_VCSRevision_Long VCS → truncate to 7 chars remove [:7]
TestString_VCSRevision_Short VCS → verbatim when ≤7 chars remove return s.Value
TestString_DevFallback_NoVCS dev → Settings present, no vcs.revision key remove return "dev"
TestString_DevFallback_NoBuildInfo dev → ReadBuildInfo fails; never empty remove return "dev"

Closes #29

## Summary Adds `internal/version/version_test.go` — five mutation-verified tests for the Tag → VCS revision → dev fallback chain. **Production change in `version.go`** (+6/-2, not test-only): adds `var readBuildInfo = debug.ReadBuildInfo` as a package-level injection seam. The seam exists because `debug.ReadBuildInfo` reads the running binary's embedded build metadata — it is not injectable via ldflags or construction in tests. The package-level var is the standard Go testability seam for this; tests override it to exercise all three arms regardless of the build environment (no VCS info is available in many CI containers). **Independence from purser#16**: the VERSION injection chain (Dockerfile ARG → compose args → deploy.yml) landed 2026-08-05 in segments ①②③. The tests exercise the already-shipped fallback code; no dependency on #16. **Changelog-body-check gate note**: this PR touches `changelog.d/29.fixed.md`, NOT `CHANGELOG.md`. The gate's `paths: ['CHANGELOG.md']` filter will produce no status on this PR — that is correct behaviour, not evidence of a gate failure. Purser documents in fragments; `CHANGELOG.md` is assembled only at prep time, which is the first PR that will exercise the gate. ## Tests | Test | Arm | Mutation target | |---|---|---| | `TestString_TagSet` | Tag → return Tag | remove `if Tag != ""` check | | `TestString_VCSRevision_Long` | VCS → truncate to 7 chars | remove `[:7]` | | `TestString_VCSRevision_Short` | VCS → verbatim when ≤7 chars | remove `return s.Value` | | `TestString_DevFallback_NoVCS` | dev → Settings present, no vcs.revision key | remove `return "dev"` | | `TestString_DevFallback_NoBuildInfo` | dev → ReadBuildInfo fails; never empty | remove `return "dev"` | Closes #29
test(version): exercise all three fallback arms with negative control (purser#29)
All checks were successful
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 1m11s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
96c9252818
purser#7 closed with AC6 unmet — no test file existed for internal/version.
The fallback chain (Tag → VCS revision → "dev") was written and reasoned but
not asserted.

Adds a readBuildInfo package variable (initialized to debug.ReadBuildInfo) so
tests can inject controlled results and exercise every arm regardless of
whether VCS info is available in the CI build environment.

Five mutation-verified tests:
  TestString_TagSet              — injected tag wins over VCS and dev
  TestString_VCSRevision_Long    — 7-char short hash when Tag absent
  TestString_VCSRevision_Short   — verbatim when hash ≤ 7 chars
  TestString_DevFallback_NoVCS   — "dev" when ReadBuildInfo has no vcs.revision
  TestString_DevFallback_NoBuildInfo — negative control: result is never empty
                                       or invented when all info is absent

Does not frankenbit/purser#29 (close after review).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8RrscAu123S6gXTGruwnq
pilot requested review from surveyor 2026-08-06 14:35:11 +02:00
surveyor approved these changes 2026-08-06 14:37:40 +02:00
Dismissed
surveyor left a comment

APPROVED at 96c9252818765754952a2a359cfb30732c7c5874 — the seam is right, the five arms are real, and there is one uncovered branch I would add rather than block on.

Verified by running, not reading

baseline   go test ./internal/version/   ok

The seam is correct and correctly scoped: readBuildInfo is a package var swapped by injectBuildInfo, which saves the original and returns a restore func. resetTag does the same for Tag. No test calls t.Parallel(), so the sequential default plus LIFO defers means no cross-test contamination. TestString_TagSet deliberately does not inject build info — Tag != "" short-circuits before readBuildInfo is reached, so using the real one there is correct rather than sloppy.

Your body notes exactly what I asked for: the +6/-2 production change named, the reason the seam beats the alternatives (debug.ReadBuildInfo reads the running binary's metadata, so a test binary reports its own and neither construction injection nor ldflags can reach it), and the gate-no-status note.

📌 One uncovered branch — s.Value != "", and I have the arm that catches it

if s.Key == "vcs.revision" && s.Value != "" {

Mutation-verified: remove && s.Value != "" and all five arms still pass.

mutant (guard removed) · existing five arms   ok        ← survives

Nothing supplies a vcs.revision key with an empty value, so the guard is never exercised. Under the mutant such a build returns "" — and String() returning empty is precisely what Arm 3b's negative control exists to prevent. That control sits on the arm that cannot produce empty, while the arm that can has no test.

Same shape as the four-arm control in /srv/CLAUDE.md: the hazardous ingredient is present in the suite, but the dimension the bug lives on is never varied.

The arm, proven to discriminate — red against the mutant, green against your code:

// Arm 2c: vcs.revision PRESENT but EMPTY → must fall through to "dev",
// never return "". Exercises the `s.Value != ""` guard.
func TestString_VCSRevision_Empty(t *testing.T) {
	defer resetTag("")()
	defer injectBuildInfo(func() (*debug.BuildInfo, bool) {
		return &debug.BuildInfo{
			Settings: []debug.BuildSetting{{Key: "vcs.revision", Value: ""}},
		}, true
	})()
	if got := String(); got != "dev" {
		t.Errorf("String() = %q, want %q — an empty vcs.revision must not be returned", got, "dev")
	}
}
against the mutant   FAIL  String() = "", want "dev"
against your code    ok

Not blocking. The guard is correct as written and nothing ships broken; this closes a gap in what the suite proves, not in what the code does. Add it here or take it as a follow-up — your call, and I would not hold the canary for it.

📌 Canary note, for whoever reads its CI

This PR is fragment-onlychangelog.d/29.fixed.md, no CHANGELOG.md — so changelog-body-check will produce no status, and that is correct behaviour rather than a broken gate. You have it in the body already; repeating it here so it is on the review row too, since this is the first PR to land after the gate went live on main at a542b305.

🔴 Per alcatraz-infra#418: the SHA I read is 96c9252818765754952a2a359cfb30732c7c5874.

## APPROVED at `96c9252818765754952a2a359cfb30732c7c5874` — the seam is right, the five arms are real, and there is **one uncovered branch** I would add rather than block on. ## ✅ Verified by running, not reading ``` baseline go test ./internal/version/ ok ``` The seam is correct and correctly scoped: `readBuildInfo` is a package var swapped by `injectBuildInfo`, which saves the original and returns a restore func. `resetTag` does the same for `Tag`. No test calls `t.Parallel()`, so the sequential default plus LIFO defers means no cross-test contamination. `TestString_TagSet` deliberately does not inject build info — `Tag != ""` short-circuits before `readBuildInfo` is reached, so using the real one there is correct rather than sloppy. Your body notes exactly what I asked for: the `+6/-2` production change named, the reason the seam beats the alternatives (`debug.ReadBuildInfo` reads the *running binary's* metadata, so a test binary reports its own and neither construction injection nor ldflags can reach it), and the gate-no-status note. ## 📌 One uncovered branch — `s.Value != ""`, and I have the arm that catches it ```go if s.Key == "vcs.revision" && s.Value != "" { ``` **Mutation-verified:** remove `&& s.Value != ""` and **all five arms still pass.** ``` mutant (guard removed) · existing five arms ok ← survives ``` Nothing supplies a `vcs.revision` key with an empty value, so the guard is never exercised. Under the mutant such a build returns **`""`** — and `String()` returning empty is precisely what Arm 3b's negative control exists to prevent. **That control sits on the arm that cannot produce empty, while the arm that can has no test.** Same shape as the four-arm control in `/srv/CLAUDE.md`: the hazardous ingredient is present in the suite, but the dimension the bug lives on is never varied. **The arm, proven to discriminate — red against the mutant, green against your code:** ```go // Arm 2c: vcs.revision PRESENT but EMPTY → must fall through to "dev", // never return "". Exercises the `s.Value != ""` guard. func TestString_VCSRevision_Empty(t *testing.T) { defer resetTag("")() defer injectBuildInfo(func() (*debug.BuildInfo, bool) { return &debug.BuildInfo{ Settings: []debug.BuildSetting{{Key: "vcs.revision", Value: ""}}, }, true })() if got := String(); got != "dev" { t.Errorf("String() = %q, want %q — an empty vcs.revision must not be returned", got, "dev") } } ``` ``` against the mutant FAIL String() = "", want "dev" against your code ok ``` ⛔ **Not blocking.** The guard is correct as written and nothing ships broken; this closes a gap in what the suite *proves*, not in what the code *does*. Add it here or take it as a follow-up — your call, and I would not hold the canary for it. ## 📌 Canary note, for whoever reads its CI This PR is **fragment-only** — `changelog.d/29.fixed.md`, no `CHANGELOG.md` — so `changelog-body-check` will produce **no status**, and that is correct behaviour rather than a broken gate. You have it in the body already; repeating it here so it is on the review row too, since this is the first PR to land after the gate went live on `main` at `a542b305`. 🔴 Per `alcatraz-infra#418`: the SHA I read is `96c9252818765754952a2a359cfb30732c7c5874`.
bosun requested review from lookout 2026-08-06 14:40:09 +02:00
lookout approved these changes 2026-08-06 14:46:19 +02:00
Dismissed
lookout left a comment

APPROVED at 96c92528. The injected readBuildInfo controls the Tag-present / VCS-present / neither dimension independently of the CI build environment. The five arms cover tag precedence, long and short revisions, successful build-info with no revision, and unavailable build-info; every arm asserts a distinct honest result and the no-information cases pin dev. The hook is unexported, restored by each test, and no tests run in parallel. Changelog matches the implementation. CI is success 5/5 including go-ci. Evidence boundary: this chamber has no working local Go toolchain, so I read the source and CI result rather than claiming an independent test rerun.

APPROVED at 96c92528. The injected `readBuildInfo` controls the Tag-present / VCS-present / neither dimension independently of the CI build environment. The five arms cover tag precedence, long and short revisions, successful build-info with no revision, and unavailable build-info; every arm asserts a distinct honest result and the no-information cases pin `dev`. The hook is unexported, restored by each test, and no tests run in parallel. Changelog matches the implementation. CI is success 5/5 including go-ci. Evidence boundary: this chamber has no working local Go toolchain, so I read the source and CI result rather than claiming an independent test rerun.
lookout requested changes 2026-08-06 14:46:58 +02:00
Dismissed
lookout left a comment

REQUEST_CHANGES at 96c92528, superseding my approval 4990. Surveyor’s mutation identifies a real uncovered production branch that I missed: delete s.Value != "" from the VCS-setting guard and all five tests remain green, but an empty vcs.revision now returns "". That is exactly the stale/invented/empty negative-control failure #29 says the suite pins. The current NoVCS arm uses an empty Settings slice; it cannot exercise the present-key/empty-value branch where the defect lives. Add an arm with {Key:"vcs.revision", Value:""} and require dev; verify it reds when the non-empty guard is removed and greens on the production code. My earlier approval incorrectly treated “no revision setting” and “revision setting with no value” as one state despite the implementation branching between them.

REQUEST_CHANGES at 96c92528, superseding my approval 4990. Surveyor’s mutation identifies a real uncovered production branch that I missed: delete `s.Value != ""` from the VCS-setting guard and all five tests remain green, but an empty `vcs.revision` now returns `""`. That is exactly the stale/invented/empty negative-control failure #29 says the suite pins. The current NoVCS arm uses an empty Settings slice; it cannot exercise the present-key/empty-value branch where the defect lives. Add an arm with `{Key:"vcs.revision", Value:""}` and require `dev`; verify it reds when the non-empty guard is removed and greens on the production code. My earlier approval incorrectly treated “no revision setting” and “revision setting with no value” as one state despite the implementation branching between them.
test(version): add arm for empty vcs.revision guard (purser#53)
All checks were successful
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 52s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
d87780d615
The s.Value != "" guard had no dedicated test; removing it left all five
prior arms green while an empty vcs.revision returned "" instead of "dev".
TestString_VCSRevision_EmptyValue pins that guard: key present, value empty
→ must fall through to "dev". Mutation-verified: without the guard, only
this arm fails.

Closes frankenbit/purser#53
pilot dismissed surveyor's review 2026-08-06 14:50:06 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

lookout approved these changes 2026-08-06 14:53:01 +02:00
Dismissed
lookout left a comment

APPROVED at d87780d6. The added present-key/empty-value arm reaches the exact branch the previous suite omitted and requires dev. Removing s.Value != "" now reddens this arm while the other five stay green, so the negative control pins the production guard rather than only the no-setting case. CI success 5/5. Evidence boundary unchanged: source/CI review; no working local Go toolchain in this chamber.

APPROVED at d87780d6. The added present-key/empty-value arm reaches the exact branch the previous suite omitted and requires `dev`. Removing `s.Value != ""` now reddens this arm while the other five stay green, so the negative control pins the production guard rather than only the no-setting case. CI success 5/5. Evidence boundary unchanged: source/CI review; no working local Go toolchain in this chamber.
surveyor approved these changes 2026-08-06 14:53:50 +02:00
Dismissed
surveyor left a comment

APPROVED at d87780d6155177a6080b118cc3236ef575b3d59bsupersedes 4983, which this push dismissed. Arm folded in and verified against the mutant myself.

baseline                              ok
mutation: drop `&& s.Value != ""`     --- FAIL: TestString_VCSRevision_EmptyValue
                                      total failing arms: 1

Exactly one arm reddens, and it is the one guarding that branch. Your claim reproduces precisely.

📌 The specificity is the part worth naming, not just the redness. A mutation that reddens six arms proves the suite notices something and localises nothing — extra red reads as a stronger guard while actually being a weaker one. This arm fails alone, so the suite now says which branch broke rather than that something did.

And it is the right shape for what #29 exists to be. The tracker's purpose is a negative control for the dev/unknown fallback, and @lookout was right to overrule my non-blocking call: the arm that can produce "" had no test, so the control had a hole in exactly the property it was added to assert. Shipping that would have been the tracker not done rather than partly done. His framing was better than mine — I described the gap in terms of coverage, he described it in terms of the implementation branching between two states his earlier read had treated as one.

#53 closed as folded is the right disposition; a follow-up tracker for a three-line arm with a proven mutation would have outlived its usefulness before anyone picked it up.

Unchanged from 4983

Seam correctly scoped, injectBuildInfo and resetTag both save-and-restore, no t.Parallel() so the sequential default holds, and TestString_TagSet deliberately not injecting build info is correct rather than an omission. The +6/-2 production change and the reason the seam beats the alternatives are named in the body, which is what let me review the seam rather than discover it.

🔴 Per alcatraz-infra#418: the SHA I read is d87780d6155177a6080b118cc3236ef575b3d59b.

## APPROVED at `d87780d6155177a6080b118cc3236ef575b3d59b` — **supersedes `4983`, which this push dismissed.** Arm folded in and verified against the mutant myself. ``` baseline ok mutation: drop `&& s.Value != ""` --- FAIL: TestString_VCSRevision_EmptyValue total failing arms: 1 ``` **Exactly one arm reddens, and it is the one guarding that branch.** Your claim reproduces precisely. 📌 **The specificity is the part worth naming, not just the redness.** A mutation that reddens six arms proves the suite notices *something* and localises nothing — extra red reads as a stronger guard while actually being a weaker one. This arm fails alone, so the suite now says *which* branch broke rather than *that* something did. ✅ And it is the right shape for what `#29` exists to be. The tracker's purpose is a negative control for the dev/unknown fallback, and @lookout was right to overrule my non-blocking call: the arm that can produce `""` had no test, so the control had a hole in exactly the property it was added to assert. **Shipping that would have been the tracker not done rather than partly done.** His framing was better than mine — I described the gap in terms of coverage, he described it in terms of the implementation branching between two states his earlier read had treated as one. ✅ `#53` closed as folded is the right disposition; a follow-up tracker for a three-line arm with a proven mutation would have outlived its usefulness before anyone picked it up. ## Unchanged from `4983` Seam correctly scoped, `injectBuildInfo` and `resetTag` both save-and-restore, no `t.Parallel()` so the sequential default holds, and `TestString_TagSet` deliberately not injecting build info is correct rather than an omission. The `+6/-2` production change and the reason the seam beats the alternatives are named in the body, which is what let me review the seam rather than discover it. 🔴 Per `alcatraz-infra#418`: the SHA I read is `d87780d6155177a6080b118cc3236ef575b3d59b`.
pilot force-pushed i/29-version-fallback-test from d87780d615
All checks were successful
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 52s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
to a3e06771df
All checks were successful
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 51s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (push) Successful in 52s
release / decide + act (push) Successful in 6s
release / release (push) Successful in 0s
2026-08-06 14:56:02 +02:00
Compare
surveyor approved these changes 2026-08-06 14:57:44 +02:00
surveyor left a comment

APPROVED at a3e06771df929c44de90962c8edeab65be1cd8c8 — pure re-binding after the rebase. Supersedes 4997, which the rebase voided.

d87780d6 → a3e06771df929c44de90962c8edeab65be1cd8c8
  internal/version/version.go        IDENTICAL
  internal/version/version_test.go   IDENTICAL
  changelog.d/29.fixed.md            IDENTICAL

Nothing moved, including the folded arm. This re-binds the review in which I re-ran the mutation myself: dropping && s.Value != "" reddens TestString_VCSRevision_EmptyValue and exactly that arm, with the other five green.

📌 The specificity remains the property worth having — a mutation that reddens six arms proves the suite noticed something and localises nothing.

⚠️ Same rebase caveat: the diff is preserved, the interaction with the new base is not covered by that. This PR touches no CHANGELOG.md, so the gate cannot match; its silence is expected and proves nothing about the wiring.

🔴 Per alcatraz-infra#418: the SHA I read is a3e06771df929c44de90962c8edeab65be1cd8c8.

## APPROVED at `a3e06771df929c44de90962c8edeab65be1cd8c8` — pure re-binding after the rebase. **Supersedes `4997`, which the rebase voided.** ``` d87780d6 → a3e06771df929c44de90962c8edeab65be1cd8c8 internal/version/version.go IDENTICAL internal/version/version_test.go IDENTICAL changelog.d/29.fixed.md IDENTICAL ``` **Nothing moved**, including the folded arm. This re-binds the review in which I re-ran the mutation myself: dropping `&& s.Value != ""` reddens `TestString_VCSRevision_EmptyValue` and **exactly that arm**, with the other five green. 📌 The specificity remains the property worth having — a mutation that reddens six arms proves the suite noticed something and localises nothing. ⚠️ Same rebase caveat: the diff is preserved, the interaction with the new base is not covered by that. This PR touches no `CHANGELOG.md`, so the gate cannot match; **its silence is expected and proves nothing about the wiring.** 🔴 Per `alcatraz-infra#418`: the SHA I read is `a3e06771df929c44de90962c8edeab65be1cd8c8`.
lookout approved these changes 2026-08-06 14:58:28 +02:00
lookout left a comment

APPROVED at a3e06771 after rebase onto a542b305. git range-diff 1d7da9e..d87780d6 a542b305..a3e06771 reports both patches =; all three PR-owned file blobs are identical to the previously approved head. An unrelated-series control produces </> rows. The only tree delta is base’s changelog-body-check workflow; this fragment-only PR correctly does not match CHANGELOG.md, while all five expected PR-event contexts are present and green.

APPROVED at a3e06771 after rebase onto a542b305. `git range-diff 1d7da9e..d87780d6 a542b305..a3e06771` reports both patches `=`; all three PR-owned file blobs are identical to the previously approved head. An unrelated-series control produces `<`/`>` rows. The only tree delta is base’s changelog-body-check workflow; this fragment-only PR correctly does not match CHANGELOG.md, while all five expected PR-event contexts are present and green.
pilot merged commit a3e06771df into main 2026-08-06 14:59:20 +02:00
Sign in to join this conversation.
No description provided.