From 5a11b34aad87ad18ee023885a769b9bd6da43efe Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 7 Oct 2026 14:50:41 +0000 Subject: [PATCH] Test release wiring by its effective behavior --- docs/roadmap/remediation.md | 10 +++++- scripts/release_test.go | 68 ++++++++++++------------------------- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index dc7b8ff..dcbf03a 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -4,7 +4,7 @@ `3c405f0524ec6c098d00d4e371abbace7648d446`. **Scope:** ReleaseKit adoption commits after `721365e`, including CLI version reporting, local release wiring, CI delivery, tests, and documentation. -**Status:** R1–R2 addressed; R3 pending. The findings below describe the reviewed +**Status:** R1–R3 addressed. The findings below describe the reviewed baseline; completed work is recorded under Resolution Record. ## Architecture Assessment @@ -141,3 +141,11 @@ matrix. and an unintended target, while accepting equivalent reordered/repeated lists. The cases reproduced the faulty membership behavior before the fix; `go test ./scripts` passed afterward. +- **R3:** Wrapper coverage now checks the executable's actual destination, and + profile coverage checks the effective additional-check list. Equivalent output + paths and an explicit empty list are accepted. Fixture helpers use standard + library operations where appropriate. Disposable mutation checks accepted + `-o bin/releasekit`, `-o=bin/releasekit`, and `"checks": []`, while detecting a + wrong executable destination, changed product, nonempty checks, lost argument + boundaries, and lost tool status. The complete `go test ./...` suite passed + with all three remediations in place. No new release gates were added. diff --git a/scripts/release_test.go b/scripts/release_test.go index 6d4a29d..11aa36c 100644 --- a/scripts/release_test.go +++ b/scripts/release_test.go @@ -7,6 +7,8 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" + "strconv" "strings" "testing" ) @@ -19,10 +21,13 @@ if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then fi output= while (($#)); do - if [[ $1 == -o ]]; then + case $1 in + -o) shift output=${1:?missing output path} - fi + ;; + -o=*) output=${1#-o=} ;; + esac shift done [[ -n $output ]] @@ -36,8 +41,8 @@ chmod +x "$output" ` func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) { - for _, toolStatus := range []string{"0", "3"} { - t.Run("status "+toolStatus, func(t *testing.T) { + for _, toolStatus := range []int{0, 3} { + t.Run("status "+strconv.Itoa(toolStatus), func(t *testing.T) { layout := newReleaseTestLayout(t) args := []string{"publish", "v4.2.0", "argument with spaces"} cmd := exec.Command("bash", append([]string{layout.wrapper}, args...)...) @@ -45,11 +50,11 @@ func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) { cmd.Env = layout.environment(map[string]string{ "RELEASEKIT_ARGS_LOG": layout.argsLog, "RELEASEKIT_BUILD_LOG": layout.buildLog, - "RELEASEKIT_TOOL_STATUS": toolStatus, + "RELEASEKIT_TOOL_STATUS": strconv.Itoa(toolStatus), }) output, err := cmd.CombinedOutput() - if got := commandStatus(err); got != toolStatusInt(toolStatus) { - t.Fatalf("exit=%d, want %s; output=%q, error=%v", got, toolStatus, output, err) + if got := commandStatus(err); got != toolStatus { + t.Fatalf("exit=%d, want %d; output=%q, error=%v", got, toolStatus, output, err) } if len(output) != 0 { t.Fatalf("unexpected output: %q", output) @@ -63,11 +68,11 @@ func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) { t.Fatalf("build environment=%q", buildInvocation[1:3]) } buildArgs := buildInvocation[3:] - if buildArgs[0] != "build" || !containsArgument(buildArgs, "-mod=readonly") || !containsArgument(buildArgs, "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit") { + if buildArgs[0] != "build" || !slices.Contains(buildArgs, "-mod=readonly") || !slices.Contains(buildArgs, "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit") { t.Fatalf("unexpected build arguments: %q", buildArgs) } - if !hasAdjacentArguments(buildArgs, "-o", "./bin/releasekit") { - t.Fatalf("build output was not selected: %q", buildArgs) + if info, err := os.Stat(filepath.Join(layout.repo, "bin", "releasekit")); err != nil || !info.Mode().IsRegular() { + t.Fatalf("ReleaseKit executable was not built in bin: %v", err) } toolInvocation := readNULArguments(t, layout.argsLog) @@ -106,9 +111,6 @@ func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) { if err := json.Unmarshal(data, &raw); err != nil { t.Fatal(err) } - if _, exists := raw["checks"]; exists { - t.Fatal("profile must not define additional checks") - } var deliveryFields map[string]json.RawMessage if err := json.Unmarshal(raw["delivery"], &deliveryFields); err != nil { t.Fatal(err) @@ -117,8 +119,9 @@ func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) { t.Fatal("profile must not contain a credential value") } var profile struct { - Product string `json:"product"` - Remote string `json:"remote"` + Product string `json:"product"` + Remote string `json:"remote"` + Checks []json.RawMessage `json:"checks"` Build struct { MainPackage string `json:"main_package"` BinaryName string `json:"binary_name"` @@ -134,6 +137,9 @@ func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) { if err := json.Unmarshal(data, &profile); err != nil { t.Fatal(err) } + if len(profile.Checks) != 0 { + t.Fatal("profile must not define additional checks") + } if profile.Product != "Audita" || profile.Remote != "origin" { t.Fatalf("product=%q remote=%q", profile.Product, profile.Remote) } @@ -211,7 +217,7 @@ func (layout releaseTestLayout) environment(values map[string]string) []string { filtered := make([]string, 0, len(os.Environ())+len(values)+1) for _, value := range os.Environ() { key, _, _ := strings.Cut(value, "=") - if key == "PATH" || hasKey(values, key) { + if _, overridden := values[key]; key == "PATH" || overridden { continue } filtered = append(filtered, value) @@ -253,31 +259,6 @@ func commandStatus(err error) int { return -1 } -func toolStatusInt(value string) int { - if value == "3" { - return 3 - } - return 0 -} - -func containsArgument(args []string, want string) bool { - for _, arg := range args { - if arg == want { - return true - } - } - return false -} - -func hasAdjacentArguments(args []string, first, second string) bool { - for i := 0; i+1 < len(args); i++ { - if args[i] == first && args[i+1] == second { - return true - } - } - return false -} - func sameMembers(got, want []string) bool { gotSet := make(map[string]struct{}, len(got)) for _, value := range got { @@ -289,8 +270,3 @@ func sameMembers(got, want []string) bool { } return maps.Equal(gotSet, wantSet) } - -func hasKey(values map[string]string, key string) bool { - _, exists := values[key] - return exists -}