release: D2 box tests from the final-review triage
The refusing pass's seed is asserted on disk (ReadPrograms after the pass). An agent-style pass (BakeDir plus an OnStageBlocked that fails the test, install tree unwritable) after root's wrong-program refusal relays nothing and prints the gate line. A -> authorised B -> A adoption runs on an exec route, a carlos-app@ route converged with capabilities, and a named-unit route, with the history built by adoption alone. The helper-only seed test is renamed TestRecordProgramKeepsOneEntryPerProgram.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+168
−3
internal/release/programhistory_test.go+168 −3
CI passed — run details
diff --git a/internal/release/programhistory_test.go b/internal/release/programhistory_test.go| index 812a40f07..41fb1dc2f 100644 |
| --- a/internal/release/programhistory_test.go |
| +++ b/internal/release/programhistory_test.go |
| @@ -3,9 +3,14 @@ package release |
| import ( |
| "bytes" |
| "context" |
| + "io/fs" |
| + "os" |
| + "path/filepath" |
| "slices" |
| "strings" |
| "testing" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| ) |
| // The route is live on "old" (a console build). The channel is promoted to |
| @@ -52,6 +57,11 @@ func TestBoxRefusesWrongProgramWithNoManifestField(t *testing.T) { |
| if r.Kind != RefusalWrongProgram || !r.Recovered || r.RestoredVersion != "old" || r.RecoveryError != "" { |
| t.Fatalf("refusal = %+v, want a recovered wrong-program record still on old", r) |
| } |
| + // The history started empty; the refusing pass seeded it from the live |
| + // binary, and the seed is on disk, not only in that pass's memory. |
| + if got := ReadPrograms(bake, host); !slices.Equal(got, []string{"example.com/console"}) { |
| + t.Fatalf("history after the refusing pass = %v, want the live binary's program persisted", got) |
| + } |
| // The next pass reads the record at the gate: no --retry (it would loop), |
| // and the way out is named: a NEW version, since this one's manifest is |
| // immutable. |
| @@ -212,13 +222,168 @@ func TestBoxDefersAWrongProgramWhoseManifestItCannotRead(t *testing.T) { |
| } |
| } |
| -func TestBoxSeedsHistoryFromTheLiveBinary(t *testing.T) { |
| +// After root's pass refuses a wrong program, the agent's own passes (BakeDir |
| +// and OnStageBlocked, the shape internal/host's loops run) must not relay the |
| +// version to root again: a relay would run a root pass that refuses the same |
| +// bytes, every tick. The install directory is made unwritable for the agent |
| +// pass, as carlos-edge.service's sandbox makes it, so any attempt to stage or |
| +// repoint would be a write refusal that relays. |
| +func TestAgentPassDoesNotRelayAfterRootsWrongProgramRefusal(t *testing.T) { |
| + host := "console.example.com" |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt, bake := t.TempDir(), t.TempDir() |
| + reg := testRegistry(t) |
| + good, bad := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos") |
| + shipRelease(t, s, "bab", "console", "old", adoptPlatform, good) |
| + shipRelease(t, s, "bab", "console", "new", adoptPlatform, bad) // old client: no program |
| + addRoute(t, reg, host, "console", "canary/x", false) |
| + restart := func(h string) error { t.Fatalf("restart consulted for a wrong-program build on %s", h); return nil } |
| + if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, BakeDir(bake)); err != nil { |
| + t.Fatalf("root Pass old: %v", err) |
| + } |
| + if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "new"); err != nil { |
| + t.Fatal(err) |
| + } |
| + // Root's pass: stages new and refuses it. |
| + if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake)); err != nil { |
| + t.Fatalf("root Pass new: %v", err) |
| + } |
| + if r, _ := ReadRefusal(bake, host); r == nil || r.Kind != RefusalWrongProgram { |
| + t.Fatalf("refusal = %+v, want root's wrong-program record", r) |
| + } |
| + |
| + readOnlyTree(t, opt) |
| + relay := func(h, ver string, err error) { t.Fatalf("agent pass relayed %s %s to root: %v", h, ver, err) } |
| + var out bytes.Buffer |
| + withCapturedStdout(t, &out, func() { |
| + for i := 0; i < 2; i++ { |
| + if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake), OnStageBlocked(relay)); err != nil { |
| + t.Fatalf("agent Pass %d: %v", i, err) |
| + } |
| + } |
| + }) |
| + if v, _ := LiveVersion(opt, host); v != "old" { |
| + t.Fatalf("live = %q, want old", v) |
| + } |
| + if !strings.Contains(out.String(), "is the wrong program for this route") { |
| + t.Fatalf("agent pass printed %q, want the gate line", out.String()) |
| + } |
| +} |
| + |
| +// readOnlyTree makes every directory under root unwritable until the test |
| +// ends (then restores it so the temp dir can be removed). |
| +func readOnlyTree(t *testing.T, root string) { |
| + t.Helper() |
| + var dirs []string |
| + if err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { |
| + if err == nil && d.IsDir() { |
| + dirs = append(dirs, p) |
| + } |
| + return err |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, d := range dirs { |
| + if err := os.Chmod(d, 0o555); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + t.Cleanup(func() { |
| + for _, d := range dirs { |
| + os.Chmod(d, 0o755) |
| + } |
| + }) |
| + if f, err := os.CreateTemp(root, "probe"); err == nil { |
| + f.Close() |
| + os.Remove(f.Name()) |
| + t.Skip("directories stay writable (running as root?)") |
| + } |
| +} |
| + |
| +// Spec §7: a deliberate A -> B change and the rollback to A, on each route |
| +// kind that runs a release: an exec child, a carlos-app@ unit (converged with |
| +// capabilities, as root's hourly pass does) and a named unit. The history is |
| +// built by adoption alone. |
| +func TestBoxRollsBackAcrossADeliberateChangeOnEveryRouteKind(t *testing.T) { |
| + for _, tc := range []struct { |
| + name string |
| + rt registry.Route |
| + caps bool |
| + }{ |
| + {"exec", registry.Route{}, false}, |
| + {"carlos-app unit", registry.Route{Backing: registry.BackingUnit}, true}, |
| + {"named unit", registry.Route{Unit: "console.service"}, false}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + host := "console.example.com" |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt, bake := t.TempDir(), t.TempDir() |
| + reg := testRegistry(t) |
| + a, b := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos") |
| + if err := shipWithProgram(t, s, "va", a, false); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := shipWithProgram(t, s, "vb", b, true); err != nil { |
| + t.Fatal(err) |
| + } |
| + rt := tc.rt |
| + rt.Host, rt.Kind, rt.App, rt.Channel = host, "instance", "console", "canary/x" |
| + if err := reg.Upsert(rt); err != nil { |
| + t.Fatal(err) |
| + } |
| + var restarted []string |
| + opts := []PassOption{BakeDir(bake)} |
| + if tc.caps { |
| + o, _ := capsTestOps(t, newCapsRecorder(opt, host)) |
| + opts = append(opts, WithCapabilities(o)) |
| + } |
| + restart := func(h string) error { restarted = append(restarted, h); return nil } |
| + for _, step := range []struct { |
| + ver string |
| + hist []string |
| + }{ |
| + {"va", []string{"example.com/console"}}, |
| + {"vb", []string{"example.com/console", "example.com/carlos"}}, |
| + {"va", []string{"example.com/console", "example.com/carlos"}}, |
| + } { |
| + if err := Promote(ctx, s, priv, "bab", "console", "canary/x", step.ver); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := Pass(ctx, reg, s, pub, opt, restart, opts...); err != nil { |
| + t.Fatalf("Pass %s: %v", step.ver, err) |
| + } |
| + if v, _ := LiveVersion(opt, host); v != step.ver { |
| + t.Fatalf("live = %q, want %s", v, step.ver) |
| + } |
| + if got := ReadPrograms(bake, host); !slices.Equal(got, step.hist) { |
| + t.Fatalf("history after %s = %v, want %v", step.ver, got, step.hist) |
| + } |
| + if r, _ := ReadRefusal(bake, host); r != nil { |
| + t.Fatalf("refusal after %s = %+v, want none", step.ver, r) |
| + } |
| + } |
| + if len(restarted) < 2 { |
| + t.Fatalf("restarted = %v, want the route cycled on each change", restarted) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// RecordProgram keeps one entry per program. |
| +func TestRecordProgramKeepsOneEntryPerProgram(t *testing.T) { |
| bake := t.TempDir() |
| if got := ReadPrograms(bake, "console.example.com"); len(got) != 0 { |
| t.Fatalf("fresh history = %v", got) |
| } |
| - // The refusal test above starts with no history: passing it proves the |
| - // seed. Pin the file shape directly too. |
| + // The seed itself is asserted after a real refusing pass in |
| + // TestBoxRefusesWrongProgramWithNoManifestField. Pin the file shape here. |
| if err := RecordProgram(bake, "console.example.com", "example.com/console"); err != nil { |
| t.Fatal(err) |
| } |