package host import ( "context" "crypto/ecdh" "crypto/ed25519" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // Held writes under the claim (moves after claim Plan 12, Task 7; Paul's T7 // ruling of 2026-10-12 and platform-1e's answer to Q2.2). // heldSelections is the claim's selector as the held hooks see it: it // records each selection and runs every write it is handed, recording the // identity the write names (and, with log, the order of the writes). type heldSelections struct { selected []string ids []hostwrite.Identity log *[]string refuse error } func (s *heldSelections) Select(_ context.Context, host, action string) (hostwrite.Work, error) { s.selected = append(s.selected, host+"/"+action) if s.refuse != nil { return nil, s.refuse } return heldSelectionWork{s}, nil } type heldSelectionWork struct{ s *heldSelections } func (w heldSelectionWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error { w.s.ids = append(w.s.ids, id) if w.s.log != nil { *w.s.log = append(*w.s.log, "write") } return fn() } func claimWrites(sel *heldSelections) hostwrite.Config { return hostwrite.Config{Enabled: true, Selector: sel} } // TestT7UnderTheClaimStagesWithoutPublishing: under the claim T7 stages the // pinned release into the shared version store and verifies it, admitted // and authorized, and passes without publishing live/ and without any // selection (a moving claim grants no held write). A release D3 does not // admit is T7's not-proven sentence, a build nothing authorizes fails, and // a version store this process cannot write is MV-N455, with nothing relayed // to the root stage pass, which has no publication to make under the claim. func TestT7UnderTheClaimStagesWithoutPublishing(t *testing.T) { ctx := context.Background() setup := func(t *testing.T) (*phase1Harness, provision.Record) { t.Helper() h := newPhase1Harness(t) rec := h.start(t, "manual") shipPinnedV7(t, h.s) h.pass(t) t.Setenv("CARLOS_HOST_CLAIM", "1") return h, rec } t.Run("staged and verified, not published", func(t *testing.T) { h, rec := setup(t) sel := &heldSelections{} data, opt := t.TempDir(), t.TempDir() relayed := false ready, _ := heldReleaseHooks(claimWrites(sel), testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), func(string, string, error) { relayed = true }) for i := 0; i < 2; i++ { if c := ready(ctx, h.s, rec, "v7"); !c.OK { t.Fatalf("T7 under the claim, pass %d = %+v", i+1, c) } } if _, err := os.Stat(release.VersionPath(opt, "bab", "v7", "app")); err != nil { t.Fatalf("v7 was not staged: %v", err) } if _, err := os.Lstat(release.LiveLink(opt, p1Host)); !os.IsNotExist(err) { t.Fatalf("T7 published live/ under the claim: %v", err) } if len(sel.selected) != 0 || relayed { t.Fatalf("T7 selected %v (relayed %v); a moving claim grants no held write", sel.selected, relayed) } // Through the target's report: T7 passes. tc := h.targetCfg() tc.DataDir = data tc.ReleaseReady = ready h.targetPass(t, tc) if f := failing(h.targetReport(t)); f[moves.CheckRelease] != "" { t.Fatalf("T7 in the report = %q", f[moves.CheckRelease]) } }) t.Run("not proven", func(t *testing.T) { h, rec := setup(t) box := newHeldProofBox(t) box.require(t, "app") opt := t.TempDir() ready, _ := heldReleaseHooks(claimWrites(&heldSelections{}), testRegistry(t), opt, t.TempDir(), "uk-box", hsPub, hsRatchet(t), box.gate(), nil) if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7NotProven { t.Fatalf("T7 = %+v, want %q", c, t7NotProven) } if _, err := os.Stat(filepath.Join(opt, "versions")); !os.IsNotExist(err) { t.Fatalf("an unproven release was staged: %v", err) } }) t.Run("not authorized", func(t *testing.T) { h, rec := setup(t) opt := t.TempDir() ready, _ := heldReleaseHooks(claimWrites(&heldSelections{}), testRegistry(t), opt, t.TempDir(), "uk-box", nil, hsRatchet(t), offProofGate(t), nil) if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed { t.Fatalf("T7 with no release key = %+v, want %q", c, t7Failed) } if _, err := os.Stat(filepath.Join(opt, "versions")); !os.IsNotExist(err) { t.Fatalf("an unauthorized build was staged: %v", err) } }) t.Run("blocked", func(t *testing.T) { h, rec := setup(t) prev := heldStageVerified t.Cleanup(func() { heldStageVerified = prev }) heldStageVerified = func(context.Context, release.Store, ed25519.PublicKey, *release.DigestRatchet, string, registry.Route, string, *release.ProofGate) (*release.Manifest, error) { return nil, fmt.Errorf("%w: read-only", release.ErrStageBlocked) } data := t.TempDir() relayed := false ready, _ := heldReleaseHooks(claimWrites(&heldSelections{}), testRegistry(t), t.TempDir(), data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), func(string, string, error) { relayed = true }) if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7NotStaged { t.Fatalf("T7 on a read-only version store = %+v, want %q", c, t7NotStaged) } if _, err := os.Stat(filepath.Join(release.HeldStageDir(data), p1Host+".json")); !os.IsNotExist(err) || relayed { t.Fatalf("a held stage was relayed to the root pass under the claim (%v, %v)", err, relayed) } }) } // TestClaimOffT7IsUnchanged: with the claim off, T7 stages and publishes // live/ exactly as before (heldReleaseReady), relays a blocked stage // to the root stage pass, and the configured step's release is that same // check. func TestClaimOffT7IsUnchanged(t *testing.T) { ctx := context.Background() h := newPhase1Harness(t) rec := h.start(t, "manual") shipPinnedV7(t, h.s) h.pass(t) data, opt := t.TempDir(), t.TempDir() ready, live := heldReleaseHooks(hostwrite.Config{}, testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil) if c := ready(ctx, h.s, rec, "v7"); !c.OK { t.Fatalf("T7 = %+v", c) } if v, _ := release.LiveVersion(opt, p1Host); v != "v7" { t.Fatalf("live = %q, want v7: T7 publishes with the claim off", v) } if c := live(ctx, h.s, rec, "v7"); !c.OK { t.Fatalf("the configured step's release = %+v", c) } prev := heldMaterialize t.Cleanup(func() { heldMaterialize = prev }) heldMaterialize = func(context.Context, release.Store, ed25519.PublicKey, *release.DigestRatchet, string, registry.Route, string, *release.ProofGate, func(func() error) error) (*release.Manifest, error) { return nil, fmt.Errorf("%w: read-only", release.ErrStageBlocked) } data2 := t.TempDir() relayed := false ready2, _ := heldReleaseHooks(hostwrite.Config{}, testRegistry(t), t.TempDir(), data2, "uk-box", hsPub, hsRatchet(t), offProofGate(t), func(string, string, error) { relayed = true }) if c := ready2(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Waiting { t.Fatalf("T7 on a read-only version store = %+v, want %q", c, t7Waiting) } if _, err := os.Stat(filepath.Join(release.HeldStageDir(data2), p1Host+".json")); err != nil || !relayed { t.Fatalf("the blocked stage was not relayed to the root pass (%v, %v)", err, relayed) } } // heldConfigKit is a real held render's inputs: a store with bab/app's // default bundle, the secrets key, a data directory and the held row. type heldConfigKit struct { s release.Store priv *ecdh.PrivateKey dd string rt registry.Route pin *provision.MoveInputs } func newHeldConfigKit(t *testing.T) heldConfigKit { t.Helper() ctx := context.Background() s := release.NewDirStore(t.TempDir()) privB64, _, err := appconfig.GenerateKey() if err != nil { t.Fatal(err) } priv, err := appconfig.ParsePrivateKey(privB64, "test") if err != nil { t.Fatal(err) } if err := appconfig.SetEnv(ctx, s, "bab", "app", map[string]string{"GREETING": "hello"}); err != nil { t.Fatal(err) } _, def, err := s.Get(ctx, appconfig.Key("bab", "app")) if err != nil { t.Fatal(err) } return heldConfigKit{s: s, priv: priv, dd: t.TempDir(), rt: registry.Route{Host: p1Host, Kind: "instance", App: "app", Account: "bab", Status: "landing", HoldOp: p1ID, Socket: provision.DefaultSocket(p1Host)}, pin: &provision.MoveInputs{BundleETags: map[string]string{"default": def, "environment": ""}, Release: "v7"}} } // TestConfiguredRendersUnderTheHeldConfigSelection: under the claim the // landing's held render is selected as held-config for the host, under the // identity of the pinned configuration, and keeps carry (c): the journal // learns the sha256 of the bytes before the selection writes them. A render // whose merged inputs are not the pins selects and writes nothing and is the // landing's "configuration changed" refusal, never the policy's. With the // claim off the render is RenderHeldBefore's, unchanged. func TestConfiguredRendersUnderTheHeldConfigSelection(t *testing.T) { ctx := context.Background() t.Run("the render", func(t *testing.T) { k := newHeldConfigKit(t) t.Setenv("CARLOS_HOST_CLAIM", "1") var log []string sel := &heldSelections{log: &log} render := heldRenderer(claimWrites(sel), k.priv, k.dd, "uk-box") path := appconfig.ConfigEnvPath(k.dd, k.rt) var told string hr, err := render(ctx, k.s, k.rt, p1ID, k.pin, func(sha string) error { log = append(log, "journal") if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("the file was there before the journal: %v", err) } told = sha return nil }) if err != nil { t.Fatal(err) } if len(sel.selected) != 1 || sel.selected[0] != p1Host+"/"+movesActionHeldConfig { t.Fatalf("selected %v, want one held-config selection for the host", sel.selected) } want := HeldConfigIdentity(k.pin.BundleETags, k.pin.OverlayETag) if len(sel.ids) == 0 { t.Fatal("nothing was written through the selection") } for _, id := range sel.ids { if id != want { t.Fatalf("a write named %+v, want the pinned configuration %+v", id, want) } } if len(log) < 2 || log[0] != "journal" || log[1] != "write" { t.Fatalf("order %v, want the journal before the writes", log) } body, err := os.ReadFile(path) if err != nil { t.Fatal(err) } sum := sha256.Sum256(body) if got := hex.EncodeToString(sum[:]); told != got || hr.SHA256 != got { t.Fatalf("journal %q, render %q, file %q", told, hr.SHA256, got) } if !sameBundle(hr.BundleETags, k.pin.BundleETags) { t.Fatalf("rendered %v, pinned %v", hr.BundleETags, k.pin.BundleETags) } }) t.Run("a changed configuration selects nothing", func(t *testing.T) { k := newHeldConfigKit(t) t.Setenv("CARLOS_HOST_CLAIM", "1") if err := appconfig.SetEnv(ctx, k.s, "bab", "app", map[string]string{"GREETING": "changed"}); err != nil { t.Fatal(err) } sel := &heldSelections{} journalled := false _, err := heldRenderer(claimWrites(sel), k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { journalled = true; return nil }) if !errors.Is(err, errHeldRenderUnpinned) { t.Fatalf("a changed configuration: %v, want errHeldRenderUnpinned", err) } if len(sel.selected) != 0 || journalled { t.Fatalf("selected %v, journalled %v", sel.selected, journalled) } if _, err := os.Stat(appconfig.ConfigEnvPath(k.dd, k.rt)); !os.IsNotExist(err) { t.Fatalf("a file was written: %v", err) } }) t.Run("a refused selection writes nothing", func(t *testing.T) { k := newHeldConfigKit(t) t.Setenv("CARLOS_HOST_CLAIM", "1") sel := &heldSelections{refuse: hostwrite.ErrUnavailable} if _, err := heldRenderer(claimWrites(sel), k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("a refused selection: %v", err) } if _, err := os.Stat(appconfig.ConfigEnvPath(k.dd, k.rt)); !os.IsNotExist(err) { t.Fatalf("a file was written: %v", err) } }) t.Run("claim off", func(t *testing.T) { k := newHeldConfigKit(t) hr, err := heldRenderer(hostwrite.Config{}, k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { return nil }) if err != nil || hr.SHA256 == "" { t.Fatalf("the claim-off render: %+v, %v", hr, err) } t.Setenv("CARLOS_HOST_CLAIM", "1") if _, err := heldRenderer(hostwrite.Config{}, k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { return nil }); !errors.Is(err, appconfig.ErrHeldClaimWired) { t.Fatalf("the legacy render under the flag: %v, want ErrHeldClaimWired as before", err) } }) t.Run("configured passes the pins and refuses a changed configuration", func(t *testing.T) { h := newLandingHarness(t) h.flip(t) t.Setenv("CARLOS_HOST_CLAIM", "1") withMovesWakeSource(t, true) var pinned []*provision.MoveInputs h.renderPin = func(pin *provision.MoveInputs) { pinned = append(pinned, pin) } h.renderHook = func(func(string) error) (appconfig.HeldRender, error) { return appconfig.HeldRender{}, errHeldRenderUnpinned } h.land(t) m := h.read(t, p1Host).Move if m.Reason != provision.RollbackConfigChanged || m.LandingPhase != provision.LandConfigured { t.Fatalf("after an unpinned render under the claim: %+v", m) } if len(pinned) == 0 || pinned[0] == nil || !sameBundle(pinned[0].BundleETags, m.Inputs.BundleETags) || pinned[0].OverlayETag != m.Inputs.OverlayETag { t.Fatalf("configured rendered with pins %+v, want the record's %+v", pinned, m.Inputs) } }) } // TestConfiguredPublishesTheReleaseUnderHeldRelease: under the claim T7 // leaves live/ alone and the configured step publishes it, selected as // held-release for the host under the identity naming exactly the pinned // version, then reads it back as T7 used to (VerifyHeldLive). A replay finds // it live and selects nothing; a refused selection publishes nothing. The // landing calls this hook at configured, never T7's. func TestConfiguredPublishesTheReleaseUnderHeldRelease(t *testing.T) { ctx := context.Background() t.Run("the publication", func(t *testing.T) { h := newPhase1Harness(t) rec := h.start(t, "manual") shipPinnedV7(t, h.s) h.pass(t) t.Setenv("CARLOS_HOST_CLAIM", "1") sel := &heldSelections{} data, opt := t.TempDir(), t.TempDir() ready, live := heldReleaseHooks(claimWrites(sel), testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil) if c := ready(ctx, h.s, rec, "v7"); !c.OK { t.Fatalf("T7 = %+v", c) } if v, _ := release.LiveVersion(opt, p1Host); v != "" { t.Fatalf("live = %q after T7 under the claim", v) } if c := live(ctx, h.s, rec, "v7"); !c.OK { t.Fatalf("configured's release = %+v", c) } if v, _ := release.LiveVersion(opt, p1Host); v != "v7" { t.Fatalf("live = %q, want v7", v) } if len(sel.selected) != 1 || sel.selected[0] != p1Host+"/"+movesActionHeldRelease { t.Fatalf("selected %v, want one held-release selection for the host", sel.selected) } if len(sel.ids) != 1 || sel.ids[0] != (hostwrite.Identity{Release: "v7"}) { t.Fatalf("published under %v, want the pinned release alone", sel.ids) } if c := live(ctx, h.s, rec, "v7"); !c.OK || len(sel.selected) != 1 { t.Fatalf("a replay: %+v, selections %v", c, sel.selected) } }) t.Run("a refused selection publishes nothing", func(t *testing.T) { h := newPhase1Harness(t) rec := h.start(t, "manual") shipPinnedV7(t, h.s) h.pass(t) t.Setenv("CARLOS_HOST_CLAIM", "1") sel := &heldSelections{refuse: hostwrite.ErrUnavailable} opt := t.TempDir() _, live := heldReleaseHooks(claimWrites(sel), testRegistry(t), opt, t.TempDir(), "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil) if c := live(ctx, h.s, rec, "v7"); c.OK { t.Fatalf("a refused selection published: %+v", c) } if v, _ := release.LiveVersion(opt, p1Host); v != "" { t.Fatalf("live = %q", v) } }) t.Run("configured calls the publication", func(t *testing.T) { h := newLandingHarness(t) h.flip(t) t.Setenv("CARLOS_HOST_CLAIM", "1") withMovesWakeSource(t, true) var published []string h.releaseLive = func(_ context.Context, _ release.Store, _ provision.Record, v string) moves.Check { published = append(published, v) return moves.Check{OK: true} } h.release = moves.Check{Reason: "T7's hook was asked at configured"} h.land(t) h.installed(t) if pin := h.read(t, p1Host).Move.Inputs; len(published) == 0 || published[0] != pin.Release { t.Fatalf("configured published %v, want the pinned %q", published, pin.Release) } }) t.Run("under the claim configured never falls back to T7", func(t *testing.T) { h := newLandingHarness(t) h.flip(t) t.Setenv("CARLOS_HOST_CLAIM", "1") withMovesWakeSource(t, true) h.land(t) if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandConfigured || m.Reason != "" { t.Fatalf("with no publication hook under the claim: %+v", m) } if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the database was installed with nothing published: %v", err) } }) } // TestCheckingRefusesUntilTheWakeSourceRegisters (Q2.3): under the claim // the landing keeps refusing at checking with RollbackClaimWired while the // wake-source gate is shut, and lands once it opens. With the claim off the // gate does not matter. func TestCheckingRefusesUntilTheWakeSourceRegisters(t *testing.T) { t.Run("shut", func(t *testing.T) { h := newLandingHarness(t) h.flip(t) t.Setenv("CARLOS_HOST_CLAIM", "1") withMovesWakeSource(t, false) h.land(t) if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackClaimWired || m.LandingPhase != provision.LandChecking { t.Fatalf("under the claim with the gate shut: %+v", m) } }) t.Run("open", func(t *testing.T) { h := newLandingHarness(t) h.flip(t) t.Setenv("CARLOS_HOST_CLAIM", "1") withMovesWakeSource(t, true) h.releaseLive = func(context.Context, release.Store, provision.Record, string) moves.Check { return moves.Check{OK: true} } h.land(t) if m := h.read(t, p1Host).Move; m.Reason != "" { t.Fatalf("under the claim with the gate open: %+v", m) } h.installed(t) }) t.Run("claim off", func(t *testing.T) { h := newLandingHarness(t) h.flip(t) withMovesWakeSource(t, false) h.land(t) if m := h.read(t, p1Host).Move; m.Reason != "" { t.Fatalf("with the claim off: %+v", m) } h.installed(t) }) }