package host import ( "encoding/base64" "encoding/json" "errors" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostfloor" ) func sharedGuardFixture(t *testing.T) (*applyFixture, string) { t.Helper() f, id := guardRecoveryFixture(t) dir := systemGuardDir(f.a.optDir) if err := os.Rename(f.a.floor.Dir, dir); err != nil { t.Fatal(err) } f.a.floor.Dir = dir f.a.regionHA = true return f, id } func TestSystemGuardRegionHAPreservesReservationThroughRecovery(t *testing.T) { f, id := sharedGuardFixture(t) a := f.a if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { t.Fatal(err) } before, err := os.ReadFile(updateBegunPath(a.optDir)) if err != nil { t.Fatal(err) } if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err != nil { t.Fatal(err) } a.settleUpdateBegun(t.Context()) after, err := os.ReadFile(updateBegunPath(a.optDir)) if err != nil || string(after) != string(before) { t.Fatalf("region HA changed the floor reservation: %s, %v", after, err) } if err := a.journal(systemJournal{Phase: systemPhaseBackingUp, Target: "new-build", PrevBuild: "old-build", StartedAt: f.now}); err != nil { t.Fatal(err) } if err := a.Run(t.Context(), true); err != nil { t.Fatal(err) } completed, err := os.ReadFile(filepath.Join(a.floor.Dir, "completed.json")) if err != nil || string(completed) != id { t.Fatalf("completion = %q, %v", completed, err) } if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) { t.Fatalf("marker remains after recovery: %v", err) } } func TestSystemGuardRegionHAKeepsFailedCompletionEvidence(t *testing.T) { for _, failure := range []string{"completion", "controller", "marker"} { t.Run(failure, func(t *testing.T) { f, id := sharedGuardFixture(t) a := f.a if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil { t.Fatal(err) } if err := os.MkdirAll(a.stagingDir(), 0755); err != nil { t.Fatal(err) } evidence := filepath.Join(a.stagingDir(), "evidence") if err := os.WriteFile(evidence, []byte("retain"), 0600); err != nil { t.Fatal(err) } switch failure { case "completion": if err := os.Mkdir(filepath.Join(a.floor.Dir, "completed.json"), 0755); err != nil { t.Fatal(err) } case "controller": if err := os.WriteFile(filepath.Join(a.dataDir, hostexecDir, "pending.json"), []byte(`{"state":"pending"}`), 0600); err != nil { t.Fatal(err) } if err := a.beginExecutableChange(t.Context(), "new-build", f.now); !errors.Is(err, errSystemDeferred) { t.Fatalf("late participation = %v", err) } case "marker": if err := os.WriteFile(updateBegunPath(a.optDir), []byte(`{"target":"new-build"}`), 0600); err != nil { t.Fatal(err) } if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err == nil { t.Fatal("region marker accepted for an armed transaction") } } a.settleUpdateBegun(t.Context()) if err := a.Run(t.Context(), true); err == nil { t.Fatal("unsafe recovery succeeded") } for _, path := range []string{updateBegunPath(a.optDir), evidence} { if _, err := os.Stat(path); err != nil { t.Fatalf("recovery discarded %s: %v", path, err) } } }) } } func TestSystemGuardUnpublishedKeepsRegionHAParticipation(t *testing.T) { f, _ := applyHappy(t) a := f.a a.regionHA = true a.floor = &hostfloor.Disk{Dir: systemGuardDir(a.optDir), Bucket: "bucket", PublicKey: f.pub, Participation: a.participation} if err := os.MkdirAll(a.floor.Dir, 0755); err != nil { t.Fatal(err) } raw, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "unpublished"}) if err := hostfloor.Durable(filepath.Join(a.floor.Dir, "policy.json"), raw, 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(hostexec(t, f), "pending.json"), []byte(`{}`), 0600); err != nil { t.Fatal(err) } if err := a.beginExecutableChange(t.Context(), "v2", f.now); !errors.Is(err, errSystemDeferred) { t.Fatalf("unpublished floor bypassed participation: %v", err) } if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) { t.Fatalf("refused unarmed update retained marker: %v", err) } if err := os.Remove(filepath.Join(a.dataDir, hostexecDir, "pending.json")); err != nil { t.Fatal(err) } if err := a.beginExecutableChange(t.Context(), "v2", f.now); err != nil { t.Fatal(err) } raw, err := os.ReadFile(updateBegunPath(a.optDir)) var begun updateBegun if err != nil || json.Unmarshal(raw, &begun) != nil || begun.Target != "v2" { t.Fatalf("unpublished marker = %s, %v", raw, err) } a.settleUpdateBegun(t.Context()) if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) { t.Fatalf("completed unarmed update retained marker: %v", err) } if err := os.MkdirAll(a.stagingDir(), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(a.file("journal.json"), []byte("{"), 0600); err != nil { t.Fatal(err) } if err := a.Run(t.Context(), true); err == nil { t.Fatal("malformed journal recovered") } if _, err := os.Stat(a.stagingDir()); err != nil { t.Fatalf("unresolved root recovery removed staging: %v", err) } if _, err := os.Stat(a.file("journal.json")); err != nil { t.Fatalf("malformed journal discarded: %v", err) } } func TestSystemGuardHookRecognizesOnlyLegacyRegionState(t *testing.T) { for _, state := range []string{"absent", "empty", "region-marker", "floor-marker", "malformed-marker", "tmp-only", "marker+tmp", "floor-marker+tmp", "checker", "identities", "activation", "completed", "policy", "symlink"} { t.Run(state, func(t *testing.T) { f := newApplyFixture(t, "stable", "v1") t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(f.pub)) t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "bucket") dir := systemGuardDir(f.a.optDir) if state == "symlink" { if err := os.Symlink(t.TempDir(), dir); err != nil { t.Skip(err) } } else if state != "absent" { if err := os.MkdirAll(dir, 0755); err != nil { t.Fatal(err) } } name, body := "", "" switch state { case "region-marker": name, body = updateBegunFile, `{"target":"v2"}` case "floor-marker": name, body = updateBegunFile, `{"id":"activation"}` case "malformed-marker": name, body = updateBegunFile, `{` case "tmp-only": name, body = updateBegunFile+".tmp", `{"tar` case "marker+tmp", "floor-marker+tmp": name, body = updateBegunFile, `{"target":"v2"}` if state == "floor-marker+tmp" { body = `{"id":"activation"}` } if err := os.WriteFile(filepath.Join(dir, updateBegunFile+".tmp"), []byte(`{"tar`), 0644); err != nil { t.Fatal(err) } case "checker": name = "carlos" case "identities": if err := os.Mkdir(filepath.Join(dir, "identities"), 0755); err != nil { t.Fatal(err) } case "activation", "completed", "policy": name = state + ".json" } if name != "" { if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0644); err != nil { t.Fatal(err) } } err := installFloorHook(f.a) // A crash inside main's durable marker write leaves the .tmp or a // torn marker; neither is floor state (Opus merge review M1-1). legacy := state == "absent" || state == "empty" || state == "region-marker" || state == "malformed-marker" || state == "tmp-only" || state == "marker+tmp" if legacy { if err != nil || f.a.guarded() { t.Fatalf("legacy state armed floor: %v", err) } if err := f.a.Run(t.Context(), true); err != nil { t.Fatal(err) } if _, err := os.Stat(updateBegunPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("legacy recovery kept marker: %v", err) } } else if err == nil { t.Fatal("missing or invalid floor policy accepted") } }) } } // The floor decision is read under the apply lock: a locked-out run never // looks at a guard directory another run may be writing (Opus merge M1-1). func TestSystemFloorHookRunsOnlyUnderTheApplyLock(t *testing.T) { f := newApplyFixture(t, "stable", "v1") f.promote("v2", []byte("x"), false) called := 0 f.a.floorHook = func(*systemApplier) error { called++; return nil } held, err := os.OpenFile(f.a.lockPath(), os.O_CREATE|os.O_RDWR, 0o600) if err != nil { t.Fatal(err) } if err := flockExclusiveNonblock(held); err != nil { t.Fatal(err) } if err := f.a.Run(t.Context(), false); err != nil || called != 0 { t.Fatalf("locked-out run: err=%v hook calls=%d", err, called) } held.Close() if err := f.a.Run(t.Context(), false); err != nil || called != 1 { t.Fatalf("lock-holding run: err=%v hook calls=%d", err, called) } }