carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "fmt"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/hostbox"
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/hostguard"
13 "github.com/carlosframework/platform/internal/hostwrite"
14 "github.com/carlosframework/platform/internal/registry"
15 "github.com/carlosframework/platform/internal/rootstate"
16 "github.com/carlosframework/platform/internal/schedule"
17)
18
19// witnessPolicy is the production claim policy as claimconstruction.go
20// composes it (hostbox.Policy with the registry hold policy, then
21// WithRefusals) carrying claimproduction.go's unit-shape refusal from
22// productionClaimUnitShapePolicy. The witnesses used an allow-all policy,
23// which hid that the refusal refused every fresh install (harness G10,
24// 2026-10-21).
25//
26// The refusal runs in instance-control mode: agent mode asks root's
27// boundInputs over the role-launch socket, which these in-process worlds do
28// not serve. Both modes share the row lookup this pins, and
29// TestClaimUnitShapePolicyFreshInstallAgentReader covers root's reader. The
30// unit-binding store and claim-units configuration are empty seams: no
31// configured shape and no saved binding, as on a box with no bound units.
32// missing counts the lookups that found no row.
33func witnessPolicy(t *testing.T, reg *registry.Registry) (policy func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error, missing *int) {
34 t.Helper()
35 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes
36 t.Cleanup(func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes })
37 openClaimUnitBindingsRead = func() (rootstate.Store, error) { return &rollbackMemory{files: map[string][]byte{}}, nil }
38 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
39 missing = new(int)
40 route := func(host string) (*registry.Route, error) {
41 rt, err := reg.Get(host)
42 if err == nil && rt == nil {
43 *missing++
44 }
45 return rt, err
46 }
47 return hostbox.Policy{Held: hostbox.RegistryHoldPolicy(reg)}.WithRefusals(productionClaimUnitShapePolicy("instance-control", route)).Check, missing
48}
49
50// G10 (harness, 2026-10-21): the first claim install on an empty box, through
51// the remote path into root's real commitRootInventory, under the production
52// policy. The install's first work.With (inventory_registration.go's route
53// entry) runs before the registry row exists; the unit-shape refusal used to
54// answer ErrChanged there on every attempt, leaving root's inventory at
55// revision 1 with no route entry and no row.
56func TestClaimFreshInstallUnderProductionPolicy(t *testing.T) {
57 w := newBindingInstallWorld(t)
58 policy, missing := witnessPolicy(t, w.reg)
59 w.source.Policy = policy
60 w.adapter.Reconciler.Writes = hostwrite.Config{Enabled: true, Selector: w.source}
61 if row, err := w.reg.Get(w.rec.Host); err != nil || row != nil {
62 t.Fatalf("not a fresh install: row %v, %v", row, err)
63 }
64
65 w.install(t)
66
67 if *missing == 0 {
68 t.Fatal("the unit-shape refusal never saw the fresh install's missing row")
69 }
70 in, revision := w.inventory(t)
71 if revision != 3 || len(in.Entries) != 1 || in.Entries[0].Kind != "route" || in.Entries[0].Materialization != "materialized" || in.DirectoryRevision != w.pinned.Binding {
72 t.Fatalf("inventory revision %d, entries %+v, stamp %q", revision, in.Entries, in.DirectoryRevision)
73 }
74 row, err := w.reg.Get(w.rec.Host)
75 if err != nil || row == nil || row.ClaimOwnership != w.route.ClaimOwnership || row.ClaimRowID != in.Entries[0].RowIdentity {
76 t.Fatalf("registry row %+v, %v", row, err)
77 }
78 holder := w.f.read().Claim.Holder
79 if row.ClaimIncarnation != holder.Incarnation || row.ClaimStamp != holder.Stamp {
80 t.Fatalf("row owned by %s/%d, holder %s/%d", row.ClaimIncarnation, row.ClaimStamp, holder.Incarnation, holder.Stamp)
81 }
82 view, err := w.rootAuth.View(t.Context(), w.rec.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp})
83 if err != nil || view.Installation.InventoryRevision != revision {
84 t.Fatalf("guard inventory revision %d (journal %d), %v", view.Installation.InventoryRevision, revision, err)
85 }
86}
87
88// Fix round 2 (Astra on 4dd4c436): only the installation's own work may go
89// on without a registry row. A timer the runner selected for a serving host
90// keeps its cached route; when the route is removed after that read, while
91// the claim, installation, timer row and running producer all survive, the
92// production policy must refuse both the delivery's producer registration
93// (ClaimTimerDelivery) and the runner's bookkeeping (Runner.record's
94// MarkTimerFired, which runs inside the timer work's With exactly as here).
95func TestClaimTimerRefusedAfterRouteRemoval(t *testing.T) {
96 w := newBindingInstallWorld(t)
97 w.install(t)
98 ctx := t.Context()
99 host := w.rec.Host
100 if err := w.reg.UpsertTimer(registry.Timer{Host: host, Name: "nightly", Kind: schedule.KindOnce, Spec: "once", Path: "/tick", NextAt: 1}); err != nil {
101 t.Fatal(err)
102 }
103 timer, err := w.reg.GetTimer(host, "nightly")
104 if err != nil || timer == nil {
105 t.Fatal(timer, err)
106 }
107 route, err := w.reg.Get(host) // the runner's cached route
108 if err != nil || route == nil || !route.ClaimOwnership.Valid() {
109 t.Fatal(route, err)
110 }
111 timer.ClaimOwnership = route.ClaimOwnership // as a claim box's timer rows carry it
112 holder := w.f.read().Claim.Holder
113 work, err := w.source.Select(ctx, host, "timer")
114 if err != nil {
115 t.Fatal(err)
116 }
117 if s, err := hostbox.Selected(work); err != nil || s.Installation() {
118 t.Fatal("timer work is marked as installation work", err)
119 }
120 target := hostguard.FenceTarget{Host: host, Incarnation: holder.Incarnation, Stamp: holder.Stamp, Session: holder.Session, Box: w.pinned.Box, Enrollment: w.pinned.Enrollment, ControllerID: "controller"}
121 var registered []string
122 p, err := NewClaimProducers(target, "start", func(_ context.Context, _ hostguard.FenceTarget, id string) error {
123 registered = append(registered, id)
124 return nil
125 })
126 if err != nil {
127 t.Fatal(err)
128 }
129 set := &ClaimProducerSet{}
130 if err := set.Add(p); err != nil {
131 t.Fatal(err)
132 }
133 seq := 0
134 deliver := ClaimTimerDelivery(set, func() string { seq++; return fmt.Sprintf("d%d", seq) })
135 mark := func(at int64) (bool, error) {
136 var applied bool
137 err := work.With(ctx, hostwrite.Identity{}, func() error {
138 var e error
139 applied, e = w.reg.MarkTimerFired(host, timer.Name, timer.Registration, at, "200", 5, 0, 0, 0)
140 return e
141 })
142 return applied, err
143 }
144
145 // With the route present the same work delivers and records.
146 _, finish, err := deliver(ctx, work, *route, *timer)
147 if err != nil || len(registered) != 1 {
148 t.Fatalf("delivery with the route present: %v, registrations %v", err, registered)
149 }
150 finish()
151 if applied, err := mark(100); err != nil || !applied {
152 t.Fatalf("bookkeeping with the route present: applied %v, %v", applied, err)
153 }
154 before, err := w.reg.GetTimer(host, "nightly")
155 if err != nil || before == nil || before.LastAt != 100 {
156 t.Fatal(before, err)
157 }
158
159 // The route goes after the runner read it.
160 if err := w.reg.Delete(host); err != nil {
161 t.Fatal(err)
162 }
163 if rt, err := w.reg.Get(host); err != nil || rt != nil {
164 t.Fatal("route still present", rt, err)
165 }
166 if still, err := w.reg.GetTimer(host, "nightly"); err != nil || still == nil {
167 t.Fatal("the timer row went with the route; this witness needs it to survive", err)
168 }
169 if _, _, err := deliver(ctx, work, *route, *timer); err == nil || len(registered) != 1 {
170 t.Errorf("delivered after the route was removed: %v, registrations %v", err, registered)
171 }
172 if applied, err := mark(200); err == nil || applied {
173 t.Errorf("bookkeeping after the route was removed: applied %v, %v", applied, err)
174 }
175 after, err := w.reg.GetTimer(host, "nightly")
176 if err != nil || after == nil || *after != *before {
177 t.Errorf("timer row changed after the route was removed: %+v (was %+v), %v", after, before, err)
178 }
179}
180