carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/base64"
8 "encoding/json"
9 "errors"
10 "os"
11 "path/filepath"
12 "slices"
13 "strconv"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/caps"
19 "github.com/carlosframework/platform/internal/registry"
20)
21
22func testRegistry(t *testing.T) *registry.Registry {
23 t.Helper()
24 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
25 if err != nil {
26 t.Fatalf("open registry: %v", err)
27 }
28 return reg
29}
30
31func addRoute(t *testing.T, reg *registry.Registry, host, app, channel string, hibernate bool) {
32 t.Helper()
33 if err := reg.Upsert(registry.Route{
34 Host: host, Kind: "instance", App: app, Channel: channel, Hibernate: hibernate,
35 }); err != nil {
36 t.Fatalf("upsert %s: %v", host, err)
37 }
38}
39
40func TestPassRepointsAwakeAndHibernating(t *testing.T) {
41 s := NewDirStore(t.TempDir())
42 ctx := context.Background()
43 priv, pub := testKeys(t)
44 opt := t.TempDir()
45 reg := testRegistry(t)
46
47 // canary/* has no bake window, so the pass applies immediately.
48 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
49 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
50 t.Fatalf("Promote: %v", err)
51 }
52 addRoute(t, reg, "awake.example.com", "myapp", "canary/x", false)
53 addRoute(t, reg, "sleepy.example.com", "myapp", "canary/x", true)
54
55 var restarted []string
56 restart := func(host string) error { restarted = append(restarted, host); return nil }
57 if err := Pass(ctx, reg, s, pub, opt, restart); err != nil {
58 t.Fatalf("Pass: %v", err)
59 }
60
61 for _, host := range []string{"awake.example.com", "sleepy.example.com"} {
62 if v, _ := LiveVersion(opt, host); v != "v1" {
63 t.Fatalf("%s live = %q, want v1", host, v)
64 }
65 rt, err := reg.Get(host)
66 if err != nil || rt == nil || rt.Version != "v1" {
67 t.Fatalf("%s registry version = %+v, %v", host, rt, err)
68 }
69 }
70 // Only the awake host was restarted; the hibernating host is NEVER woken
71 // — its next wake execs the new binary via the live symlink.
72 if len(restarted) != 1 || restarted[0] != "awake.example.com" {
73 t.Fatalf("restarted = %v, want only awake.example.com", restarted)
74 }
75 // The staged artifact matches the shipped bytes (sha256-gated download).
76 body, err := os.ReadFile(VersionPath(opt, "bab", "v1", "myapp"))
77 if err != nil || string(body) != "build one" {
78 t.Fatalf("staged bytes = %q, %v", body, err)
79 }
80}
81
82// TestPassRestartHibernatingOptIn pins both sides of the RestartHibernating
83// door. Default (RunAdopt's shape, no option): hibernate-flagged routes get
84// the pure repoint and the callback never sees them — byte-identical to the
85// behavior TestPassRepointsAwakeAndHibernating above pins. Opted in (the
86// agent's in-process loops): the callback is consulted for sleepers too, so a
87// WARM exec child — minted Hibernate:true, but running right now — can be
88// cycled onto the binary this pass just pointed it at.
89//
90// The pass itself still starts nothing either way: all the option changes is
91// whether the callback is asked. Not waking a sleeper is the CALLBACK's
92// promise (Bounce returns false when nothing is awake; try-restart is a no-op
93// on a stopped unit), which is why handing it a sleeping host is safe.
94func TestPassRestartHibernatingOptIn(t *testing.T) {
95 for _, tc := range []struct {
96 name string
97 optIn bool
98 want []string
99 reason string
100 }{
101 {"default: sleepers are not offered to the callback", false,
102 []string{"awake.example.com"},
103 "the root adopt pass has no children of its own to cycle"},
104 {"opted in: sleepers are offered too", true,
105 []string{"awake.example.com", "sleepy.example.com"},
106 "a warm exec child must bounce onto its new binary"},
107 } {
108 t.Run(tc.name, func(t *testing.T) {
109 s := NewDirStore(t.TempDir())
110 ctx := context.Background()
111 priv, pub := testKeys(t)
112 opt := t.TempDir()
113 reg := testRegistry(t)
114
115 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
116 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
117 t.Fatalf("Promote: %v", err)
118 }
119 addRoute(t, reg, "awake.example.com", "myapp", "canary/x", false)
120 addRoute(t, reg, "sleepy.example.com", "myapp", "canary/x", true)
121
122 var restarted []string
123 restart := func(host string) error { restarted = append(restarted, host); return nil }
124 var opts []PassOption
125 if tc.optIn {
126 opts = append(opts, RestartHibernating(restart))
127 }
128 if err := Pass(ctx, reg, s, pub, opt, restart, opts...); err != nil {
129 t.Fatalf("Pass: %v", err)
130 }
131 if strings.Join(restarted, ",") != strings.Join(tc.want, ",") {
132 t.Fatalf("restarted = %v, want %v (%s)", restarted, tc.want, tc.reason)
133 }
134 // Either way the repoint happened for both — the option changes who
135 // is offered to the callback, never which hosts adopt.
136 for _, host := range []string{"awake.example.com", "sleepy.example.com"} {
137 if v, _ := LiveVersion(opt, host); v != "v1" {
138 t.Fatalf("%s live = %q, want v1", host, v)
139 }
140 }
141 })
142 }
143}
144
145// TestPassRestartHibernatingKeepsSleepersLast pins the ordering the option
146// must not disturb: awake hosts are still adopted BEFORE any sleeper, so an
147// awake host can discover a boot-broken build and poison it before a
148// hibernating host is moved onto it. Handing sleepers a callback must not
149// promote them into the first loop.
150func TestPassRestartHibernatingKeepsSleepersLast(t *testing.T) {
151 s := NewDirStore(t.TempDir())
152 ctx := context.Background()
153 priv, pub := testKeys(t)
154 opt := t.TempDir()
155 reg := testRegistry(t)
156
157 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
158 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
159 t.Fatalf("Promote: %v", err)
160 }
161 // Named so a plain registry ordering (alphabetical by host) would put the
162 // sleeper first — only the awake/hibernating split can produce the order
163 // this test wants.
164 addRoute(t, reg, "aaa-sleepy.example.com", "myapp", "canary/x", true)
165 addRoute(t, reg, "zzz-awake.example.com", "myapp", "canary/x", false)
166
167 var order []string
168 restart := func(host string) error { order = append(order, host); return nil }
169 if err := Pass(ctx, reg, s, pub, opt, restart, RestartHibernating(restart)); err != nil {
170 t.Fatalf("Pass: %v", err)
171 }
172 want := []string{"zzz-awake.example.com", "aaa-sleepy.example.com"}
173 if strings.Join(order, ",") != strings.Join(want, ",") {
174 t.Fatalf("restart order = %v, want %v (awake hosts must still go first)", order, want)
175 }
176}
177
178// TestPassRestartHibernatingPoisonsLikeAnyOtherHost: a sleeper's failed
179// restart takes the same rollback the awake path takes — the symlink goes
180// back and the version is poisoned for the rest of the pass. Nothing about
181// hibernation makes a broken build safe to leave pointed at.
182func TestPassRestartHibernatingPoisonsLikeAnyOtherHost(t *testing.T) {
183 s := NewDirStore(t.TempDir())
184 ctx := context.Background()
185 priv, pub := testKeys(t)
186 opt := t.TempDir()
187 reg := testRegistry(t)
188
189 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
190 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("build two"))
191 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
192 t.Fatalf("Promote v1: %v", err)
193 }
194 addRoute(t, reg, "sleepy.example.com", "myapp", "canary/x", true)
195 ok := func(string) error { return nil }
196 if err := Pass(ctx, reg, s, pub, opt, ok, RestartHibernating(ok)); err != nil {
197 t.Fatalf("Pass v1: %v", err)
198 }
199
200 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil {
201 t.Fatalf("Promote v2: %v", err)
202 }
203 boom := func(string) error { return errors.New("bounce failed") }
204 if err := Pass(ctx, reg, s, pub, opt, boom, RestartHibernating(boom)); err != nil {
205 t.Fatalf("Pass v2: %v", err)
206 }
207 if v, _ := LiveVersion(opt, "sleepy.example.com"); v != "v1" {
208 t.Fatalf("live = %q, want the symlink rolled back to v1", v)
209 }
210}
211
212// TestPassBakeDirMovesTheClock proves the BakeDir option's whole point
213// (issue #13): the bake clock lands under the given dir, not optDir's
214// legacy /opt-dir location.
215func TestPassBakeDirMovesTheClock(t *testing.T) {
216 s := NewDirStore(t.TempDir())
217 ctx := context.Background()
218 priv, pub := testKeys(t)
219 opt := t.TempDir()
220 bakeDir := t.TempDir()
221 reg := testRegistry(t)
222
223 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
224 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
225 t.Fatalf("Promote: %v", err)
226 }
227 addRoute(t, reg, "awake.example.com", "myapp", "canary/x", false)
228
229 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, BakeDir(bakeDir)); err != nil {
230 t.Fatalf("Pass: %v", err)
231 }
232 if _, err := os.Stat(filepath.Join(bakeDir, "channel-first-seen.json")); err != nil {
233 t.Fatalf("BakeDir option did not land the clock in bakeDir: %v", err)
234 }
235 if _, err := os.Stat(FirstSeenPath(opt)); !os.IsNotExist(err) {
236 t.Fatalf("bake clock also written to the legacy opt dir despite BakeDir (stat err %v)", err)
237 }
238}
239
240// TestPassMigratesBakeClockDurably proves the migration off the legacy path
241// completes in ONE pass even when nothing new was observed (2026-08-03
242// review, IMPORTANT): Save is changed-gated, so with the head already
243// recorded in the legacy clock, Observe reports no change this pass and the
244// ordinary "tick and save" path would never write the new location — a box
245// could run for days with the new path absent, and deleting the legacy file
246// in that window would silently reset the channel's bake window. The legacy
247// clock here already has this exact head, so the ONLY way the new path ends
248// up populated is the migration forcing its own first Save.
249func TestPassMigratesBakeClockDurably(t *testing.T) {
250 s := NewDirStore(t.TempDir())
251 ctx := context.Background()
252 priv, pub := testKeys(t)
253 opt := t.TempDir()
254 bakeDir := t.TempDir()
255 reg := testRegistry(t)
256
257 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
258 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
259 t.Fatalf("Promote: %v", err)
260 }
261 addRoute(t, reg, "awake.example.com", "myapp", "canary/x", false)
262
263 legacy := LoadFirstSeen(FirstSeenPath(opt))
264 legacy.Observe("bab", "myapp", "canary/x", "v1", time.Now())
265 if err := legacy.Save(); err != nil {
266 t.Fatalf("seed legacy clock: %v", err)
267 }
268
269 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, BakeDir(bakeDir)); err != nil {
270 t.Fatalf("Pass: %v", err)
271 }
272
273 st := LoadFirstSeen(filepath.Join(bakeDir, "channel-first-seen.json"))
274 if _, ok := st.EligibleAt("bab", "myapp", "canary/x", "v1", DefaultHolds); !ok {
275 t.Fatal("migrated content did not land in the new path after one pass with no new head observed")
276 }
277}
278
279func TestPassPoisonedBuildSkipsRemainingHosts(t *testing.T) {
280 s := NewDirStore(t.TempDir())
281 ctx := context.Background()
282 priv, pub := testKeys(t)
283 opt := t.TempDir()
284 reg := testRegistry(t)
285
286 // Both hosts start on v1 (staged locally), channel head moves to v2.
287 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("good"))
288 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("bad"))
289 if err := StageArtifact(opt, "bab", "v1", "myapp", []byte("good")); err != nil {
290 t.Fatal(err)
291 }
292 for _, h := range []string{"a.example.com", "b.example.com"} {
293 addRoute(t, reg, h, "myapp", "canary/x", false)
294 if err := PointLive(opt, h, "bab", "v1", "myapp"); err != nil {
295 t.Fatal(err)
296 }
297 }
298 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil {
299 t.Fatalf("Promote v2: %v", err)
300 }
301
302 // v2 fails its restart on the first host — it must be poisoned for the
303 // rest of the pass, never cycling every host in turn.
304 var calls int
305 restart := func(host string) error { calls++; return errors.New("unit failed") }
306 if err := Pass(ctx, reg, s, pub, opt, restart); err != nil {
307 t.Fatalf("Pass: %v", err)
308 }
309 if calls != 1 {
310 t.Fatalf("restart called %d times, want 1 (poisoned build must not bounce every host)", calls)
311 }
312 // The failed host was rolled back to what it ran; the second never moved.
313 for _, h := range []string{"a.example.com", "b.example.com"} {
314 if v, _ := LiveVersion(opt, h); v != "v1" {
315 t.Fatalf("%s live = %q, want v1", h, v)
316 }
317 }
318}
319
320func TestPassStableHeldByBakeWindow(t *testing.T) {
321 s := NewDirStore(t.TempDir())
322 ctx := context.Background()
323 priv, pub := testKeys(t)
324 opt := t.TempDir()
325 reg := testRegistry(t)
326
327 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
328 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
329 t.Fatalf("Promote: %v", err)
330 }
331 // The app is DECLARED production (2026-08-04 spec §2): the window is no
332 // longer unconditional — it is what the flag buys.
333 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
334 t.Fatalf("SetProduction: %v", err)
335 }
336 addRoute(t, reg, "h.example.com", "myapp", "", false) // "" = stable
337
338 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
339 t.Fatalf("Pass: %v", err)
340 }
341 // A brand-new stable head is inside its 72h supply-chain window: the
342 // clock started this pass, nothing installs.
343 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
344 t.Fatalf("live = %q, want unmoved (bake hold)", v)
345 }
346 // The pass persisted the first sighting so the window is measured from
347 // the box's own clock.
348 st := LoadFirstSeen(FirstSeenPath(opt))
349 if _, ok := st.EligibleAt("bab", "myapp", "stable", "v1", DefaultHolds); !ok {
350 t.Fatalf("Pass did not tick the first-seen clock")
351 }
352}
353
354func TestPassSkipsUnadoptableRoutes(t *testing.T) {
355 s := NewDirStore(t.TempDir())
356 ctx := context.Background()
357 priv, pub := testKeys(t)
358 opt := t.TempDir()
359 reg := testRegistry(t)
360
361 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
362 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
363 t.Fatalf("Promote: %v", err)
364 }
365 // A static route, an app-less instance, and a mid-provision instance:
366 // none may be moved by an adoption pass.
367 if err := reg.Upsert(registry.Route{Host: "site.example.com", Kind: "static", App: "myapp", Channel: "canary/x"}); err != nil {
368 t.Fatal(err)
369 }
370 addRoute(t, reg, "noapp.example.com", "", "canary/x", false)
371 if err := reg.Upsert(registry.Route{Host: "prov.example.com", Kind: "instance", App: "myapp", Channel: "canary/x", Status: "creating"}); err != nil {
372 t.Fatal(err)
373 }
374 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
375 t.Fatalf("Pass: %v", err)
376 }
377 for _, h := range []string{"site.example.com", "noapp.example.com", "prov.example.com"} {
378 if v, _ := LiveVersion(opt, h); v != "" {
379 t.Fatalf("%s was moved (%q) but is not adoptable", h, v)
380 }
381 }
382}
383
384// TestPassSkipsPlatformMintedRoutes: a platform domain is an ALIAS for an
385// app's canonical route, not a lifecycle host of its own. Adopting one would
386// stage a second copy of the artifacts, mint a live/<alias> symlink no unit
387// ever execs, try-restart a carlos-app@<alias> unit that doesn't exist, and
388// stamp Version onto a row the console then renders as if it were a real
389// instance. The canonical route it aliases is adopted normally — that IS the
390// deploy.
391func TestPassSkipsPlatformMintedRoutes(t *testing.T) {
392 s := NewDirStore(t.TempDir())
393 ctx := context.Background()
394 priv, pub := testKeys(t)
395 opt := t.TempDir()
396 reg := testRegistry(t)
397
398 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
399 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
400 t.Fatalf("Promote: %v", err)
401 }
402 // The canonical route and its platform alias, identical but for the flag.
403 addRoute(t, reg, "myapp.example.com", "myapp", "canary/x", false)
404 if err := reg.Upsert(registry.Route{Host: "myapp.bab.oncarlos.test", Kind: "instance",
405 App: "myapp", Channel: "canary/x", Status: "ready", Platform: true}); err != nil {
406 t.Fatal(err)
407 }
408 var restarted []string
409 if err := Pass(ctx, reg, s, pub, opt, func(h string) error {
410 restarted = append(restarted, h)
411 return nil
412 }); err != nil {
413 t.Fatalf("Pass: %v", err)
414 }
415 if v, _ := LiveVersion(opt, "myapp.bab.oncarlos.test"); v != "" {
416 t.Errorf("platform alias was adopted (live = %q) — aliases are not lifecycle hosts", v)
417 }
418 for _, h := range restarted {
419 if h == "myapp.bab.oncarlos.test" {
420 t.Error("adopt try-restarted a platform alias's nonexistent unit")
421 }
422 }
423 if rt, _ := reg.Get("myapp.bab.oncarlos.test"); rt != nil && rt.Version != "" {
424 t.Errorf("adopt stamped Version %q onto a platform alias row", rt.Version)
425 }
426 // The canonical route still adopts: the alias must not have stolen its
427 // pass (canaries bypass bake, so this lands immediately).
428 if v, _ := LiveVersion(opt, "myapp.example.com"); v != "v1" {
429 t.Errorf("canonical route live = %q, want v1 — skipping aliases must not skip real hosts", v)
430 }
431}
432
433// The test that proves per-account app names actually work end to end: two
434// routes, the SAME app name, different accounts, different channel pointers.
435// Each host must adopt its own account's version. Before accounts, adopt
436// grouped by (app, channel) and would have installed one of them on both.
437func TestPassResolvesPerAccount(t *testing.T) {
438 s := NewDirStore(t.TempDir())
439 ctx := context.Background()
440 priv, pub := testKeys(t)
441 opt := t.TempDir()
442 reg := testRegistry(t)
443
444 shipRelease(t, s, "bab", "blog", "v1", adoptPlatform, []byte("ops build"))
445 shipRelease(t, s, "kqmv", "blog", "v2", adoptPlatform, []byte("kass build"))
446 if err := Promote(ctx, s, priv, "bab", "blog", "canary/x", "v1"); err != nil {
447 t.Fatal(err)
448 }
449 if err := Promote(ctx, s, priv, "kqmv", "blog", "canary/x", "v2"); err != nil {
450 t.Fatal(err)
451 }
452 for _, rt := range []registry.Route{
453 {Host: "ops.example.com", Kind: "instance", App: "blog", Channel: "canary/x", Account: "bab", Status: "ready"},
454 {Host: "kass.example.com", Kind: "instance", App: "blog", Channel: "canary/x", Account: "kqmv", Status: "ready"},
455 } {
456 if err := reg.Upsert(rt); err != nil {
457 t.Fatal(err)
458 }
459 }
460 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
461 t.Fatalf("Pass: %v", err)
462 }
463 for host, want := range map[string]string{
464 "ops.example.com": "v1",
465 "kass.example.com": "v2",
466 } {
467 if got, _ := LiveVersion(opt, host); got != want {
468 t.Errorf("%s live = %q, want %q — each account follows its OWN pointer", host, got, want)
469 }
470 }
471}
472
473// Same app name, same VERSION STRING, different accounts, different bytes.
474// The on-box version store must keep them apart, or one account's binary gets
475// executed under another account's host — the same collision in a different
476// layer from the channel-pointer one above.
477func TestPassStagesPerAccountForIdenticalVersions(t *testing.T) {
478 s := NewDirStore(t.TempDir())
479 ctx := context.Background()
480 priv, pub := testKeys(t)
481 opt := t.TempDir()
482 reg := testRegistry(t)
483
484 shipRelease(t, s, "bab", "blog", "v1", adoptPlatform, []byte("ops bytes"))
485 shipRelease(t, s, "kqmv", "blog", "v1", adoptPlatform, []byte("kass bytes"))
486 for _, acct := range []string{"bab", "kqmv"} {
487 if err := Promote(ctx, s, priv, acct, "blog", "canary/x", "v1"); err != nil {
488 t.Fatal(err)
489 }
490 }
491 for _, rt := range []registry.Route{
492 {Host: "ops.example.com", Kind: "instance", App: "blog", Channel: "canary/x", Account: "bab", Status: "ready"},
493 {Host: "kass.example.com", Kind: "instance", App: "blog", Channel: "canary/x", Account: "kqmv", Status: "ready"},
494 } {
495 if err := reg.Upsert(rt); err != nil {
496 t.Fatal(err)
497 }
498 }
499 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
500 t.Fatalf("Pass: %v", err)
501 }
502 for host, want := range map[string]string{
503 "ops.example.com": "ops bytes",
504 "kass.example.com": "kass bytes",
505 } {
506 target, err := os.Readlink(LiveLink(opt, host))
507 if err != nil {
508 t.Fatalf("%s: read live link: %v", host, err)
509 }
510 got, err := os.ReadFile(target)
511 if err != nil {
512 t.Fatalf("%s: read staged binary: %v", host, err)
513 }
514 if string(got) != want {
515 t.Errorf("%s runs %q, want %q — one account's build was staged over another's", host, got, want)
516 }
517 }
518}
519
520// The ops account keeps the pre-accounts on-disk layout byte for byte, so
521// rolling this code out does not restage and restart every existing host.
522func TestOpsVersionPathIsUnchanged(t *testing.T) {
523 opt := "/opt/carlos"
524 if got := VersionPath(opt, "bab", "v1", "kass"); got != "/opt/carlos/versions/v1/kass" {
525 t.Errorf("ops VersionPath = %q, want the pre-accounts path", got)
526 }
527 if got := VersionPath(opt, "kqmv", "v1", "kass"); got != "/opt/carlos/versions/v1/kqmv/kass" {
528 t.Errorf("non-ops VersionPath = %q", got)
529 }
530}
531
532func TestPassMissingPlatformArtifactSkips(t *testing.T) {
533 s := NewDirStore(t.TempDir())
534 ctx := context.Background()
535 priv, pub := testKeys(t)
536 opt := t.TempDir()
537 reg := testRegistry(t)
538
539 // Shipped for some other platform only: this box must skip, never
540 // install the wrong arch.
541 shipRelease(t, s, "bab", "myapp", "v1", "plan9-mips", []byte("bin"))
542 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
543 t.Fatalf("Promote: %v", err)
544 }
545 addRoute(t, reg, "h.example.com", "myapp", "canary/x", false)
546 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
547 t.Fatalf("Pass: %v", err)
548 }
549 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
550 t.Fatalf("live = %q, want unmoved (no artifact for this platform)", v)
551 }
552}
553
554func TestPassFilterOnlyTouchesRoutesExtraAccepts(t *testing.T) {
555 s := NewDirStore(t.TempDir())
556 ctx := context.Background()
557 priv, pub := testKeys(t)
558 opt := t.TempDir()
559 reg := testRegistry(t)
560
561 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
562 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
563 t.Fatalf("Promote canary/x: %v", err)
564 }
565 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
566 t.Fatalf("Promote stable: %v", err)
567 }
568 addRoute(t, reg, "canary.example.com", "myapp", "canary/x", false)
569 addRoute(t, reg, "stable.example.com", "myapp", "stable", false)
570
571 canaryOnly := func(rt registry.Route) bool { return rt.Channel == "canary/x" }
572 restart := func(string) error { return nil }
573 if err := PassFilter(ctx, reg, s, pub, opt, restart, canaryOnly); err != nil {
574 t.Fatalf("PassFilter: %v", err)
575 }
576
577 if v, _ := LiveVersion(opt, "canary.example.com"); v != "v1" {
578 t.Errorf("canary.example.com live = %q, want v1 (extra accepts it)", v)
579 }
580 if v, _ := LiveVersion(opt, "stable.example.com"); v != "" {
581 t.Errorf("stable.example.com live = %q, want unmoved — PassFilter must not touch a route extra rejects", v)
582 }
583}
584
585// WithPointerGate (S3 change-index design §5): a group the gate skips is out
586// of the pass, its pointer unread; every other group is read as before and
587// its outcome reported once.
588func TestPassFilterPointerGate(t *testing.T) {
589 s := NewDirStore(t.TempDir())
590 ctx := context.Background()
591 priv, pub := testKeys(t)
592 opt := t.TempDir()
593 reg := testRegistry(t)
594
595 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
596 for _, ch := range []string{"canary/x", "stable"} {
597 if err := Promote(ctx, s, priv, "bab", "myapp", ch, "v1"); err != nil {
598 t.Fatalf("Promote %s: %v", ch, err)
599 }
600 }
601 addRoute(t, reg, "canary.example.com", "myapp", "canary/x", false)
602 addRoute(t, reg, "stable.example.com", "myapp", "stable", false)
603 addRoute(t, reg, "beta.example.com", "myapp", "beta", false) // nothing promoted
604
605 asked := map[string][]string{}
606 read := map[string]string{}
607 gate := func(acct, app, channel string, routes []registry.Route) (bool, func(*ChannelPointer, error)) {
608 for _, rt := range routes {
609 asked[channel] = append(asked[channel], rt.Host)
610 }
611 if channel == "canary/x" {
612 return true, nil
613 }
614 return false, func(ptr *ChannelPointer, err error) {
615 switch {
616 case err != nil:
617 read[channel] += "error;"
618 default:
619 read[channel] += ptr.Version + ";"
620 }
621 }
622 }
623 if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, nil, WithPointerGate(gate)); err != nil {
624 t.Fatalf("PassFilter: %v", err)
625 }
626 want := map[string][]string{"canary/x": {"canary.example.com"}, "stable": {"stable.example.com"}, "beta": {"beta.example.com"}}
627 if len(asked) != len(want) {
628 t.Fatalf("the gate was asked about %v, want %v", asked, want)
629 }
630 for ch, hosts := range want {
631 if !slices.Equal(asked[ch], hosts) {
632 t.Fatalf("the gate was asked about %s with %v, want %v", ch, asked[ch], hosts)
633 }
634 }
635 if v, _ := LiveVersion(opt, "canary.example.com"); v != "" {
636 t.Errorf("a skipped group adopted: canary.example.com live = %q", v)
637 }
638 if v, _ := LiveVersion(opt, "stable.example.com"); v != "v1" {
639 t.Errorf("stable.example.com live = %q, want v1", v)
640 }
641 if read["stable"] != "v1;" || read["beta"] != "error;" || read["canary/x"] != "" {
642 t.Errorf("read outcomes %v, want stable v1 once, beta's error once, nothing for the skipped group", read)
643 }
644}
645
646// writeBackdatedPointer signs and writes a channel pointer with an arbitrary
647// PromotedAt, bypassing Promote/signPointer's own "stamp now" behavior — the
648// only way to get an EXPIRED canary pointer into a store under test, since
649// every public promotion path stamps the real clock. Kept in this internal
650// test package (never exported) precisely because forging a pointer's lease
651// is not something production code should ever be able to do.
652func writeBackdatedPointer(t *testing.T, s Store, priv ed25519.PrivateKey, acct, app, channel, version string, promotedAt time.Time) {
653 t.Helper()
654 p := &ChannelPointer{Schema: 1, App: app, Channel: channel, Version: version, PromotedAt: promotedAt}
655 payload, err := json.MarshalIndent(p, "", " ")
656 if err != nil {
657 t.Fatalf("marshal backdated pointer: %v", err)
658 }
659 b64 := base64.StdEncoding.EncodeToString(payload)
660 sig := ed25519.Sign(priv, []byte(b64))
661 wrapper, err := json.MarshalIndent(signedWrapper{
662 Payload: b64, Sig: base64.StdEncoding.EncodeToString(sig),
663 }, "", " ")
664 if err != nil {
665 t.Fatalf("marshal backdated wrapper: %v", err)
666 }
667 if _, err := s.Put(context.Background(), ChannelKey(acct, app, channel), wrapper, ""); err != nil {
668 t.Fatalf("write backdated pointer: %v", err)
669 }
670}
671
672// TestPassAdoptsAnExpiredCanary pins spec §5's core promise: expiry is
673// VISIBILITY, not enforcement. A canary/* route whose pointer is well past
674// CanaryTTL must still adopt exactly like a fresh one — nothing about
675// expiry may block a repoint, a restart, or a rollback/promote (the same
676// signed-pointer plumbing every other channel uses; expiry checks nothing
677// about the ladder or the CAS path).
678func TestPassAdoptsAnExpiredCanary(t *testing.T) {
679 s := NewDirStore(t.TempDir())
680 ctx := context.Background()
681 priv, pub := testKeys(t)
682 opt := t.TempDir()
683 reg := testRegistry(t)
684
685 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
686 writeBackdatedPointer(t, s, priv, "bab", "myapp", "canary/x", "v1",
687 time.Now().Add(-(CanaryTTL + 30*24*time.Hour))) // long expired
688 addRoute(t, reg, "old-canary.example.com", "myapp", "canary/x", false)
689
690 var restarted []string
691 restart := func(host string) error { restarted = append(restarted, host); return nil }
692 if err := Pass(ctx, reg, s, pub, opt, restart); err != nil {
693 t.Fatalf("Pass: %v", err)
694 }
695
696 if v, _ := LiveVersion(opt, "old-canary.example.com"); v != "v1" {
697 t.Fatalf("an expired canary route did not adopt: live = %q, want v1", v)
698 }
699 if len(restarted) != 1 || restarted[0] != "old-canary.example.com" {
700 t.Fatalf("an expired canary route was not restarted: restarted = %v", restarted)
701 }
702 rt, err := reg.Get("old-canary.example.com")
703 if err != nil || rt == nil || rt.Version != "v1" {
704 t.Fatalf("registry version after adopting an expired canary = %+v, %v", rt, err)
705 }
706
707 // Promotion off (or onto) an expired canary channel is unaffected too —
708 // expiry is computed from PromotedAt at read time, never stored, so
709 // nothing about a pointer's own expiry state can gate a write to it.
710 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("build two"))
711 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil {
712 t.Fatalf("promoting a new version onto an expired canary channel: %v", err)
713 }
714 p, err := Channel(ctx, s, pub, "bab", "myapp", "canary/x")
715 if err != nil {
716 t.Fatalf("Channel after re-promoting an expired canary: %v", err)
717 }
718 if p.Version != "v2" {
719 t.Fatalf("channel = %q after promote, want v2", p.Version)
720 }
721 // And the re-promotion refreshed the lease — CanaryExpired.go's whole
722 // point: an actively-updated canary never expires.
723 if CanaryExpired(p, time.Now()) {
724 t.Error("re-promoting an expired canary must refresh its lease, not leave it expired")
725 }
726}
727
728// TestPassFilterDoesNotNagWithoutTheOption pins the fast-loop half of the
729// nag's scoping (2026-08-03 review): internal/host/agent.go's fastConvergeLoop
730// calls PassFilter every 2-3 seconds for its fast set, and NEVER
731// passes NagExpiredCanaries — so an expired canary that is sitting still
732// must produce ZERO nag lines from that path, not one every few seconds.
733// This is PassFilter called the way that loop calls it (without the nag
734// option) against the same expired fixture TestPassAdoptsAnExpiredCanary
735// uses.
736func TestPassFilterDoesNotNagWithoutTheOption(t *testing.T) {
737 s := NewDirStore(t.TempDir())
738 ctx := context.Background()
739 priv, pub := testKeys(t)
740 opt := t.TempDir()
741 reg := testRegistry(t)
742
743 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
744 writeBackdatedPointer(t, s, priv, "bab", "myapp", "canary/x", "v1",
745 time.Now().Add(-(CanaryTTL + 30*24*time.Hour)))
746 addRoute(t, reg, "old-canary.example.com", "myapp", "canary/x", false)
747
748 var buf bytes.Buffer
749 withCapturedStdout(t, &buf, func() {
750 if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
751 t.Fatalf("PassFilter: %v", err)
752 }
753 })
754 if strings.Contains(buf.String(), "expired") {
755 t.Errorf("PassFilter without NagExpiredCanaries printed a nag line:\n%s", buf.String())
756 }
757}
758
759// TestPassNagsOncePerExpiredCanaryRoute pins the root path's half: RunAdopt
760// (carlos-adopt.service, hourly) passes NagExpiredCanaries(os.Stdout) to
761// Pass, and that call must print ONE nag line PER EXPIRED CANARY ROUTE it
762// sees — not one per hourly pass regardless of how many routes follow the
763// expired channel. Three routes (two awake, one hibernating — the awake and
764// hibernating lists are walked separately in Pass/PassFilter, so this is
765// also the regression test for "hibernating routes get skipped" and
766// "a route is visited twice") on ONE expired canary channel must produce
767// exactly three lines, each naming its own host.
768//
769// (2026-08-03 review, MINOR 6: this test used to be named
770// TestPassNagsExactlyOnceWithTheOption and only ever exercised one route,
771// so "not once per host revisit" was asserted by name only, never proven.)
772func TestPassNagsOncePerExpiredCanaryRoute(t *testing.T) {
773 s := NewDirStore(t.TempDir())
774 ctx := context.Background()
775 priv, pub := testKeys(t)
776 opt := t.TempDir()
777 reg := testRegistry(t)
778
779 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
780 writeBackdatedPointer(t, s, priv, "bab", "myapp", "canary/x", "v1",
781 time.Now().Add(-(CanaryTTL + 30*24*time.Hour)))
782 addRoute(t, reg, "awake-a.example.com", "myapp", "canary/x", false)
783 addRoute(t, reg, "awake-b.example.com", "myapp", "canary/x", false)
784 addRoute(t, reg, "sleepy.example.com", "myapp", "canary/x", true)
785
786 var buf bytes.Buffer
787 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, NagExpiredCanaries(&buf)); err != nil {
788 t.Fatalf("Pass: %v", err)
789 }
790 out := buf.String()
791 if got := strings.Count(out, "expired"); got != 3 {
792 t.Errorf("nag lines containing \"expired\" = %d, want exactly 3 (one per route, awake and hibernating alike):\n%s", got, out)
793 }
794 for _, host := range []string{"awake-a.example.com", "awake-b.example.com", "sleepy.example.com"} {
795 if !strings.Contains(out, host) {
796 t.Errorf("nag output does not name %s:\n%s", host, out)
797 }
798 }
799}
800
801// TestNagReportsTimePastTTLNotTotalAge pins the 2026-08-03 review's
802// IMPORTANT 2 fix: the nag must report how long PAST CanaryTTL a canary is,
803// not its total age. A pointer promoted CanaryTTL+1h ago has been EXPIRED
804// for about an hour, not 169 hours (7 days 1 hour, its total age) — the
805// second number is real but answers a different question, and printing it
806// under "has been expired" is the bug the review caught. It must also
807// disagree with nothing the console shows for the same pointer
808// (internal/console/ladder_test.go's TestLadderExpiredCanaryIsBadgedWithAge
809// exercises the same release.CanaryAge/FormatCanaryAge pair).
810func TestNagReportsTimePastTTLNotTotalAge(t *testing.T) {
811 s := NewDirStore(t.TempDir())
812 ctx := context.Background()
813 priv, pub := testKeys(t)
814 opt := t.TempDir()
815 reg := testRegistry(t)
816
817 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
818 writeBackdatedPointer(t, s, priv, "bab", "myapp", "canary/x", "v1",
819 time.Now().Add(-(CanaryTTL + time.Hour)))
820 addRoute(t, reg, "old-canary.example.com", "myapp", "canary/x", false)
821
822 var buf bytes.Buffer
823 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, NagExpiredCanaries(&buf)); err != nil {
824 t.Fatalf("Pass: %v", err)
825 }
826 out := buf.String()
827 if !strings.Contains(out, "1h") {
828 t.Errorf("nag does not say 1h (time past TTL) for a canary expired ~1h ago:\n%s", out)
829 }
830 if strings.Contains(out, "169h") {
831 t.Errorf("nag reports total age (169h) instead of time past TTL:\n%s", out)
832 }
833}
834
835// TestPassDoesNotNagAFreshCanaryEvenWithTheOption: the option opts a CALL
836// into nagging; it does not turn every canary into a nag target. A fresh
837// canary must stay silent whether or not NagExpiredCanaries is set.
838func TestPassDoesNotNagAFreshCanaryEvenWithTheOption(t *testing.T) {
839 s := NewDirStore(t.TempDir())
840 ctx := context.Background()
841 priv, pub := testKeys(t)
842 opt := t.TempDir()
843 reg := testRegistry(t)
844
845 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
846 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
847 t.Fatalf("Promote: %v", err)
848 }
849 addRoute(t, reg, "fresh-canary.example.com", "myapp", "canary/x", false)
850
851 var buf bytes.Buffer
852 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, NagExpiredCanaries(&buf)); err != nil {
853 t.Fatalf("Pass: %v", err)
854 }
855 if strings.Contains(buf.String(), "expired") {
856 t.Errorf("a freshly-promoted canary produced a nag line:\n%s", buf.String())
857 }
858}
859
860// isCanaryTestRoute mirrors host.isCanaryRoute (unexported, different
861// package) for the one fast-loop-shaped test above that needs a canary-only
862// filter without importing internal/host.
863func isCanaryTestRoute(rt registry.Route) bool { return strings.HasPrefix(rt.Channel, "canary/") }
864
865// withCapturedStdout redirects os.Stdout to buf for the duration of fn —
866// used ONLY to prove PassFilter's default (no NagExpiredCanaries) path
867// prints nothing canary-related even via the package's ordinary fmt.Printf
868// progress lines, which is the honest thing to assert against (adoptHost's
869// other progress lines still go to os.Stdout; NagExpiredCanaries is what
870// redirects the NAG line specifically to an injected writer instead).
871func withCapturedStdout(t *testing.T, buf *bytes.Buffer, fn func()) {
872 t.Helper()
873 r, w, err := os.Pipe()
874 if err != nil {
875 t.Fatalf("os.Pipe: %v", err)
876 }
877 orig := os.Stdout
878 os.Stdout = w
879 defer func() { os.Stdout = orig }()
880
881 done := make(chan struct{})
882 go func() {
883 buf.ReadFrom(r)
884 close(done)
885 }()
886
887 fn()
888 w.Close()
889 <-done
890}
891
892// adoptRefusesManifest runs the whole fail-closed scenario for a stored
893// manifest whose capability block this binary will not accept: a box already
894// serving v1, a channel that has moved to v2, and v2's manifest bytes written
895// DIRECTLY to the store (bypassing Ship, which is exactly what a newer
896// platform binary would leave behind). It asserts the four things that make
897// the refusal safe — the live symlink unmoved, nothing staged, no restart,
898// the registry unchanged — and returns what the pass printed.
899//
900// Shared by the two tests below because the two refusals arrive by DIFFERENT
901// routes and both have to land here: one from caps.Set's strict decoder
902// (json.Unmarshal fails), one from ValidCapabilities (the block decodes
903// perfectly and is only then refused). A gate that caught the first and not
904// the second would look correct in a single test.
905func adoptRefusesManifest(t *testing.T, capsJSON string) string {
906 t.Helper()
907 s := NewDirStore(t.TempDir())
908 ctx := context.Background()
909 priv, pub := testKeys(t)
910 opt := t.TempDir()
911 reg := testRegistry(t)
912
913 // The box is up and running on v1.
914 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("good"))
915 if err := StageArtifact(opt, "bab", "v1", "myapp", []byte("good")); err != nil {
916 t.Fatal(err)
917 }
918 addRoute(t, reg, "h.example.com", "myapp", "canary/x", false)
919 if err := PointLive(opt, "h.example.com", "bab", "v1", "myapp"); err != nil {
920 t.Fatal(err)
921 }
922
923 // v2 ships and the channel moves onto it while it is still a manifest this
924 // binary understands...
925 body := []byte("newer")
926 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, body)
927 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil {
928 t.Fatalf("Promote v2: %v", err)
929 }
930 // ...and is then replaced by the bytes a NEWER binary would have written:
931 // same artifacts, same sha, plus a capability block this one refuses.
932 key := ManifestKey("bab", "myapp", "v2")
933 _, etag, err := s.Get(ctx, key)
934 if err != nil {
935 t.Fatalf("read v2 manifest: %v", err)
936 }
937 future := []byte(`{
938 "schema": 1,
939 "app": "myapp",
940 "version": "v2",
941 "kind": "binary",
942 "artifacts": { "` + adoptPlatform + `": { "file": "` + adoptPlatform + `", "sha256": "` + etagOf(body) + `", "size": ` + strconv.Itoa(len(body)) + ` } },
943 "capabilities": ` + capsJSON + `
944}`)
945 if _, err := s.Put(ctx, key, future, etag); err != nil {
946 t.Fatalf("replace v2 manifest: %v", err)
947 }
948
949 var out bytes.Buffer
950 var restarted []string
951 withCapturedStdout(t, &out, func() {
952 if err := Pass(ctx, reg, s, pub, opt, func(h string) error {
953 restarted = append(restarted, h)
954 return nil
955 }); err != nil {
956 t.Fatalf("Pass: %v", err)
957 }
958 })
959
960 if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
961 t.Fatalf("live = %q, want v1 — the box must keep serving what it has", v)
962 }
963 if _, err := os.Stat(VersionPath(opt, "bab", "v2", "myapp")); !os.IsNotExist(err) {
964 t.Fatalf("v2 was staged (stat err = %v); a refused version must never reach disk", err)
965 }
966 if len(restarted) != 0 {
967 t.Fatalf("restarted = %v, want none", restarted)
968 }
969 if rt, _ := reg.Get("h.example.com"); rt == nil || rt.Version == "v2" {
970 t.Fatalf("registry recorded v2 as adopted: %+v", rt)
971 }
972 return out.String()
973}
974
975// TestAdoptRefusesTheVersionWhenAManifestDeclaresAnUnknownName is the
976// fail-closed direction, and it is the test that matters most in this task.
977//
978// The box's answer to a capability name from a vocabulary it does not have
979// must be to refuse the VERSION: nothing staged, the live symlink unmoved,
980// one printed line naming the capability and the fix. The box keeps serving
981// what it has, which for a daemon is the safe direction; the alternative —
982// materializing a unit that is missing something the app says it needs, and
983// looking healthy — is the config-that-lies failure mode this whole design
984// exists to avoid.
985//
986// This refusal comes from caps.Set's strict DECODER: json.Unmarshal never
987// completes, so the manifest never becomes a value at all.
988func TestAdoptRefusesTheVersionWhenAManifestDeclaresAnUnknownName(t *testing.T) {
989 line := adoptRefusesManifest(t, `{ "v": 1, "daemon": true, "gpu": true }`)
990 if !strings.Contains(line, "gpu") {
991 t.Fatalf("printed %q, want a line naming the capability", line)
992 }
993 if !strings.Contains(line, "update the platform binary") {
994 t.Fatalf("printed %q, want a line naming the fix", line)
995 }
996}
997
998// TestAdoptRefusesTheVersionWhenAManifestIsSemanticallyInvalid closes the
999// other half of the gate, and it is the half a decoder-only test would miss:
1000// this block decodes PERFECTLY — every name is known, "v" is 1, the shape is
1001// right — and is refused only by ValidCapabilities, on a port that is not a
1002// capability at all. It is reached because validateManifest runs on every
1003// GetManifest, not only at ship; delete that one line and this test goes red
1004// while the unknown-name test above stays green.
1005func TestAdoptRefusesTheVersionWhenAManifestIsSemanticallyInvalid(t *testing.T) {
1006 line := adoptRefusesManifest(t, `{ "v": 1, "daemon": true, "ports": [ { "port": 8080 } ] }`)
1007 if !strings.Contains(line, "8080") {
1008 t.Fatalf("printed %q, want a line naming the offending port", line)
1009 }
1010 if !strings.Contains(line, "capabilities") {
1011 t.Fatalf("printed %q, want a line naming what was refused", line)
1012 }
1013}
1014
1015// TestEnsureStagedWritesTheSiblingManifest pins the fresh-download half of
1016// Task 3: after ensureStaged fetches and stages a version for the first
1017// time, ReadStagedManifest returns a manifest whose App/Version match and
1018// whose Capabilities round-trip byte for byte — this is what lets the
1019// converge pass read a running version's capabilities off disk, with no
1020// bucket round-trip on every tick.
1021func TestEnsureStagedWritesTheSiblingManifest(t *testing.T) {
1022 s := NewDirStore(t.TempDir())
1023 ctx := context.Background()
1024 opt := t.TempDir()
1025
1026 m := Manifest{
1027 App: "keymail", Version: "v1abc", Kind: "binary",
1028 Artifacts: map[string]Artifact{adoptPlatform: {}},
1029 Capabilities: &caps.Set{V: 1, Command: "relay", Daemon: true, RuntimeDir: "keymail"},
1030 }
1031 if err := Ship(ctx, s, "bab", m, map[string][]byte{adoptPlatform: []byte("relay binary")}); err != nil {
1032 t.Fatalf("Ship: %v", err)
1033 }
1034
1035 inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "keymail.example.com")
1036 if err != nil {
1037 t.Fatalf("ensureStaged: %v", err)
1038 }
1039 // The manifest ensureStaged HAS IN HAND is what adoptHost's converge
1040 // builds its plan from (Task 7's first pin) — not a re-read of the
1041 // sibling it just wrote.
1042 if inHand == nil || inHand.Capabilities == nil || inHand.Capabilities.Command != "relay" {
1043 t.Fatalf("ensureStaged returned %+v, want the fetched manifest with its capability block", inHand)
1044 }
1045
1046 got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail")
1047 if err != nil {
1048 t.Fatalf("ReadStagedManifest: %v", err)
1049 }
1050 if got == nil || got.App != "keymail" || got.Version != "v1abc" {
1051 t.Fatalf("got = %+v, want App/Version to match", got)
1052 }
1053 if got.Capabilities == nil || got.Capabilities.Command != "relay" || !got.Capabilities.Daemon || got.Capabilities.RuntimeDir != "keymail" {
1054 t.Fatalf("Capabilities did not round-trip: %+v", got.Capabilities)
1055 }
1056}
1057
1058// TestEnsureStagedBackfillsASiblingForAnAlreadyStagedBinary pins the named
1059// finding: a box that staged a version's binary before this feature existed
1060// (or before the sibling write otherwise happened) must have the sibling
1061// back-filled on the next pass WITHOUT the artifact being re-downloaded —
1062// otherwise the converge pass is permanently blind on exactly the versions
1063// already running, since a staged binary never touches ensureStaged's
1064// download branch again on its own.
1065func TestEnsureStagedBackfillsASiblingForAnAlreadyStagedBinary(t *testing.T) {
1066 s := NewDirStore(t.TempDir())
1067 ctx := context.Background()
1068 opt := t.TempDir()
1069
1070 m := Manifest{
1071 App: "keymail", Version: "v1abc", Kind: "binary",
1072 Artifacts: map[string]Artifact{adoptPlatform: {}},
1073 Capabilities: &caps.Set{V: 1, Daemon: true, RuntimeDir: "keymail"},
1074 }
1075 if err := Ship(ctx, s, "bab", m, map[string][]byte{adoptPlatform: []byte("relay binary")}); err != nil {
1076 t.Fatalf("Ship: %v", err)
1077 }
1078
1079 // Stage the binary BY HAND, as a pre-Task-3 box would have — no sibling.
1080 if err := StageArtifact(opt, "bab", "v1abc", "keymail", []byte("relay binary")); err != nil {
1081 t.Fatalf("StageArtifact: %v", err)
1082 }
1083 binPath := VersionPath(opt, "bab", "v1abc", "keymail")
1084 beforeInfo, err := os.Stat(binPath)
1085 if err != nil {
1086 t.Fatalf("stat before backfill: %v", err)
1087 }
1088 beforeBytes, err := os.ReadFile(binPath)
1089 if err != nil {
1090 t.Fatalf("read before backfill: %v", err)
1091 }
1092 if m2, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail"); err != nil || m2 != nil {
1093 t.Fatalf("sibling should be absent before backfill: %+v, %v", m2, err)
1094 }
1095
1096 inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "keymail.example.com")
1097 if err != nil {
1098 t.Fatalf("ensureStaged (backfill): %v", err)
1099 }
1100 if inHand == nil || inHand.Capabilities == nil || !inHand.Capabilities.Daemon {
1101 t.Fatalf("back-fill returned %+v, want the fetched manifest in hand", inHand)
1102 }
1103
1104 got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail")
1105 if err != nil {
1106 t.Fatalf("ReadStagedManifest after backfill: %v", err)
1107 }
1108 if got == nil || got.Capabilities == nil || !got.Capabilities.Daemon || got.Capabilities.RuntimeDir != "keymail" {
1109 t.Fatalf("backfilled sibling missing capabilities: %+v", got)
1110 }
1111
1112 afterInfo, err := os.Stat(binPath)
1113 if err != nil {
1114 t.Fatalf("stat after backfill: %v", err)
1115 }
1116 afterBytes, err := os.ReadFile(binPath)
1117 if err != nil {
1118 t.Fatalf("read after backfill: %v", err)
1119 }
1120 if !beforeInfo.ModTime().Equal(afterInfo.ModTime()) {
1121 t.Fatalf("backfill re-touched the binary: mtime %v -> %v", beforeInfo.ModTime(), afterInfo.ModTime())
1122 }
1123 if string(beforeBytes) != string(afterBytes) {
1124 t.Fatalf("backfill re-downloaded the binary bytes")
1125 }
1126}
1127
1128// TestEnsureStagedSiblingWriteFailureIsNotFatalToStagingTheBinary is
1129// killing test 2 from the 2026-08-03 review: a mutant that makes the
1130// marshal/StageManifest failure at the end of ensureStaged's fresh-download
1131// path fatal must be caught. The binary must stage successfully even when
1132// its sibling cannot be written — a missing sibling is the safe leave-alone
1133// state, and backfillManifest gets another chance at it every later pass.
1134//
1135// StageManifest's own write is blocked by pre-creating its final rename
1136// target AS A DIRECTORY — rename then fails to replace it, without ever
1137// touching StageArtifact's own (different) binary path.
1138func TestEnsureStagedSiblingWriteFailureIsNotFatalToStagingTheBinary(t *testing.T) {
1139 s := NewDirStore(t.TempDir())
1140 ctx := context.Background()
1141 opt := t.TempDir()
1142
1143 m := Manifest{
1144 App: "keymail", Version: "v1abc", Kind: "binary",
1145 Artifacts: map[string]Artifact{adoptPlatform: {}},
1146 Capabilities: &caps.Set{V: 1, Daemon: true, RuntimeDir: "keymail"},
1147 }
1148 if err := Ship(ctx, s, "bab", m, map[string][]byte{adoptPlatform: []byte("relay binary")}); err != nil {
1149 t.Fatalf("Ship: %v", err)
1150 }
1151
1152 tmpBlock := StagedManifestPath(opt, "bab", "v1abc", "keymail")
1153 if err := os.MkdirAll(tmpBlock, 0o755); err != nil {
1154 t.Fatalf("pre-create tmp block: %v", err)
1155 }
1156
1157 inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "h.example.com")
1158 if err != nil {
1159 t.Fatalf("ensureStaged returned an error for a failed SIBLING write: %v — the binary staged fine and that must not be fatal", err)
1160 }
1161 // Task 7's first pin, in its sharpest form: the sibling write FAILED, so
1162 // a converge that re-read the sibling would see (nil, nil) and leave the
1163 // previous version's drop-ins in place under a new binary. The manifest
1164 // this function returns is the fetched one and is unaffected.
1165 if inHand == nil || inHand.Capabilities == nil || !inHand.Capabilities.Daemon {
1166 t.Fatalf("ensureStaged returned %+v after a failed sibling write, want the fetched manifest still in hand", inHand)
1167 }
1168
1169 body, err := os.ReadFile(VersionPath(opt, "bab", "v1abc", "keymail"))
1170 if err != nil || string(body) != "relay binary" {
1171 t.Fatalf("binary did not stage despite the sibling write failing: %q, %v", body, err)
1172 }
1173 if got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail"); err == nil || got != nil {
1174 t.Fatalf("sibling = %+v, %v; want blocked (the write genuinely failed)", got, err)
1175 }
1176}
1177
1178// TestEnsureStagedBackfillFetchFailureIsNotFatal is killing test 3 / I2's
1179// regression test (2026-08-03 review): ensureStaged's back-fill fetch (a
1180// GetManifest for an ALREADY-STAGED binary) must not be fatal to adoption.
1181// The store here has no release shipped at all, so GetManifest is
1182// guaranteed to fail — simulating a bucket hiccup for a version that is
1183// already running and needs no adoption. A mutant that makes this fetch
1184// failure fatal again would hard-fail (and, on a canary route's 2-3-second
1185// fast loop, journal-flood) adoption of a binary that doesn't need it.
1186func TestEnsureStagedBackfillFetchFailureIsNotFatal(t *testing.T) {
1187 s := NewDirStore(t.TempDir()) // empty — nothing ever shipped
1188 ctx := context.Background()
1189 opt := t.TempDir()
1190
1191 if err := StageArtifact(opt, "bab", "v1abc", "keymail", []byte("relay binary")); err != nil {
1192 t.Fatalf("StageArtifact: %v", err)
1193 }
1194
1195 inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "h.example.com")
1196 if err != nil {
1197 t.Fatalf("ensureStaged returned an error for an already-staged binary's failed back-fill fetch: %v — adoption of an already-running version must proceed", err)
1198 }
1199 // This is the ONE path with no manifest to give. It stays (nil, nil) so
1200 // ordinary adoption proceeds; a capability-armed pass turns that nil into
1201 // a refusal of the version rather than a silent leave-alone — see
1202 // TestArmedPassRefusesAVersionWhoseManifestItCouldNotObtain.
1203 if inHand != nil {
1204 t.Fatalf("ensureStaged returned %+v, want nil (no manifest could be obtained)", inHand)
1205 }
1206
1207 if got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail"); err != nil || got != nil {
1208 t.Fatalf("sibling = %+v, %v; want absent (the fetch failed, nothing to write)", got, err)
1209 }
1210}
1211
1212// TestPassBackfillsTheLiveVersionsSiblingOnAnUnchangedPass pins C1, the
1213// critical finding from the 2026-08-03 review: ensureStaged's own back-fill
1214// only runs from the call inside adoptHost that sits BEHIND the cur == ver
1215// short-circuit, so a version that is already live — with no repoint due
1216// this pass — never reaches it on its own. Since the migration path is
1217// entirely "a box's CURRENT version was staged before this feature
1218// existed," without a separate per-pass repair call the sibling for
1219// exactly the version actually running would stay missing forever. This
1220// test exercises the real call site (Pass, not ensureStaged directly): the
1221// route is already on v1abc, nothing repoints, and the sibling must still
1222// appear.
1223func TestPassBackfillsTheLiveVersionsSiblingOnAnUnchangedPass(t *testing.T) {
1224 s := NewDirStore(t.TempDir())
1225 ctx := context.Background()
1226 priv, pub := testKeys(t)
1227 opt := t.TempDir()
1228 reg := testRegistry(t)
1229
1230 m := Manifest{
1231 App: "keymail", Version: "v1abc", Kind: "binary",
1232 Artifacts: map[string]Artifact{adoptPlatform: {}},
1233 Capabilities: &caps.Set{V: 1, Daemon: true, RuntimeDir: "keymail"},
1234 }
1235 if err := Ship(ctx, s, "bab", m, map[string][]byte{adoptPlatform: []byte("relay binary")}); err != nil {
1236 t.Fatalf("Ship: %v", err)
1237 }
1238 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1abc"); err != nil {
1239 t.Fatalf("Promote: %v", err)
1240 }
1241
1242 // The binary is staged and pointed live BY HAND, exactly as a
1243 // pre-Task-3 box would be — no sibling, and no version change due this
1244 // pass (the route is already on v1abc).
1245 if err := StageArtifact(opt, "bab", "v1abc", "keymail", []byte("relay binary")); err != nil {
1246 t.Fatalf("StageArtifact: %v", err)
1247 }
1248 addRoute(t, reg, "h.example.com", "keymail", "canary/x", false)
1249 if err := PointLive(opt, "h.example.com", "bab", "v1abc", "keymail"); err != nil {
1250 t.Fatalf("PointLive: %v", err)
1251 }
1252
1253 if m2, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail"); err != nil || m2 != nil {
1254 t.Fatalf("sibling should be absent before the pass: %+v, %v", m2, err)
1255 }
1256
1257 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
1258 t.Fatalf("Pass: %v", err)
1259 }
1260
1261 got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail")
1262 if err != nil {
1263 t.Fatalf("ReadStagedManifest after Pass: %v", err)
1264 }
1265 if got == nil || got.Capabilities == nil || !got.Capabilities.Daemon {
1266 t.Fatalf("Pass did not back-fill the live version's sibling: %+v", got)
1267 }
1268 if v, _ := LiveVersion(opt, "h.example.com"); v != "v1abc" {
1269 t.Fatalf("live version moved unexpectedly: %q", v)
1270 }
1271}
1272
1273// ---- Task 7: the capability converge inside adoptHost -----------------------
1274
1275// capsRecorder is a tiny in-memory systemd for the capability tests in this
1276// package, plus the one thing internal/caps's own fake cannot observe: WHERE
1277// THE LIVE SYMLINK POINTED at the moment each verb ran. That is how the
1278// ordering rule — converge before PointLive — is asserted as a fact about
1279// this pass rather than as a reading of the code.
1280type capsRecorder struct {
1281 opt, host string
1282 calls []string
1283 live []string // live[i] is LiveVersion when calls[i] ran
1284 liveAt map[string]string // verb+unit -> LiveVersion at its FIRST call
1285 active map[string]bool
1286 enabled map[string]bool
1287 fail map[string]error // "verb unit" -> error
1288 onCall func(call string) // optional: fires after a call is recorded
1289}
1290
1291func newCapsRecorder(opt, host string) *capsRecorder {
1292 return &capsRecorder{
1293 opt: opt, host: host,
1294 liveAt: map[string]string{},
1295 active: map[string]bool{},
1296 enabled: map[string]bool{},
1297 fail: map[string]error{},
1298 }
1299}
1300
1301func (r *capsRecorder) run(args ...string) error {
1302 call := strings.Join(args, " ")
1303 v, _ := LiveVersion(r.opt, r.host)
1304 r.calls = append(r.calls, call)
1305 r.live = append(r.live, v)
1306 if _, seen := r.liveAt[call]; !seen {
1307 r.liveAt[call] = v
1308 }
1309 if r.onCall != nil {
1310 r.onCall(call)
1311 }
1312 if err := r.fail[call]; err != nil {
1313 return err
1314 }
1315 if len(args) >= 2 {
1316 switch args[0] {
1317 case "start", "restart":
1318 r.active[args[1]] = true
1319 case "stop":
1320 r.active[args[1]] = false
1321 case "enable":
1322 r.enabled[args[1]] = true
1323 }
1324 }
1325 return nil
1326}
1327
1328func (r *capsRecorder) query(args ...string) bool {
1329 if len(args) < 2 {
1330 return false
1331 }
1332 switch args[0] {
1333 case "is-active":
1334 return r.active[args[1]]
1335 case "is-enabled":
1336 return r.enabled[args[1]]
1337 }
1338 return false
1339}
1340
1341func (r *capsRecorder) called(call string) bool {
1342 for _, c := range r.calls {
1343 if c == call {
1344 return true
1345 }
1346 }
1347 return false
1348}
1349
1350// lastLiveAt is where the live symlink pointed at the LAST occurrence of a
1351// call. liveAt records only the first, which is blind to everything a
1352// rollback does after the same verb has already run once — the ordering
1353// inside the rollback (symlink first, drop-ins second) is exactly what needs
1354// pinning, so it needs the last one.
1355func (r *capsRecorder) lastLiveAt(call string) (string, bool) {
1356 for i := len(r.calls) - 1; i >= 0; i-- {
1357 if r.calls[i] == call {
1358 return r.live[i], true
1359 }
1360 }
1361 return "", false
1362}
1363
1364// serviceCycles is every verb that STARTS the service unit — the count that
1365// says whether a version change cycled the app once or twice.
1366func (r *capsRecorder) serviceCycles(host string) (n int, lastLive string) {
1367 for i, c := range r.calls {
1368 if c == "start "+caps.ServiceUnit(host) || c == "restart "+caps.ServiceUnit(host) {
1369 n++
1370 lastLive = r.live[i]
1371 }
1372 }
1373 return n, lastLive
1374}
1375
1376// reset clears the call log between passes.
1377func (r *capsRecorder) reset() {
1378 r.calls, r.live = nil, nil
1379 r.liveAt = map[string]string{}
1380}
1381
1382// capsTestOps wires every caps seam at a temp dir, so a Pass in this package
1383// touches no systemd, no /etc, no /run and no /var/lib. Logf is left nil on
1384// purpose: converge's own lines then go to os.Stdout with every other adopt
1385// line, which is what withCapturedStdout asserts against.
1386func capsTestOps(t *testing.T, r *capsRecorder) (caps.Ops, string) {
1387 t.Helper()
1388 etc := t.TempDir()
1389 return caps.Ops{
1390 EtcDir: etc,
1391 StateRoot: t.TempDir(),
1392 RunDir: t.TempDir(),
1393 Systemctl: r.run,
1394 Query: r.query,
1395 EnsureUser: func(string) error { return nil },
1396 }, etc
1397}
1398
1399// addCapRoute adds a route with a backing and a granted capability column —
1400// addRoute plus the two columns Task 7 reads.
1401func addCapRoute(t *testing.T, reg *registry.Registry, host, app, channel, backing, grants string, hibernate, platform bool) {
1402 t.Helper()
1403 if err := reg.Upsert(registry.Route{
1404 Host: host, Kind: "instance", App: app, Channel: channel,
1405 Hibernate: hibernate, Backing: backing, Caps: grants, Platform: platform,
1406 }); err != nil {
1407 t.Fatalf("upsert %s: %v", host, err)
1408 }
1409}
1410
1411// shipCapRelease is shipRelease with a capability block.
1412func shipCapRelease(t *testing.T, s Store, acct, app, version string, set *caps.Set, body []byte) {
1413 t.Helper()
1414 m := Manifest{
1415 App: app, Version: version, Kind: "binary",
1416 Artifacts: map[string]Artifact{adoptPlatform: {}},
1417 Capabilities: set,
1418 }
1419 if err := Ship(context.Background(), s, acct, m, map[string][]byte{adoptPlatform: body}); err != nil {
1420 t.Fatalf("Ship %s/%s: %v", app, version, err)
1421 }
1422}
1423
1424func relaySet() *caps.Set {
1425 return &caps.Set{
1426 V: 1, Command: "relay", Daemon: true, RuntimeDir: "keymail",
1427 Ports: []caps.Port{{Port: 25, Name: "smtp"}},
1428 }
1429}
1430
1431// TestAdoptConvergesCapabilitiesBeforePointLive is the ordering rule of spec
1432// §4d, asserted where it can actually fail: the drop-ins are written and
1433// daemon-reloaded while the live symlink still names the OLD version, and the
1434// daemon's restart happens after it names the new one. Converge after
1435// PointLive would leave a window where the new binary is live under the old
1436// unit shape; converge without a reload would leave bytes systemd never read.
1437func TestAdoptConvergesCapabilitiesBeforePointLive(t *testing.T) {
1438 s := NewDirStore(t.TempDir())
1439 ctx := context.Background()
1440 priv, pub := testKeys(t)
1441 opt := t.TempDir()
1442 reg := testRegistry(t)
1443 host := "keymail.dev"
1444
1445 // The box is up on v1, which declares nothing.
1446 shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old relay"))
1447 if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old relay")); err != nil {
1448 t.Fatal(err)
1449 }
1450 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
1451 if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil {
1452 t.Fatal(err)
1453 }
1454
1455 set := relaySet()
1456 shipCapRelease(t, s, "bab", "keymail", "v2", set, []byte("new relay"))
1457 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
1458 t.Fatalf("Promote v2: %v", err)
1459 }
1460
1461 rec := newCapsRecorder(opt, host)
1462 o, etc := capsTestOps(t, rec)
1463 var restarted []string
1464 restart := func(h string) error { restarted = append(restarted, h); return nil }
1465 if err := Pass(ctx, reg, s, pub, opt, restart, WithCapabilities(o)); err != nil {
1466 t.Fatalf("Pass: %v", err)
1467 }
1468
1469 if v, _ := LiveVersion(opt, host); v != "v2" {
1470 t.Fatalf("live = %q, want v2", v)
1471 }
1472 if v, seen := rec.liveAt["daemon-reload"]; !seen || v != "v1" {
1473 t.Fatalf("daemon-reload observed live=%q (seen=%v), want v1 — the drop-ins must be loaded BEFORE the symlink moves", v, seen)
1474 }
1475
1476 // The bytes are the renderer's, byte for byte — this test pins the
1477 // wiring, not a second copy of the drop-in format.
1478 g := caps.Grants{Ports: map[int]bool{25: true}}
1479 svcPath, sockPath := caps.DropInPaths(etc, host)
1480 wantSvc, err := caps.RenderService("keymail", set, g)
1481 if err != nil {
1482 t.Fatalf("RenderService: %v", err)
1483 }
1484 gotSvc, err := os.ReadFile(svcPath)
1485 if err != nil || !bytes.Equal(gotSvc, wantSvc) {
1486 t.Fatalf("service drop-in = %q (%v), want %q", gotSvc, err, wantSvc)
1487 }
1488 wantSock, err := caps.RenderSocket(set, g)
1489 if err != nil {
1490 t.Fatalf("RenderSocket: %v", err)
1491 }
1492 gotSock, err := os.ReadFile(sockPath)
1493 if err != nil || !bytes.Equal(gotSock, wantSock) {
1494 t.Fatalf("socket drop-in = %q (%v), want %q", gotSock, err, wantSock)
1495 }
1496
1497 // A converged DAEMON's restart is caps.Restart, not the caller's
1498 // try-restart callback — that is what makes its failure real enough to
1499 // roll back (restartRoute swallows every error it sees).
1500 if len(restarted) != 0 {
1501 t.Fatalf("restart callback called for %v; a converged daemon's restart is caps.Restart's", restarted)
1502 }
1503 svcRestart := "restart " + caps.ServiceUnit(host)
1504 if v, seen := rec.liveAt[svcRestart]; !seen || v != "v2" {
1505 t.Fatalf("%q observed live=%q (seen=%v), want v2 — the daemon restarts onto the NEW binary", svcRestart, v, seen)
1506 }
1507 // And exactly ONCE. Converge runs before the symlink moves, so any start
1508 // it performs itself boots the OLD binary under the NEW unit shape — a
1509 // combination nobody shipped — and then the caller cycles the app a
1510 // second time. Plan.DeferRestart is what collapses that to one cycle.
1511 if n, last := rec.serviceCycles(host); n != 1 || last != "v2" {
1512 t.Fatalf("service started/restarted %d time(s), last at live=%q; want exactly 1, at v2 (calls: %v)", n, last, rec.calls)
1513 }
1514}
1515
1516// TestRestartFailureRestoresBothTheSymlinkAndTheDropIns is the rollback that
1517// keeps a bad build off a mail server: when the daemon will not come back on
1518// the new version, the box must end the pass exactly where it started — old
1519// symlink, old unit text, the version poisoned for the rest of the pass.
1520// Restoring one without the other is the divergence this pairing exists to
1521// prevent (bytes that say :465 under a binary that never asked for it).
1522func TestRestartFailureRestoresBothTheSymlinkAndTheDropIns(t *testing.T) {
1523 s := NewDirStore(t.TempDir())
1524 ctx := context.Background()
1525 priv, pub := testKeys(t)
1526 opt := t.TempDir()
1527 reg := testRegistry(t)
1528 host := "a.example.com"
1529
1530 v1 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}}}
1531 v2 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}, {Port: 465}}}
1532 shipCapRelease(t, s, "bab", "keymail", "v1", v1, []byte("good relay"))
1533 shipCapRelease(t, s, "bab", "keymail", "v2", v2, []byte("bad relay"))
1534 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
1535 t.Fatalf("Promote v1: %v", err)
1536 }
1537 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25,port:465", false, false)
1538 // b is exec-backed (materializes nothing) and exists only to prove the
1539 // poison: it must never be moved onto the version a's restart rejected.
1540 addCapRoute(t, reg, "b.example.com", "keymail", "canary/x", "", "", false, false)
1541
1542 rec := newCapsRecorder(opt, host)
1543 o, etc := capsTestOps(t, rec)
1544 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1545 t.Fatalf("Pass v1: %v", err)
1546 }
1547 svcPath, sockPath := caps.DropInPaths(etc, host)
1548 wantSvc, err := os.ReadFile(svcPath)
1549 if err != nil {
1550 t.Fatalf("read converged service drop-in: %v", err)
1551 }
1552 wantSock, err := os.ReadFile(sockPath)
1553 if err != nil {
1554 t.Fatalf("read converged socket drop-in: %v", err)
1555 }
1556 if !strings.Contains(string(wantSock), "ListenStream=25") || strings.Contains(string(wantSock), "465") {
1557 t.Fatalf("v1 socket drop-in = %q, want :25 only", wantSock)
1558 }
1559
1560 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
1561 t.Fatalf("Promote v2: %v", err)
1562 }
1563 rec.fail["restart "+caps.ServiceUnit(host)] = errors.New("relay failed to start")
1564
1565 var out bytes.Buffer
1566 withCapturedStdout(t, &out, func() {
1567 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1568 t.Fatalf("Pass v2: %v", err)
1569 }
1570 })
1571
1572 if v, _ := LiveVersion(opt, host); v != "v1" {
1573 t.Fatalf("live = %q, want v1 — the symlink must be rolled back", v)
1574 }
1575 gotSvc, _ := os.ReadFile(svcPath)
1576 gotSock, _ := os.ReadFile(sockPath)
1577 if !bytes.Equal(gotSvc, wantSvc) {
1578 t.Fatalf("service drop-in = %q, want v1's %q", gotSvc, wantSvc)
1579 }
1580 if !bytes.Equal(gotSock, wantSock) {
1581 t.Fatalf("socket drop-in = %q, want v1's %q — a rolled-back version must not leave :465 bound", gotSock, wantSock)
1582 }
1583 // Bytes are not a binding: restoring the socket half has to re-run the
1584 // socket sequence, or systemd keeps the listener the rolled-back version
1585 // asked for.
1586 if !rec.called("restart " + caps.SocketUnit(host)) {
1587 t.Fatalf("calls = %v, want the socket unit re-bound during the restore", rec.calls)
1588 }
1589 // The ordering inside the rollback is load-bearing and easy to get
1590 // backwards: the symlink goes back FIRST, so the service the restore
1591 // starts comes up on the OLD binary. Restoring first would start v2's
1592 // binary under v1's unit text — the failure this whole branch exists to
1593 // undo. lastLiveAt, not liveAt: the same verb already ran during the
1594 // forward converge.
1595 if v, seen := rec.lastLiveAt("start " + caps.ServiceUnit(host)); !seen || v != "v1" {
1596 t.Fatalf("the restore's service start observed live=%q (seen=%v), want v1 — the symlink must be rolled back BEFORE the drop-ins are", v, seen)
1597 }
1598 if v, _ := LiveVersion(opt, "b.example.com"); v != "v1" {
1599 t.Fatalf("b live = %q, want v1 — the failed version must be poisoned for the rest of the pass", v)
1600 }
1601 line := out.String()
1602 if !strings.Contains(line, "rolled the symlink back") {
1603 t.Fatalf("printed %q, want a line saying the symlink was rolled back", line)
1604 }
1605 if !strings.Contains(line, "capability drop-ins") {
1606 t.Fatalf("printed %q, want a line saying the drop-ins were restored too", line)
1607 }
1608}
1609
1610// TestExecBackedRouteWithCapabilitiesMaterializesNothing: Backing == "" is
1611// the whole existing fleet. A release declaring the full relay set must
1612// change no unit text on such a route (its process is an exec child, not a
1613// carlos-app@ unit) while adoption itself proceeds exactly as before. This
1614// is also why no existing test in this package can reach the converge path.
1615func TestExecBackedRouteWithCapabilitiesMaterializesNothing(t *testing.T) {
1616 s := NewDirStore(t.TempDir())
1617 ctx := context.Background()
1618 priv, pub := testKeys(t)
1619 opt := t.TempDir()
1620 reg := testRegistry(t)
1621 host := "exec.example.com"
1622
1623 shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay"))
1624 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
1625 t.Fatalf("Promote: %v", err)
1626 }
1627 addCapRoute(t, reg, host, "keymail", "canary/x", "", "port:25", false, false)
1628
1629 rec := newCapsRecorder(opt, host)
1630 o, etc := capsTestOps(t, rec)
1631 var restarted []string
1632 if err := Pass(ctx, reg, s, pub, opt, func(h string) error {
1633 restarted = append(restarted, h)
1634 return nil
1635 }, WithCapabilities(o)); err != nil {
1636 t.Fatalf("Pass: %v", err)
1637 }
1638
1639 assertEmptyDir(t, etc, "an exec-backed route must materialize no unit text")
1640 if len(rec.calls) != 0 {
1641 t.Fatalf("systemctl calls = %v, want none", rec.calls)
1642 }
1643 if v, _ := LiveVersion(opt, host); v != "v1" {
1644 t.Fatalf("live = %q, want v1 — adoption itself must proceed", v)
1645 }
1646 if len(restarted) != 1 || restarted[0] != host {
1647 t.Fatalf("restarted = %v, want the ordinary callback for %s", restarted, host)
1648 }
1649}
1650
1651// TestPlatformAliasRouteMaterializesNothing: a platform alias is a second
1652// name for one process. A second carlos-app@ unit claiming :25 would break
1653// the real one, so an alias materializes nothing — and, being outside
1654// Adoptable(), is not adopted at all.
1655func TestPlatformAliasRouteMaterializesNothing(t *testing.T) {
1656 s := NewDirStore(t.TempDir())
1657 ctx := context.Background()
1658 priv, pub := testKeys(t)
1659 opt := t.TempDir()
1660 reg := testRegistry(t)
1661 host := "keymail.bab.apps.example.com"
1662
1663 shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay"))
1664 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
1665 t.Fatalf("Promote: %v", err)
1666 }
1667 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, true)
1668
1669 rec := newCapsRecorder(opt, host)
1670 o, etc := capsTestOps(t, rec)
1671 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1672 t.Fatalf("Pass: %v", err)
1673 }
1674
1675 assertEmptyDir(t, etc, "a platform alias must materialize no unit text")
1676 if len(rec.calls) != 0 {
1677 t.Fatalf("systemctl calls = %v, want none", rec.calls)
1678 }
1679 if v, _ := LiveVersion(opt, host); v != "" {
1680 t.Fatalf("live = %q, want unset — a platform alias is not adopted at all", v)
1681 }
1682}
1683
1684// TestPassWithoutWithCapabilitiesIsUnchanged is the byte-identical-to-before
1685// assertion, and the reason this task could land on its own: a unit-backed
1686// route with a full capability manifest, adopted by a Pass that was never
1687// given the option, writes no unit text anywhere and adopts exactly as it
1688// always did.
1689func TestPassWithoutWithCapabilitiesIsUnchanged(t *testing.T) {
1690 s := NewDirStore(t.TempDir())
1691 ctx := context.Background()
1692 priv, pub := testKeys(t)
1693 opt := t.TempDir()
1694 reg := testRegistry(t)
1695 host := "unit.example.com"
1696 etc := t.TempDir() // stands in for /etc/systemd/system; nothing may reach it
1697
1698 shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay"))
1699 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
1700 t.Fatalf("Promote: %v", err)
1701 }
1702 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
1703
1704 var restarted []string
1705 if err := Pass(ctx, reg, s, pub, opt, func(h string) error {
1706 restarted = append(restarted, h)
1707 return nil
1708 }); err != nil {
1709 t.Fatalf("Pass: %v", err)
1710 }
1711
1712 assertEmptyDir(t, etc, "a Pass without WithCapabilities must write no unit text")
1713 if v, _ := LiveVersion(opt, host); v != "v1" {
1714 t.Fatalf("live = %q, want v1", v)
1715 }
1716 if len(restarted) != 1 || restarted[0] != host {
1717 t.Fatalf("restarted = %v, want the ordinary callback for %s", restarted, host)
1718 }
1719}
1720
1721// TestUnconvergedRepointOfACapabilityRouteLogsTheWindow pins the 2026-08-03
1722// ruling on the canary fast loop. That loop runs inside carlos-edge.service
1723// (ProtectSystem=strict) and CANNOT write /etc/systemd/system, so it repoints
1724// a capability-bearing canary and leaves the unit shape to the hourly root
1725// pass — a window of up to an hour where a new binary runs under the previous
1726// version's drop-ins. The window is accepted; being silent about it is not.
1727//
1728// The signal is deliberately the cheapest honest one: "the new version's
1729// manifest declares capabilities at all", not "the new set differs from what
1730// is materialized" — the difference test needs the grants and a render of
1731// both sides, and this line exists to mark the window, not to diff it.
1732func TestUnconvergedRepointOfACapabilityRouteLogsTheWindow(t *testing.T) {
1733 s := NewDirStore(t.TempDir())
1734 ctx := context.Background()
1735 priv, pub := testKeys(t)
1736 opt := t.TempDir()
1737 reg := testRegistry(t)
1738
1739 shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay"))
1740 shipRelease(t, s, "bab", "plain", "v1", adoptPlatform, []byte("plain"))
1741 for _, app := range []string{"keymail", "plain"} {
1742 if err := Promote(ctx, s, priv, "bab", app, "canary/x", "v1"); err != nil {
1743 t.Fatalf("Promote %s: %v", app, err)
1744 }
1745 }
1746 addCapRoute(t, reg, "caps.example.com", "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
1747 // A unit-backed route whose release declares NOTHING must stay quiet —
1748 // otherwise the line fires on every ordinary tenant and nobody reads it.
1749 addCapRoute(t, reg, "plain.example.com", "plain", "canary/x", registry.BackingUnit, "", false, false)
1750
1751 var out bytes.Buffer
1752 withCapturedStdout(t, &out, func() {
1753 if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
1754 t.Fatalf("PassFilter: %v", err)
1755 }
1756 })
1757
1758 line := out.String()
1759 if !strings.Contains(line, "caps.example.com: v1 declares capabilities") {
1760 t.Fatalf("printed %q, want one line naming the unconverged capability repoint", line)
1761 }
1762 if !strings.Contains(line, "carlos-adopt.service") {
1763 t.Fatalf("printed %q, want the line to name what closes the window", line)
1764 }
1765 if strings.Contains(line, "plain.example.com: v1 declares") {
1766 t.Fatalf("printed %q, want silence for a route whose release declares nothing", line)
1767 }
1768}
1769
1770// TestHibernatingUnitRouteConvergesWithoutStarting: a hibernating route
1771// reaches adoptHost with restart == nil. Its drop-ins are unit TEXT, not a
1772// wake, so they converge — but nothing may start the service, or an hourly
1773// timer would defeat hibernation.
1774func TestHibernatingUnitRouteConvergesWithoutStarting(t *testing.T) {
1775 s := NewDirStore(t.TempDir())
1776 ctx := context.Background()
1777 priv, pub := testKeys(t)
1778 opt := t.TempDir()
1779 reg := testRegistry(t)
1780 host := "sleepy.example.com"
1781
1782 // No daemon: daemon + hibernate is a contradiction converge refuses.
1783 set := &caps.Set{V: 1, Command: "web", Ports: []caps.Port{{Port: 25}}}
1784 shipCapRelease(t, s, "bab", "keymail", "v1", set, []byte("relay"))
1785 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
1786 t.Fatalf("Promote: %v", err)
1787 }
1788 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", true, false)
1789
1790 rec := newCapsRecorder(opt, host)
1791 o, etc := capsTestOps(t, rec)
1792 if err := Pass(ctx, reg, s, pub, opt, func(string) error {
1793 t.Fatal("a hibernating host must never reach the restart callback")
1794 return nil
1795 }, WithCapabilities(o)); err != nil {
1796 t.Fatalf("Pass: %v", err)
1797 }
1798
1799 svcPath, _ := caps.DropInPaths(etc, host)
1800 if _, err := os.Stat(svcPath); err != nil {
1801 t.Fatalf("stat %s: %v — a hibernating route's drop-ins still converge", svcPath, err)
1802 }
1803 if v, _ := LiveVersion(opt, host); v != "v1" {
1804 t.Fatalf("live = %q, want v1", v)
1805 }
1806 for _, c := range rec.calls {
1807 if strings.HasSuffix(c, caps.ServiceUnit(host)) {
1808 t.Fatalf("calls = %v, want no verb against the SERVICE unit — a drop-in is not a wake", rec.calls)
1809 }
1810 }
1811}
1812
1813// TestConvergeRefusalRefusesTheVersion: a refusal from converge falls exactly
1814// where an ensureStaged failure falls — nothing pointed live, the box keeps
1815// serving what it has, one line naming the operator's fix. Pointing live
1816// anyway would run a new binary against a unit shape the platform has just
1817// declined to write.
1818//
1819// The refusal used here is spec §4e's daemon + hibernate contradiction,
1820// because it is refused with nothing written at all.
1821func TestConvergeRefusalRefusesTheVersion(t *testing.T) {
1822 s := NewDirStore(t.TempDir())
1823 ctx := context.Background()
1824 priv, pub := testKeys(t)
1825 opt := t.TempDir()
1826 reg := testRegistry(t)
1827 host := "sleepy.example.com"
1828
1829 shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old"))
1830 if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old")); err != nil {
1831 t.Fatal(err)
1832 }
1833 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", true, false)
1834 if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil {
1835 t.Fatal(err)
1836 }
1837 shipCapRelease(t, s, "bab", "keymail", "v2", relaySet(), []byte("new"))
1838 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
1839 t.Fatalf("Promote v2: %v", err)
1840 }
1841
1842 rec := newCapsRecorder(opt, host)
1843 o, etc := capsTestOps(t, rec)
1844 var out bytes.Buffer
1845 withCapturedStdout(t, &out, func() {
1846 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1847 t.Fatalf("Pass: %v", err)
1848 }
1849 })
1850
1851 if v, _ := LiveVersion(opt, host); v != "v1" {
1852 t.Fatalf("live = %q, want v1 — a refused converge must not point live", v)
1853 }
1854 assertEmptyDir(t, etc, "a refusal writes nothing")
1855 if len(rec.calls) != 0 {
1856 t.Fatalf("systemctl calls = %v, want none", rec.calls)
1857 }
1858 line := out.String()
1859 if !strings.Contains(line, "not pointed live") {
1860 t.Fatalf("printed %q, want a line saying the version was not pointed live", line)
1861 }
1862 if !strings.Contains(line, "-hibernate=false") {
1863 t.Fatalf("printed %q, want converge's own line naming the operator's fix", line)
1864 }
1865}
1866
1867// TestArmedPassRefusesAVersionWhoseManifestItCouldNotObtain is the second of
1868// Task 7's two pins, in the one place it can bite: the binary is staged (so
1869// ensureStaged succeeds) but its manifest is obtainable neither from the
1870// sibling nor from the bucket. caps.Plan's nil Set means "leave the drop-ins
1871// alone" — correct for a pass surveying the box, and exactly wrong here,
1872// where it would point a new binary at the previous version's unit shape and
1873// say nothing. So an armed pass refuses the version.
1874//
1875// The second half is the compatibility half: the SAME situation on a pass
1876// with no WithCapabilities adopts normally, which is what keeps every
1877// existing caller byte-identical.
1878func TestArmedPassRefusesAVersionWhoseManifestItCouldNotObtain(t *testing.T) {
1879 dir := t.TempDir()
1880 s := NewDirStore(dir)
1881 ctx := context.Background()
1882 priv, pub := testKeys(t)
1883 opt := t.TempDir()
1884 reg := testRegistry(t)
1885 host := "keymail.dev"
1886
1887 shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old"))
1888 if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old")); err != nil {
1889 t.Fatal(err)
1890 }
1891 if err := StageManifest(opt, "bab", "v1", "keymail", []byte(`{"schema":1,"app":"keymail","version":"v1","kind":"binary"}`)); err != nil {
1892 t.Fatal(err)
1893 }
1894 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
1895 if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil {
1896 t.Fatal(err)
1897 }
1898
1899 // v2's binary is on disk with no sibling, and its manifest object is then
1900 // removed from the store: the back-fill fetch cannot answer either.
1901 shipCapRelease(t, s, "bab", "keymail", "v2", relaySet(), []byte("new"))
1902 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
1903 t.Fatalf("Promote v2: %v", err)
1904 }
1905 if err := StageArtifact(opt, "bab", "v2", "keymail", []byte("new")); err != nil {
1906 t.Fatal(err)
1907 }
1908 if err := os.Remove(filepath.Join(dir, filepath.FromSlash(ManifestKey("bab", "keymail", "v2")))); err != nil {
1909 t.Fatalf("remove v2 manifest: %v", err)
1910 }
1911
1912 rec := newCapsRecorder(opt, host)
1913 o, etc := capsTestOps(t, rec)
1914 var out bytes.Buffer
1915 withCapturedStdout(t, &out, func() {
1916 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1917 t.Fatalf("armed Pass: %v", err)
1918 }
1919 })
1920 if v, _ := LiveVersion(opt, host); v != "v1" {
1921 t.Fatalf("live = %q, want v1 — an unknown unit shape must refuse the version", v)
1922 }
1923 assertEmptyDir(t, etc, "a refusal writes nothing")
1924 if line := out.String(); !strings.Contains(line, "not pointed live") {
1925 t.Fatalf("printed %q, want a line saying the version was not pointed live", line)
1926 }
1927
1928 // Unarmed, the identical situation adopts exactly as it always did.
1929 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
1930 t.Fatalf("unarmed Pass: %v", err)
1931 }
1932 if v, _ := LiveVersion(opt, host); v != "v2" {
1933 t.Fatalf("live = %q after an unarmed pass, want v2 — capabilities off means behaviour unchanged", v)
1934 }
1935}
1936
1937// assertEmptyDir fails unless dir has no entries at all.
1938func assertEmptyDir(t *testing.T, dir, why string) {
1939 t.Helper()
1940 ents, err := os.ReadDir(dir)
1941 if err != nil {
1942 t.Fatalf("read %s: %v", dir, err)
1943 }
1944 if len(ents) != 0 {
1945 var names []string
1946 for _, e := range ents {
1947 names = append(names, e.Name())
1948 }
1949 t.Fatalf("%s contains %v — %s", dir, names, why)
1950 }
1951}
1952
1953// TestAdoptRemovesDropInsWhenTheNewVersionDeclaresNothing is Task 7's second
1954// pin, and the mutant it kills is a one-word one: Plan{Set: m.Capabilities}.
1955// Manifest.Capabilities is a nil pointer for BOTH a legacy manifest and a
1956// fetch that never happened, and caps.Plan reads a nil Set as "leave the
1957// drop-ins exactly as they are". Passing it through verbatim would mean a
1958// capability DROPPED from a manifest is never removed — a relay that stopped
1959// asking for :25 would keep binding it, which is the exact opposite of spec
1960// §4d's removal semantics. A manifest that RESOLVED always carries a Set,
1961// empty when it declares nothing.
1962func TestAdoptRemovesDropInsWhenTheNewVersionDeclaresNothing(t *testing.T) {
1963 s := NewDirStore(t.TempDir())
1964 ctx := context.Background()
1965 priv, pub := testKeys(t)
1966 opt := t.TempDir()
1967 reg := testRegistry(t)
1968 host := "keymail.dev"
1969
1970 shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay"))
1971 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
1972 t.Fatalf("Promote v1: %v", err)
1973 }
1974 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
1975
1976 rec := newCapsRecorder(opt, host)
1977 o, etc := capsTestOps(t, rec)
1978 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1979 t.Fatalf("Pass v1: %v", err)
1980 }
1981 svcPath, sockPath := caps.DropInPaths(etc, host)
1982 for _, p := range []string{svcPath, sockPath} {
1983 if _, err := os.Stat(p); err != nil {
1984 t.Fatalf("stat %s after v1: %v", p, err)
1985 }
1986 }
1987
1988 // v2 declares nothing at all — a plain manifest, exactly like every
1989 // release in every production bucket today.
1990 shipRelease(t, s, "bab", "keymail", "v2", adoptPlatform, []byte("plain relay"))
1991 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
1992 t.Fatalf("Promote v2: %v", err)
1993 }
1994 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
1995 t.Fatalf("Pass v2: %v", err)
1996 }
1997
1998 if v, _ := LiveVersion(opt, host); v != "v2" {
1999 t.Fatalf("live = %q, want v2", v)
2000 }
2001 for _, p := range []string{svcPath, sockPath} {
2002 if _, err := os.Stat(p); !os.IsNotExist(err) {
2003 b, _ := os.ReadFile(p)
2004 t.Fatalf("%s still exists after a version that declares nothing (%q) — a dropped capability must be removed", p, b)
2005 }
2006 }
2007 // Removing the socket half has to reach systemd, or the listener the
2008 // removed file asked for is still bound.
2009 if !rec.called("restart " + caps.SocketUnit(host)) {
2010 t.Fatalf("calls = %v, want the socket unit re-bound when its drop-in was removed", rec.calls)
2011 }
2012}
2013
2014// TestDaemonVersionChangeStartsTheServiceExactlyOnce covers the other half of
2015// the deferred restart: a version change whose SOCKET shape is unchanged, so
2016// nothing in converge's socket ordering runs and step 9's service restart is
2017// the only thing that would have cycled the unit. Deferred, the app is
2018// started exactly once, after the symlink moves — one cycle, on the new
2019// binary, instead of two with the first one running the old.
2020func TestDaemonVersionChangeStartsTheServiceExactlyOnce(t *testing.T) {
2021 s := NewDirStore(t.TempDir())
2022 ctx := context.Background()
2023 priv, pub := testKeys(t)
2024 opt := t.TempDir()
2025 reg := testRegistry(t)
2026 host := "keymail.dev"
2027
2028 v1 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}}}
2029 v2 := &caps.Set{V: 1, Command: "relay-two", Daemon: true, Ports: []caps.Port{{Port: 25}}}
2030 shipCapRelease(t, s, "bab", "keymail", "v1", v1, []byte("relay one"))
2031 shipCapRelease(t, s, "bab", "keymail", "v2", v2, []byte("relay two"))
2032 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
2033 t.Fatalf("Promote v1: %v", err)
2034 }
2035 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
2036
2037 rec := newCapsRecorder(opt, host)
2038 o, _ := capsTestOps(t, rec)
2039 restart := func(string) error { return nil }
2040 if err := Pass(ctx, reg, s, pub, opt, restart, WithCapabilities(o)); err != nil {
2041 t.Fatalf("Pass v1: %v", err)
2042 }
2043 if n, _ := rec.serviceCycles(host); n != 1 {
2044 t.Fatalf("first adoption started the service %d time(s), want 1 (calls: %v)", n, rec.calls)
2045 }
2046 rec.reset()
2047
2048 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
2049 t.Fatalf("Promote v2: %v", err)
2050 }
2051 if err := Pass(ctx, reg, s, pub, opt, restart, WithCapabilities(o)); err != nil {
2052 t.Fatalf("Pass v2: %v", err)
2053 }
2054
2055 if v, _ := LiveVersion(opt, host); v != "v2" {
2056 t.Fatalf("live = %q, want v2", v)
2057 }
2058 if n, last := rec.serviceCycles(host); n != 1 || last != "v2" {
2059 t.Fatalf("service started/restarted %d time(s), last at live=%q; want exactly 1, at v2 (calls: %v)", n, last, rec.calls)
2060 }
2061}
2062
2063// TestConvergeFailureAfterItsReloadRestoresTheDropIns is the failure the
2064// converge contract hands to this caller by name: a systemd verb that fails
2065// AFTER the daemon-reload leaves the new version's unit text on disk, loaded,
2066// and deliberately un-rolled-back (Converge reports Changed with the error
2067// and says the version-change caller owns it). Nothing points live, so those
2068// bytes describe a version this box is not running — and they are permanent
2069// if nobody undoes them: the next pass finds no diff, snapshots them as its
2070// own Prev, and every later "restore" restores the wrong shape.
2071//
2072// The failing verb here is `enable` on the socket unit, which is post-reload
2073// and NOT one of the diverging ones Converge rolls back itself.
2074func TestConvergeFailureAfterItsReloadRestoresTheDropIns(t *testing.T) {
2075 s := NewDirStore(t.TempDir())
2076 ctx := context.Background()
2077 priv, pub := testKeys(t)
2078 opt := t.TempDir()
2079 reg := testRegistry(t)
2080 host := "keymail.dev"
2081
2082 // The box is on v1, which declares nothing, so there are no drop-ins yet.
2083 shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old"))
2084 if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old")); err != nil {
2085 t.Fatal(err)
2086 }
2087 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
2088 if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil {
2089 t.Fatal(err)
2090 }
2091 shipCapRelease(t, s, "bab", "keymail", "v2", relaySet(), []byte("new"))
2092 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
2093 t.Fatalf("Promote v2: %v", err)
2094 }
2095
2096 rec := newCapsRecorder(opt, host)
2097 o, etc := capsTestOps(t, rec)
2098 rec.fail["enable "+caps.SocketUnit(host)] = errors.New("unit masked")
2099
2100 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2101 t.Fatalf("Pass: %v", err)
2102 }
2103
2104 if v, _ := LiveVersion(opt, host); v != "v1" {
2105 t.Fatalf("live = %q, want v1", v)
2106 }
2107 svcPath, sockPath := caps.DropInPaths(etc, host)
2108 for _, p := range []string{svcPath, sockPath} {
2109 if b, err := os.ReadFile(p); err == nil {
2110 t.Fatalf("%s survives a converge that failed after its reload (%q) — v2's unit shape must not be left on disk under v1's binary", p, b)
2111 }
2112 }
2113}
2114
2115// TestConvergeRefusalLeavesAnEarlierVersionsDropInsAlone is the other side of
2116// that coin, and the reason the restore above is conditional. An ORDINARY
2117// refusal writes nothing — and the refusals that fire earliest fire before
2118// the drop-ins are even read, so the snapshot is EMPTY. caps.Restore reads an
2119// empty snapshot as "this route had no generated drop-ins" and honours it by
2120// REMOVING the ones it finds, so restoring unconditionally on a refusal would
2121// delete the shape the running version is serving under.
2122//
2123// The refusal here is spec §4e's daemon + hibernate contradiction, which
2124// Converge checks before it reads anything.
2125func TestConvergeRefusalLeavesAnEarlierVersionsDropInsAlone(t *testing.T) {
2126 s := NewDirStore(t.TempDir())
2127 ctx := context.Background()
2128 priv, pub := testKeys(t)
2129 opt := t.TempDir()
2130 reg := testRegistry(t)
2131 host := "sleepy.example.com"
2132
2133 // v1 is a hibernating tenant with a granted port and no daemon: a
2134 // perfectly legal shape, and it converges.
2135 v1 := &caps.Set{V: 1, Command: "web", Ports: []caps.Port{{Port: 25}}}
2136 shipCapRelease(t, s, "bab", "keymail", "v1", v1, []byte("web one"))
2137 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
2138 t.Fatalf("Promote v1: %v", err)
2139 }
2140 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", true, false)
2141
2142 rec := newCapsRecorder(opt, host)
2143 o, etc := capsTestOps(t, rec)
2144 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2145 t.Fatalf("Pass v1: %v", err)
2146 }
2147 svcPath, sockPath := caps.DropInPaths(etc, host)
2148 wantSvc, err := os.ReadFile(svcPath)
2149 if err != nil {
2150 t.Fatalf("read v1 service drop-in: %v", err)
2151 }
2152 wantSock, err := os.ReadFile(sockPath)
2153 if err != nil {
2154 t.Fatalf("read v1 socket drop-in: %v", err)
2155 }
2156
2157 // v2 adds `daemon` to a hibernating route — refused before anything is
2158 // read or written.
2159 shipCapRelease(t, s, "bab", "keymail", "v2", relaySet(), []byte("web two"))
2160 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
2161 t.Fatalf("Promote v2: %v", err)
2162 }
2163 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2164 t.Fatalf("Pass v2: %v", err)
2165 }
2166
2167 if v, _ := LiveVersion(opt, host); v != "v1" {
2168 t.Fatalf("live = %q, want v1 — the refusal must not point live", v)
2169 }
2170 gotSvc, _ := os.ReadFile(svcPath)
2171 gotSock, _ := os.ReadFile(sockPath)
2172 if !bytes.Equal(gotSvc, wantSvc) || !bytes.Equal(gotSock, wantSock) {
2173 t.Fatalf("drop-ins after a refusal = %q / %q, want v1's %q / %q left exactly as they were",
2174 gotSvc, gotSock, wantSvc, wantSock)
2175 }
2176}
2177
2178// TestConvergeRefusalDoesNotPoisonTheVersionForOtherHosts is the 2026-08-03
2179// ruling on the poison. A refusal bounces nothing — no symlink moved, no unit
2180// cycled — so there is no bad build to keep off the next host, which is the
2181// only thing the poison exists for. And the refusal is often HOST-shaped: the
2182// one here is a port a third route's drop-in already holds on this box.
2183// Poisoning on that would let one misconfigured route freeze every other host
2184// out of a version that is fine for them, every pass, until an operator
2185// noticed.
2186//
2187// The ordering is the point: a.example.com is refused FIRST (routes are swept
2188// in host order), so a poison set there is what b.example.com would trip over.
2189func TestConvergeRefusalDoesNotPoisonTheVersionForOtherHosts(t *testing.T) {
2190 s := NewDirStore(t.TempDir())
2191 ctx := context.Background()
2192 priv, pub := testKeys(t)
2193 opt := t.TempDir()
2194 reg := testRegistry(t)
2195
2196 shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay"))
2197 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
2198 t.Fatalf("Promote: %v", err)
2199 }
2200 // a holds the grant for :25 and is refused, because something else on
2201 // this box already claims it. b declares the same port with NO grant, so
2202 // it claims nothing, materializes only its service half, and is fine.
2203 addCapRoute(t, reg, "a.example.com", "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
2204 addCapRoute(t, reg, "b.example.com", "keymail", "canary/x", registry.BackingUnit, "", false, false)
2205
2206 rec := newCapsRecorder(opt, "a.example.com")
2207 o, etc := capsTestOps(t, rec)
2208 // A drop-in belonging to a route this pass never sees, holding :25.
2209 incumbent := filepath.Join(etc, caps.SocketUnit("zz.example.com")+".d")
2210 if err := os.MkdirAll(incumbent, 0o755); err != nil {
2211 t.Fatal(err)
2212 }
2213 if err := os.WriteFile(filepath.Join(incumbent, caps.DropInName),
2214 []byte(caps.Marker+"\n[Socket]\nListenStream=25\n"), 0o644); err != nil {
2215 t.Fatal(err)
2216 }
2217
2218 var out bytes.Buffer
2219 withCapturedStdout(t, &out, func() {
2220 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2221 t.Fatalf("Pass: %v", err)
2222 }
2223 })
2224
2225 if v, _ := LiveVersion(opt, "a.example.com"); v != "" {
2226 t.Fatalf("a live = %q, want unset — its own refusal keeps it off the version", v)
2227 }
2228 if v, _ := LiveVersion(opt, "b.example.com"); v != "v1" {
2229 t.Fatalf("b live = %q, want v1 — another host's refusal must not freeze this one out of the version", v)
2230 }
2231 line := out.String()
2232 if strings.Contains(line, "failed on another host this pass") {
2233 t.Fatalf("printed %q, want no poison — a refusal is not a bad build", line)
2234 }
2235 if !strings.Contains(line, "a.example.com: capabilities for v1 were refused") {
2236 t.Fatalf("printed %q, want a's own refusal named", line)
2237 }
2238}
2239
2240// TestPointLiveFailureRestoresTheDropIns: converge succeeded, the symlink did
2241// not move. The box is still serving the previous version, and the unit shape
2242// on disk is the new one — a port the running binary never asked for, bound
2243// in public. The drop-ins have to follow the symlink, which stayed put.
2244//
2245// PointLive is failed by parking a non-empty directory at the temp-link path
2246// it renames through, which is as close to a disk-level swap failure as this
2247// package can get without a fake filesystem.
2248func TestPointLiveFailureRestoresTheDropIns(t *testing.T) {
2249 s := NewDirStore(t.TempDir())
2250 ctx := context.Background()
2251 priv, pub := testKeys(t)
2252 opt := t.TempDir()
2253 reg := testRegistry(t)
2254 host := "keymail.dev"
2255
2256 v1 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}}}
2257 v2 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}, {Port: 465}}}
2258 shipCapRelease(t, s, "bab", "keymail", "v1", v1, []byte("relay one"))
2259 shipCapRelease(t, s, "bab", "keymail", "v2", v2, []byte("relay two"))
2260 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
2261 t.Fatalf("Promote v1: %v", err)
2262 }
2263 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25,port:465", false, false)
2264
2265 rec := newCapsRecorder(opt, host)
2266 o, etc := capsTestOps(t, rec)
2267 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2268 t.Fatalf("Pass v1: %v", err)
2269 }
2270 svcPath, sockPath := caps.DropInPaths(etc, host)
2271 wantSvc, _ := os.ReadFile(svcPath)
2272 wantSock, _ := os.ReadFile(sockPath)
2273 if !strings.Contains(string(wantSock), "ListenStream=25") || strings.Contains(string(wantSock), "465") {
2274 t.Fatalf("v1 socket drop-in = %q, want :25 only", wantSock)
2275 }
2276
2277 // Block the symlink swap.
2278 blocker := LiveLink(opt, host) + ".tmp"
2279 if err := os.MkdirAll(filepath.Join(blocker, "occupied"), 0o755); err != nil {
2280 t.Fatalf("park a blocker at %s: %v", blocker, err)
2281 }
2282 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
2283 t.Fatalf("Promote v2: %v", err)
2284 }
2285
2286 var out bytes.Buffer
2287 withCapturedStdout(t, &out, func() {
2288 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2289 t.Fatalf("Pass v2: %v", err)
2290 }
2291 })
2292
2293 if v, _ := LiveVersion(opt, host); v != "v1" {
2294 t.Fatalf("live = %q, want v1 — the repoint was supposed to fail", v)
2295 }
2296 gotSvc, _ := os.ReadFile(svcPath)
2297 gotSock, _ := os.ReadFile(sockPath)
2298 if !bytes.Equal(gotSvc, wantSvc) || !bytes.Equal(gotSock, wantSock) {
2299 t.Fatalf("drop-ins = %q / %q, want v1's %q / %q — :465 must not stay bound under a binary that never asked for it",
2300 gotSvc, gotSock, wantSvc, wantSock)
2301 }
2302 if line := out.String(); !strings.Contains(line, "repoint failed") || !strings.Contains(line, "capability drop-ins") {
2303 t.Fatalf("printed %q, want a line naming both the failed repoint and the restored drop-ins", line)
2304 }
2305}
2306
2307// TestArmedPassConvergesFromTheStagedSiblingWhenTheBucketCannotAnswer pins
2308// the read ensureStaged does on its already-staged path. The binary is on
2309// disk with its sibling beside it and the bucket object is gone: the sibling
2310// IS the manifest in hand there, and dropping that read would send this
2311// straight to a bucket fetch that cannot answer, then to a refusal of a
2312// version whose capabilities are sitting on disk, readable, all along.
2313func TestArmedPassConvergesFromTheStagedSiblingWhenTheBucketCannotAnswer(t *testing.T) {
2314 dir := t.TempDir()
2315 s := NewDirStore(dir)
2316 ctx := context.Background()
2317 priv, pub := testKeys(t)
2318 opt := t.TempDir()
2319 reg := testRegistry(t)
2320 host := "keymail.dev"
2321
2322 shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old"))
2323 if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old")); err != nil {
2324 t.Fatal(err)
2325 }
2326 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
2327 if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil {
2328 t.Fatal(err)
2329 }
2330
2331 set := relaySet()
2332 shipCapRelease(t, s, "bab", "keymail", "v2", set, []byte("new"))
2333 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
2334 t.Fatalf("Promote v2: %v", err)
2335 }
2336 // v2 is staged the way a box that adopted it once already would have it:
2337 // binary AND sibling on disk. Then the bucket object goes away.
2338 if err := StageArtifact(opt, "bab", "v2", "keymail", []byte("new")); err != nil {
2339 t.Fatal(err)
2340 }
2341 raw, _, err := s.Get(ctx, ManifestKey("bab", "keymail", "v2"))
2342 if err != nil {
2343 t.Fatalf("read v2 manifest: %v", err)
2344 }
2345 if err := StageManifest(opt, "bab", "v2", "keymail", raw); err != nil {
2346 t.Fatalf("StageManifest: %v", err)
2347 }
2348 if err := os.Remove(filepath.Join(dir, filepath.FromSlash(ManifestKey("bab", "keymail", "v2")))); err != nil {
2349 t.Fatalf("remove v2 manifest: %v", err)
2350 }
2351
2352 rec := newCapsRecorder(opt, host)
2353 o, etc := capsTestOps(t, rec)
2354 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2355 t.Fatalf("Pass: %v", err)
2356 }
2357
2358 if v, _ := LiveVersion(opt, host); v != "v2" {
2359 t.Fatalf("live = %q, want v2 — the sibling answered, so nothing needed the bucket", v)
2360 }
2361 g := caps.Grants{Ports: map[int]bool{25: true}}
2362 svcPath, _ := caps.DropInPaths(etc, host)
2363 wantSvc, err := caps.RenderService("keymail", set, g)
2364 if err != nil {
2365 t.Fatalf("RenderService: %v", err)
2366 }
2367 gotSvc, err := os.ReadFile(svcPath)
2368 if err != nil || !bytes.Equal(gotSvc, wantSvc) {
2369 t.Fatalf("service drop-in = %q (%v), want the sibling's capabilities materialized: %q", gotSvc, err, wantSvc)
2370 }
2371}
2372
2373// TestRestartFailureSaysSoWhenTheRestoreAlsoFails: the summary line is the
2374// only thing most operators read, and it must not claim a restore that did
2375// not happen. A rollback that could not put the drop-ins back is the worst
2376// state this path can end in — the symlink says v1, the unit text says v2 —
2377// so it is exactly the case that must not be reported as a tidy rollback.
2378//
2379// The restore is failed by making the socket drop-in's directory unwritable
2380// at the moment the daemon restart fails, i.e. after the forward converge has
2381// written it and before the rollback tries to.
2382func TestRestartFailureSaysSoWhenTheRestoreAlsoFails(t *testing.T) {
2383 if os.Geteuid() == 0 {
2384 t.Skip("running as root: directory permissions do not refuse a write")
2385 }
2386 s := NewDirStore(t.TempDir())
2387 ctx := context.Background()
2388 priv, pub := testKeys(t)
2389 opt := t.TempDir()
2390 reg := testRegistry(t)
2391 host := "keymail.dev"
2392
2393 v1 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}}}
2394 v2 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}, {Port: 465}}}
2395 shipCapRelease(t, s, "bab", "keymail", "v1", v1, []byte("relay one"))
2396 shipCapRelease(t, s, "bab", "keymail", "v2", v2, []byte("relay two"))
2397 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
2398 t.Fatalf("Promote v1: %v", err)
2399 }
2400 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25,port:465", false, false)
2401
2402 rec := newCapsRecorder(opt, host)
2403 o, etc := capsTestOps(t, rec)
2404 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2405 t.Fatalf("Pass v1: %v", err)
2406 }
2407
2408 _, sockPath := caps.DropInPaths(etc, host)
2409 sockDir := filepath.Dir(sockPath)
2410 t.Cleanup(func() { os.Chmod(sockDir, 0o755) })
2411 rec.fail["restart "+caps.ServiceUnit(host)] = errors.New("relay failed to start")
2412 rec.onCall = func(call string) {
2413 if call == "restart "+caps.ServiceUnit(host) {
2414 if err := os.Chmod(sockDir, 0o555); err != nil {
2415 t.Errorf("chmod %s: %v", sockDir, err)
2416 }
2417 }
2418 }
2419
2420 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil {
2421 t.Fatalf("Promote v2: %v", err)
2422 }
2423 var out bytes.Buffer
2424 withCapturedStdout(t, &out, func() {
2425 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2426 t.Fatalf("Pass v2: %v", err)
2427 }
2428 })
2429
2430 line := out.String()
2431 if !strings.Contains(line, "could not restore") {
2432 t.Fatalf("printed %q, want the summary to say the restore failed", line)
2433 }
2434 if strings.Contains(line, "and restored the previous version's capability drop-ins") {
2435 t.Fatalf("printed %q, want no claim of a restore that did not happen", line)
2436 }
2437 if v, _ := LiveVersion(opt, host); v != "v1" {
2438 t.Fatalf("live = %q, want v1 — the symlink half of the rollback still has to happen", v)
2439 }
2440}
2441
2442// TestFirstAdoptionRestartFailureLeavesTheConvergedDropIns: with no previous
2443// version there is no symlink to roll back — the box stays live on the
2444// version whose restart just failed. Handing caps.Restore the empty snapshot
2445// that describes "this route had no drop-ins" would then REMOVE the shape the
2446// live version needs, leaving it unconfigured until the hourly grant-drift
2447// pass rebuilds it. The honest state is the one the symlink names.
2448func TestFirstAdoptionRestartFailureLeavesTheConvergedDropIns(t *testing.T) {
2449 s := NewDirStore(t.TempDir())
2450 ctx := context.Background()
2451 priv, pub := testKeys(t)
2452 opt := t.TempDir()
2453 reg := testRegistry(t)
2454 host := "keymail.dev"
2455
2456 set := relaySet()
2457 shipCapRelease(t, s, "bab", "keymail", "v1", set, []byte("relay"))
2458 if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil {
2459 t.Fatalf("Promote: %v", err)
2460 }
2461 addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false)
2462
2463 rec := newCapsRecorder(opt, host)
2464 o, etc := capsTestOps(t, rec)
2465 rec.fail["restart "+caps.ServiceUnit(host)] = errors.New("relay failed to start")
2466
2467 var out bytes.Buffer
2468 withCapturedStdout(t, &out, func() {
2469 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil {
2470 t.Fatalf("Pass: %v", err)
2471 }
2472 })
2473
2474 if v, _ := LiveVersion(opt, host); v != "v1" {
2475 t.Fatalf("live = %q, want v1 — there is no previous version to roll back to", v)
2476 }
2477 g := caps.Grants{Ports: map[int]bool{25: true}}
2478 svcPath, sockPath := caps.DropInPaths(etc, host)
2479 wantSvc, _ := caps.RenderService("keymail", set, g)
2480 wantSock, _ := caps.RenderSocket(set, g)
2481 gotSvc, err := os.ReadFile(svcPath)
2482 if err != nil || !bytes.Equal(gotSvc, wantSvc) {
2483 t.Fatalf("service drop-in = %q (%v), want the converged shape the live version needs: %q", gotSvc, err, wantSvc)
2484 }
2485 gotSock, err := os.ReadFile(sockPath)
2486 if err != nil || !bytes.Equal(gotSock, wantSock) {
2487 t.Fatalf("socket drop-in = %q (%v), want %q", gotSock, err, wantSock)
2488 }
2489 if line := out.String(); !strings.Contains(line, "no previous version to roll back to") {
2490 t.Fatalf("printed %q, want the line to say why nothing was rolled back", line)
2491 }
2492}
2493
2494// ---- the production flag on the adopt path (2026-08-04 spec §6) -------------
2495
2496// The default: no flag, no window (2026-08-04 spec §2). The whole fleet
2497// runs on canary/* today, so nothing live loses protection — and a
2498// rapid-iteration app's stable is just another channel.
2499func TestPassNoProductionFlagInstallsImmediately(t *testing.T) {
2500 s := NewDirStore(t.TempDir())
2501 ctx := context.Background()
2502 priv, pub := testKeys(t)
2503 opt := t.TempDir()
2504 reg := testRegistry(t)
2505
2506 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2507 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
2508 t.Fatal(err)
2509 }
2510 addRoute(t, reg, "h.example.com", "myapp", "", false)
2511
2512 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2513 t.Fatalf("Pass: %v", err)
2514 }
2515 if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
2516 t.Fatalf("live = %q, want v1 — no production flag means no window", v)
2517 }
2518}
2519
2520// Fast-track: a production app's held head installs the moment its pointer
2521// carries the root-signed waiver (spec §5).
2522func TestPassFastTrackSkipsTheWindow(t *testing.T) {
2523 s := NewDirStore(t.TempDir())
2524 ctx := context.Background()
2525 priv, pub := testKeys(t)
2526 opt := t.TempDir()
2527 reg := testRegistry(t)
2528
2529 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2530 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
2531 t.Fatal(err)
2532 }
2533 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
2534 t.Fatal(err)
2535 }
2536 addRoute(t, reg, "h.example.com", "myapp", "", false)
2537
2538 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2539 t.Fatal(err)
2540 }
2541 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
2542 t.Fatalf("live = %q, want held (production, no waiver yet)", v)
2543 }
2544 if _, err := FastTrack(ctx, s, priv, "bab", "myapp", "stable", "test"); err != nil {
2545 t.Fatal(err)
2546 }
2547 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2548 t.Fatal(err)
2549 }
2550 if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
2551 t.Fatalf("live = %q, want v1 — the waiver did not skip the window", v)
2552 }
2553}
2554
2555// A production app whose flag OBJECT rots (exists, won't verify) keeps
2556// baking — an attacker who can corrupt but not sign shortens nothing.
2557func TestPassCorruptProductionFlagFailsTowardBaking(t *testing.T) {
2558 s := NewDirStore(t.TempDir())
2559 ctx := context.Background()
2560 priv, pub := testKeys(t)
2561 opt := t.TempDir()
2562 reg := testRegistry(t)
2563
2564 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2565 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
2566 t.Fatal(err)
2567 }
2568 if _, err := s.Put(ctx, ProductionKey("bab", "myapp"), []byte("not a wrapper"), ""); err != nil {
2569 t.Fatal(err)
2570 }
2571 addRoute(t, reg, "h.example.com", "myapp", "", false)
2572
2573 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2574 t.Fatalf("Pass: %v", err)
2575 }
2576 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
2577 t.Fatalf("live = %q, want held — a corrupt flag must read as production", v)
2578 }
2579}
2580
2581// ORDERING, pinned: ObserveProduction runs BEFORE this pass's bake verdicts
2582// are taken, so the bucket flag re-latches a wiped memory within ONE pass —
2583// the whole basis of prodMemory's "a mixed-version rollout costs at most one
2584// pass of this memory" comment. Simulates the old binary that re-marshals a
2585// struct with no Production field and thereby deletes the latch.
2586func TestPassReLatchesProductionMemoryInTheSamePass(t *testing.T) {
2587 s := NewDirStore(t.TempDir())
2588 ctx := context.Background()
2589 priv, pub := testKeys(t)
2590 opt := t.TempDir()
2591 reg := testRegistry(t)
2592
2593 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2594 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
2595 t.Fatal(err)
2596 }
2597 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
2598 t.Fatal(err)
2599 }
2600 addRoute(t, reg, "h.example.com", "myapp", "", false)
2601
2602 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2603 t.Fatal(err)
2604 }
2605 st := LoadFirstSeen(FirstSeenPath(opt))
2606 if len(st.Production) == 0 {
2607 t.Fatalf("first pass did not latch the production memory")
2608 }
2609
2610 // The old binary's write: every channel observation survives, the
2611 // production memory (and with it the replay floor) is gone.
2612 st.Production = map[string]*prodMemory{}
2613 if err := st.Save(); err != nil {
2614 t.Fatalf("Save wiped clock: %v", err)
2615 }
2616
2617 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2618 t.Fatal(err)
2619 }
2620 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
2621 t.Fatalf("live = %q, want still held — the bucket flag must re-latch BEFORE this pass's bake verdict, not after it", v)
2622 }
2623 if st := LoadFirstSeen(FirstSeenPath(opt)); len(st.Production) == 0 {
2624 t.Fatalf("the re-latch was not persisted")
2625 }
2626}
2627
2628// A clock file that exists but does not parse is rewritten within ONE pass,
2629// even when the pass observes nothing new — the taint-forced Save. Without
2630// it a corrupt file would sit there forever, and every held head on the box
2631// would fail toward baking pass after pass (ProductionBake's st.tainted).
2632func TestPassRewritesATaintedClockInOnePass(t *testing.T) {
2633 s := NewDirStore(t.TempDir())
2634 ctx := context.Background()
2635 _, pub := testKeys(t)
2636 opt := t.TempDir()
2637 reg := testRegistry(t) // no routes: nothing new is observed this pass
2638
2639 if err := os.WriteFile(FirstSeenPath(opt), []byte("{not json"), 0o644); err != nil {
2640 t.Fatal(err)
2641 }
2642 if st := LoadFirstSeen(FirstSeenPath(opt)); !st.tainted {
2643 t.Fatalf("setup: the corrupt clock did not read as tainted")
2644 }
2645
2646 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2647 t.Fatalf("Pass: %v", err)
2648 }
2649 if st := LoadFirstSeen(FirstSeenPath(opt)); st.tainted {
2650 t.Fatalf("the pass left the clock file corrupt — a tainted clock must force its own Save")
2651 }
2652}
2653
2654// prodReadStore counts Get calls for one exact key.
2655type prodReadStore struct {
2656 Store
2657 key string
2658 n int
2659}
2660
2661func (p *prodReadStore) Get(ctx context.Context, key string) ([]byte, string, error) {
2662 if key == p.key {
2663 p.n++
2664 }
2665 return p.Store.Get(ctx, key)
2666}
2667
2668// The 2-3 SECOND canary fast loop must never pay a production-flag bucket
2669// read: canary/* has no window (DefaultHolds), so no canary-only pass ever
2670// has a HELD head to ask about. The hourly pass pays exactly one read per
2671// (account, app) with a held head — not one per channel, not one per route.
2672func TestCanaryFastLoopPaysNoProductionRead(t *testing.T) {
2673 dir := NewDirStore(t.TempDir())
2674 ctx := context.Background()
2675 priv, pub := testKeys(t)
2676 opt := t.TempDir()
2677 reg := testRegistry(t)
2678
2679 shipRelease(t, dir, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2680 for _, ch := range []string{"canary/x", "canary/y", "stable"} {
2681 if err := Promote(ctx, dir, priv, "bab", "myapp", ch, "v1"); err != nil {
2682 t.Fatalf("Promote %s: %v", ch, err)
2683 }
2684 }
2685 if err := SetProduction(ctx, dir, priv, "bab", "myapp", true, "test"); err != nil {
2686 t.Fatal(err)
2687 }
2688 addRoute(t, reg, "cx.example.com", "myapp", "canary/x", false)
2689 addRoute(t, reg, "cy.example.com", "myapp", "canary/y", false)
2690 addRoute(t, reg, "s.example.com", "myapp", "stable", false)
2691
2692 s := &prodReadStore{Store: dir, key: ProductionKey("bab", "myapp")}
2693 if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
2694 t.Fatalf("PassFilter: %v", err)
2695 }
2696 if s.n != 0 {
2697 t.Fatalf("canary-only pass read the production flag %d times, want 0 — the fast loop must not pay bucket reads for a window canary/* does not have", s.n)
2698 }
2699
2700 // Not vacuous: the full pass, which DOES resolve a held head, reads it —
2701 // once, for two canary channels, a stable one and three routes.
2702 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2703 t.Fatalf("Pass: %v", err)
2704 }
2705 if s.n != 1 {
2706 t.Fatalf("full pass read the production flag %d times, want exactly 1 per (account, app)", s.n)
2707 }
2708}
2709
2710// A NARROWED pass (the canary fast loop) must never repair a tainted clock:
2711// it fetched no production flags, so its Production map is empty, and writing
2712// it would replace the corrupt file with that emptiness AND clear the taint
2713// that is currently failing closed — before anything re-observed the flags.
2714// The full pass, which holds the flags, does the repair.
2715func TestNarrowedPassNeverRepairsATaintedClock(t *testing.T) {
2716 s := NewDirStore(t.TempDir())
2717 ctx := context.Background()
2718 priv, pub := testKeys(t)
2719 opt := t.TempDir()
2720 reg := testRegistry(t)
2721
2722 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2723 for _, ch := range []string{"canary/x", "stable"} {
2724 if err := Promote(ctx, s, priv, "bab", "myapp", ch, "v1"); err != nil {
2725 t.Fatalf("Promote %s: %v", ch, err)
2726 }
2727 }
2728 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
2729 t.Fatal(err)
2730 }
2731 addRoute(t, reg, "cx.example.com", "myapp", "canary/x", false)
2732 addRoute(t, reg, "s.example.com", "myapp", "stable", false)
2733
2734 if err := os.WriteFile(FirstSeenPath(opt), []byte("{not json"), 0o644); err != nil {
2735 t.Fatal(err)
2736 }
2737
2738 if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
2739 t.Fatalf("PassFilter: %v", err)
2740 }
2741 if raw, err := os.ReadFile(FirstSeenPath(opt)); err != nil {
2742 t.Fatal(err)
2743 } else if json.Valid(raw) {
2744 t.Fatalf("the canary fast loop overwrote a tainted clock — a pass that fetched no flags cannot repair what it would overwrite; file is now %q", raw)
2745 }
2746
2747 // The hourly pass has the flags in hand, so it repairs — and the app's
2748 // production state survives, re-latched from the bucket in that same pass.
2749 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2750 t.Fatalf("Pass: %v", err)
2751 }
2752 st := LoadFirstSeen(FirstSeenPath(opt))
2753 if st.tainted {
2754 t.Fatalf("the full pass did not repair the tainted clock")
2755 }
2756 if mem := st.Production[prodKey("bab", "myapp")]; mem == nil || !mem.WasProduction {
2757 t.Fatalf("production memory = %+v, want the latch re-established by the repairing pass", mem)
2758 }
2759}
2760
2761// The off-ratchet, end to end on the adopt path: flipping production OFF does
2762// not release a held head immediately — the off is honored only after the
2763// stable window has elapsed on this box's own sighting of it (spec §4). This
2764// is the dangerous direction, so it is pinned here and not only in bake_test:
2765// it is also what fails if ProductionBake's hasBakeableHead carve-out is ever
2766// passed a constant false from this call site.
2767func TestPassProductionOffStillRatchets(t *testing.T) {
2768 s := NewDirStore(t.TempDir())
2769 ctx := context.Background()
2770 priv, pub := testKeys(t)
2771 opt := t.TempDir()
2772 reg := testRegistry(t)
2773
2774 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2775 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
2776 t.Fatal(err)
2777 }
2778 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
2779 t.Fatal(err)
2780 }
2781 addRoute(t, reg, "h.example.com", "myapp", "", false)
2782
2783 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2784 t.Fatal(err)
2785 }
2786 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
2787 t.Fatalf("live = %q, want held (declared production)", v)
2788 }
2789
2790 // Flip it off. An operator who can flip the flag must not thereby have a
2791 // same-minute bypass of the window their held head is sitting in.
2792 if err := SetProduction(ctx, s, priv, "bab", "myapp", false, "test"); err != nil {
2793 t.Fatal(err)
2794 }
2795 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2796 t.Fatal(err)
2797 }
2798 if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
2799 t.Fatalf("live = %q, want STILL held — production: false must ratchet through the stable window, never release a held head on sight", v)
2800 }
2801 // And the box recorded the off-sighting, which is what the ratchet is
2802 // measured from — its own clock, never the flag's timestamp.
2803 mem := LoadFirstSeen(FirstSeenPath(opt)).Production[prodKey("bab", "myapp")]
2804 if mem == nil || mem.OffFirstSeen.IsZero() || !mem.WasProduction {
2805 t.Fatalf("production memory = %+v, want a latched app with an off-sighting recorded", mem)
2806 }
2807}
2808
2809// ---- pipeline windows on the adopt path (release pipelines v2 spec §3) ------
2810
2811// TestPassEnforcesPipelineWindows: an app with pipeline {edge(0),
2812// production(24h)}. Four behaviours in one lifecycle, in order: a release at
2813// the head of "production" is HELD even though the app's production flag is
2814// absent (pipelined apps never consult the flag); the same release at the
2815// head of "edge" adopts immediately; the declared 24h — narrower than the
2816// 72h a brand-new channel baselines at — ratchets in only after the OLD
2817// window has elapsed on this box's own clock, and then governs; and a
2818// FastTrack-re-signed pointer adopts immediately while held (spec §3
2819// amendment — ratcheting the waiver would make it wait out the very window
2820// it exists to skip).
2821func TestPassEnforcesPipelineWindows(t *testing.T) {
2822 s := NewDirStore(t.TempDir())
2823 ctx := context.Background()
2824 priv, pub := testKeys(t)
2825 opt := t.TempDir()
2826 reg := testRegistry(t)
2827
2828 // NO production flag is ever set in this test: a pipelined app's windows
2829 // come from the pipeline alone. (Legacy behaviour — no flag, no window —
2830 // would adopt the production head immediately.)
2831 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
2832 {Name: "edge"}, {Name: "production", Bake: "24h"},
2833 }, "test"); err != nil {
2834 t.Fatalf("SetPipeline: %v", err)
2835 }
2836 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin one"))
2837 for _, ch := range []string{"edge", "production"} {
2838 if err := Promote(ctx, s, priv, "bab", "myapp", ch, "v1"); err != nil {
2839 t.Fatalf("Promote %s: %v", ch, err)
2840 }
2841 }
2842 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
2843 addRoute(t, reg, "p.example.com", "myapp", "production", false)
2844
2845 // Sub-step 1: the production head is held with no flag in sight.
2846 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2847 t.Fatalf("Pass: %v", err)
2848 }
2849 if v, _ := LiveVersion(opt, "p.example.com"); v != "" {
2850 t.Fatalf("production live = %q, want held — a pipelined app's bake window must apply with no production flag at all", v)
2851 }
2852 // Sub-step 2: the zero-bake entry channel adopted in that same pass.
2853 if v, _ := LiveVersion(opt, "e.example.com"); v != "v1" {
2854 t.Fatalf("edge live = %q, want v1 — the pipeline's zero-bake channel adopts immediately", v)
2855 }
2856
2857 // Sub-step 3: the ratchet lands. The pass above latched the 72h
2858 // fail-closed baseline and parked the declared 24h as pending. Backdate
2859 // the box's own clocks: the pending sighting past the old window (72h
2860 // jittered < 90h < 91h), and v1's first sighting past 24h jittered
2861 // (< 30h < 31h) but NOT past 72h — adoption then proves the enforced
2862 // window really is the ratcheted-in 24h, not an expired baseline.
2863 st := LoadFirstSeen(FirstSeenPath(opt))
2864 key := channelClockKey("bab", "myapp", "production")
2865 mem := st.Holds[key]
2866 if mem == nil || mem.Window != 72*time.Hour || mem.Pending != 24*time.Hour || mem.PendingAt.IsZero() {
2867 t.Fatalf("hold memory = %+v, want the 72h baseline with 24h pending", mem)
2868 }
2869 mem.PendingAt = mem.PendingAt.Add(-91 * time.Hour)
2870 if seen, ok := st.Channels[key]["v1"]; !ok {
2871 t.Fatalf("no first sighting recorded for v1 at the production head")
2872 } else {
2873 st.Channels[key]["v1"] = seen.Add(-31 * time.Hour)
2874 }
2875 if err := st.Save(); err != nil {
2876 t.Fatalf("Save backdated clock: %v", err)
2877 }
2878 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2879 t.Fatalf("Pass: %v", err)
2880 }
2881 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
2882 t.Fatalf("production live = %q, want v1 — the ratcheted-in 24h window elapsed on the box's own clock", v)
2883 }
2884 // And the landing was PERSISTED by that pass: ObservePipeline's changed
2885 // flag must reach the Save path, or the promotion would re-run (and the
2886 // window claim silently regress) every pass until some other change
2887 // happened to force a write.
2888 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]; mem == nil || mem.Window != 24*time.Hour || !mem.PendingAt.IsZero() {
2889 t.Fatalf("persisted hold memory = %+v, want the landed 24h window", mem)
2890 }
2891
2892 // Sub-step 4: fast-track. v2 at the production head is inside the (now
2893 // 24h) window; the root-re-signed waiver releases it, unchanged by the
2894 // pipeline.
2895 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("bin two"))
2896 if err := Promote(ctx, s, priv, "bab", "myapp", "production", "v2"); err != nil {
2897 t.Fatalf("Promote v2: %v", err)
2898 }
2899 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2900 t.Fatalf("Pass: %v", err)
2901 }
2902 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
2903 t.Fatalf("production live = %q, want still v1 — v2 is inside the window until the waiver", v)
2904 }
2905 if _, err := FastTrack(ctx, s, priv, "bab", "myapp", "production", "test"); err != nil {
2906 t.Fatalf("FastTrack: %v", err)
2907 }
2908 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2909 t.Fatalf("Pass: %v", err)
2910 }
2911 if v, _ := LiveVersion(opt, "p.example.com"); v != "v2" {
2912 t.Fatalf("production live = %q, want v2 — FastTrack must keep bypassing a pipelined hold", v)
2913 }
2914}
2915
2916// TestPassPipelinedAppCanaryKeepsNoWindow: canary/* lives OUTSIDE the
2917// pipeline (spec §2), so a pipelined app's canary routes must keep their
2918// zero window. The trap is structural: HoldsForApp's map carries only the
2919// pipeline's channels, and HoldFor's fallback for a name missing from the
2920// map is the 72h fail-closed default — without canary/* stitched back in at
2921// zero, declaring a pipeline would freeze every canary deploy of the app.
2922func TestPassPipelinedAppCanaryKeepsNoWindow(t *testing.T) {
2923 s := NewDirStore(t.TempDir())
2924 ctx := context.Background()
2925 priv, pub := testKeys(t)
2926 opt := t.TempDir()
2927 reg := testRegistry(t)
2928
2929 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
2930 {Name: "edge"}, {Name: "production", Bake: "24h"},
2931 }, "test"); err != nil {
2932 t.Fatalf("SetPipeline: %v", err)
2933 }
2934 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2935 for _, ch := range []string{"canary/x", "production"} {
2936 if err := Promote(ctx, s, priv, "bab", "myapp", ch, "v1"); err != nil {
2937 t.Fatalf("Promote %s: %v", ch, err)
2938 }
2939 }
2940 addRoute(t, reg, "c.example.com", "myapp", "canary/x", false)
2941 addRoute(t, reg, "p.example.com", "myapp", "production", false)
2942
2943 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
2944 t.Fatalf("Pass: %v", err)
2945 }
2946 if v, _ := LiveVersion(opt, "c.example.com"); v != "v1" {
2947 t.Fatalf("canary live = %q, want v1 — canary/* is outside the pipeline and must keep its zero window", v)
2948 }
2949 // Not vacuous: the held production head in the SAME pass proves the
2950 // pipelined holds map (not DefaultHolds-with-no-flag, which would have
2951 // adopted it) is what governed — including the canary route above.
2952 if v, _ := LiveVersion(opt, "p.example.com"); v != "" {
2953 t.Fatalf("production live = %q, want held — without the hold this test proves nothing about the map the canary resolved through", v)
2954 }
2955}
2956
2957// TestPassCorruptPipelineFailsTowardLegacyHolds: a pipeline object that
2958// EXISTS but will not read or verify is not a pipeline and not an absence —
2959// the app falls back to the legacy path (DefaultHolds + the production
2960// flag), the same fail-closed posture an old binary that cannot parse
2961// pipeline.json takes, whose mitigation is that enabling a nonzero-bake
2962// pipeline also sets production: true (spec §3). And the errored read must
2963// NEVER reach the ratchet memory as a nil: nil means verified-absent, and a
2964// nil fold would land pending promotions — a corrupt object masquerading as
2965// a deletion.
2966func TestPassCorruptPipelineFailsTowardLegacyHolds(t *testing.T) {
2967 s := NewDirStore(t.TempDir())
2968 ctx := context.Background()
2969 priv, pub := testKeys(t)
2970 opt := t.TempDir()
2971 reg := testRegistry(t)
2972
2973 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
2974 if err := Promote(ctx, s, priv, "bab", "myapp", "production", "v1"); err != nil {
2975 t.Fatalf("Promote: %v", err)
2976 }
2977 // The flag the console sets alongside any nonzero-bake pipeline.
2978 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
2979 t.Fatalf("SetProduction: %v", err)
2980 }
2981 if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), []byte("not a wrapper"), ""); err != nil {
2982 t.Fatalf("corrupt the pipeline object: %v", err)
2983 }
2984 addRoute(t, reg, "p.example.com", "myapp", "production", false)
2985
2986 // Pre-seed ratchet memory with a pending loosening whose old window has
2987 // long elapsed, and a v1 sighting past the pending 24h but inside the
2988 // enforced 72h. If the errored read were wrongly folded as nil, the
2989 // promotion would land and the memory would read 24h below.
2990 now := time.Now()
2991 st := LoadFirstSeen(FirstSeenPath(opt))
2992 key := channelClockKey("bab", "myapp", "production")
2993 st.Holds[key] = &holdMemory{
2994 Window: 72 * time.Hour, SetAt: now.Add(-100 * time.Hour),
2995 Pending: 24 * time.Hour, PendingAt: now.Add(-91 * time.Hour),
2996 }
2997 st.Channels[key] = map[string]time.Time{"v1": now.Add(-31 * time.Hour)}
2998 if err := st.Save(); err != nil {
2999 t.Fatalf("Save pre-seeded clock: %v", err)
3000 }
3001
3002 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3003 t.Fatalf("Pass: %v", err)
3004 }
3005 if v, _ := LiveVersion(opt, "p.example.com"); v != "" {
3006 t.Fatalf("live = %q, want held — an unreadable pipeline must fall to legacy holds with the flag applying, never adopt free", v)
3007 }
3008 mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]
3009 if mem == nil || mem.Window != 72*time.Hour || mem.PendingAt.IsZero() {
3010 t.Fatalf("hold memory after the errored read = %+v, want untouched (72h enforced, promotion still pending) — an errored read must never tick the ratchet", mem)
3011 }
3012}
3013
3014// TestPassErroredPipelineReadKeepsTightenedWindows: the memory floor. A
3015// verified pipeline tightened edge's window to 24h — and DefaultHolds gives
3016// edge ZERO, so a plain fall-back to DefaultHolds on a later errored read
3017// would adopt instantly: one transient GET or verify failure silently
3018// defeating the exact window the bake exists to provide. Spec §3 is
3019// binding: corruption must never loosen — on an errored read, every channel
3020// the box's own ratchet remembers is enforced at max(its DefaultHolds
3021// resolution, the remembered window).
3022func TestPassErroredPipelineReadKeepsTightenedWindows(t *testing.T) {
3023 s := NewDirStore(t.TempDir())
3024 ctx := context.Background()
3025 priv, pub := testKeys(t)
3026 opt := t.TempDir()
3027 reg := testRegistry(t)
3028
3029 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
3030 {Name: "edge", Bake: "24h"},
3031 }, "test"); err != nil {
3032 t.Fatalf("SetPipeline: %v", err)
3033 }
3034 // The flag the console co-writes with any nonzero-bake pipeline.
3035 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
3036 t.Fatalf("SetProduction: %v", err)
3037 }
3038 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3039 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3040 t.Fatalf("Promote: %v", err)
3041 }
3042 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3043
3044 // Pass 1, pipeline verified: edge's zero baseline tightens to 24h
3045 // immediately (a tighten is never pending), and v1's clock starts — held.
3046 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3047 t.Fatalf("Pass: %v", err)
3048 }
3049 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3050 t.Fatalf("live = %q, want held — the verified pipeline tightened edge to 24h", v)
3051 }
3052 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[channelClockKey("bab", "myapp", "edge")]; mem == nil || mem.Window != 24*time.Hour {
3053 t.Fatalf("hold memory = %+v, want edge tightened to 24h", mem)
3054 }
3055
3056 // Corrupt the object in place. The next read ERRORS (exists, won't
3057 // verify) — and must not hand edge back its zero legacy window.
3058 _, etag, err := s.Get(ctx, PipelineKey("bab", "myapp"))
3059 if err != nil {
3060 t.Fatalf("read pipeline etag: %v", err)
3061 }
3062 if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), []byte("not a wrapper"), etag); err != nil {
3063 t.Fatalf("corrupt the pipeline object: %v", err)
3064 }
3065 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3066 t.Fatalf("Pass: %v", err)
3067 }
3068 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3069 t.Fatalf("live = %q, want STILL held — an errored read must keep the ratcheted 24h floor on edge, never fall to its zero legacy window", v)
3070 }
3071}
3072
3073// TestPassDeletedPipelineRatchetsDown: deleting pipeline.json is a plain
3074// bucket write — no signing key — so verified absence must never collapse a
3075// ratcheted window instantly. Deletion ratchets DOWN to the legacy defaults
3076// instead: each remembered channel keeps its enforced window for one full
3077// old-window cycle, then lands at HoldFor(DefaultHolds, name) and the app
3078// resumes a legacy-shaped pass.
3079func TestPassDeletedPipelineRatchetsDown(t *testing.T) {
3080 s := NewDirStore(t.TempDir())
3081 ctx := context.Background()
3082 priv, pub := testKeys(t)
3083 opt := t.TempDir()
3084 reg := testRegistry(t)
3085
3086 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
3087 {Name: "edge", Bake: "24h"},
3088 }, "test"); err != nil {
3089 t.Fatalf("SetPipeline: %v", err)
3090 }
3091 // The flag the console co-writes with any nonzero-bake pipeline.
3092 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
3093 t.Fatalf("SetProduction: %v", err)
3094 }
3095 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3096 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3097 t.Fatalf("Promote: %v", err)
3098 }
3099 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3100
3101 // Pass 1, pipeline verified: edge tightens to 24h immediately — held.
3102 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3103 t.Fatalf("Pass: %v", err)
3104 }
3105 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3106 t.Fatalf("live = %q, want held — the verified pipeline tightened edge to 24h", v)
3107 }
3108
3109 // The attack: delete the object outright.
3110 if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
3111 t.Fatalf("delete pipeline: %v", err)
3112 }
3113
3114 // Pass 2: STILL held. Bare DefaultHolds would hand edge its zero legacy
3115 // window back and adopt v1 in this very pass.
3116 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3117 t.Fatalf("Pass: %v", err)
3118 }
3119 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3120 t.Fatalf("live = %q, want STILL held — deletion must ratchet the 24h window down, never collapse it", v)
3121 }
3122 key := channelClockKey("bab", "myapp", "edge")
3123 st := LoadFirstSeen(FirstSeenPath(opt))
3124 mem := st.Holds[key]
3125 if mem == nil || mem.Window != 24*time.Hour || mem.Pending != 0 || mem.PendingAt.IsZero() {
3126 t.Fatalf("hold memory = %+v, want 24h enforced with the legacy 0 pending", mem)
3127 }
3128 pendingAt := mem.PendingAt
3129
3130 // Pass 3, absence re-observed: the pending clock must NOT restart.
3131 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3132 t.Fatalf("Pass: %v", err)
3133 }
3134 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]; mem == nil || !mem.PendingAt.Equal(pendingAt) {
3135 t.Fatalf("re-observed absence restarted the pending clock: %+v, want PendingAt %v", mem, pendingAt)
3136 }
3137
3138 // Elapse the OLD window on the box's own clock (24h jittered < 30h <
3139 // 31h). Next pass: the legacy default lands, edge is back at zero, and a
3140 // legacy-shaped pass adopts v1.
3141 st = LoadFirstSeen(FirstSeenPath(opt))
3142 st.Holds[key].PendingAt = st.Holds[key].PendingAt.Add(-31 * time.Hour)
3143 if seen, ok := st.Channels[key]["v1"]; ok {
3144 st.Channels[key]["v1"] = seen.Add(-31 * time.Hour)
3145 }
3146 if err := st.Save(); err != nil {
3147 t.Fatalf("Save backdated clock: %v", err)
3148 }
3149 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3150 t.Fatalf("Pass: %v", err)
3151 }
3152 if v, _ := LiveVersion(opt, "e.example.com"); v != "v1" {
3153 t.Fatalf("live = %q, want v1 — the old window elapsed, so legacy holds (edge 0) resume", v)
3154 }
3155 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]; mem == nil || mem.Window != 0 || !mem.PendingAt.IsZero() {
3156 t.Fatalf("persisted hold memory = %+v, want the landed legacy 0", mem)
3157 }
3158}
3159
3160// ratchetedProductionApp arranges the case the whole app-defined-channel
3161// posture turns on, and the one the edge-named tests below cannot reach: a
3162// FLAGLESS app whose pipeline names a channel OUTSIDE the frozen vocabulary
3163// ("production"), with a bake SHORTER than that name's legacy resolution.
3164// HoldFor's fallback for an unrecognised name is the 72h fail-closed default,
3165// so the declared 24h is a LOOSENING that ratchets in over the old window —
3166// and once it has, the box enforces 24h while DefaultHolds still resolves the
3167// same name at 72h. Any "is this app pipelined" test phrased as "does the
3168// memory RAISE a window above legacy" answers NO here, which is exactly the
3169// shape that made an app-defined channel the one place the verdict collapsed.
3170//
3171// Leaves: v1 adopted, the memory ratcheted in at 24h with nothing pending,
3172// and v2 at the production head INSIDE that 24h window — held, on the
3173// pipeline's word alone, with no production flag anywhere.
3174func ratchetedProductionApp(t *testing.T, s Store, ctx context.Context, priv ed25519.PrivateKey, pub ed25519.PublicKey, opt string, reg *registry.Registry) {
3175 t.Helper()
3176 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
3177 {Name: "edge"}, {Name: "production", Bake: "24h"},
3178 }, "test"); err != nil {
3179 t.Fatalf("SetPipeline: %v", err)
3180 }
3181 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin one"))
3182 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3183 t.Fatalf("Promote edge: %v", err)
3184 }
3185 if err := Promote(ctx, s, priv, "bab", "myapp", "production", "v1"); err != nil {
3186 t.Fatalf("Promote production: %v", err)
3187 }
3188 addRoute(t, reg, "p.example.com", "myapp", "production", false)
3189
3190 // Pass 1 latches the 72h fail-closed baseline and parks the declared 24h
3191 // as pending — held.
3192 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3193 t.Fatalf("Pass 1: %v", err)
3194 }
3195 if v, _ := LiveVersion(opt, "p.example.com"); v != "" {
3196 t.Fatalf("live = %q, want held at the 72h baseline", v)
3197 }
3198 // Elapse the OLD window on the box's own clock so the 24h lands, and age
3199 // v1 past 24h (but not past 72h) so adoption proves WHICH window governs.
3200 key := channelClockKey("bab", "myapp", "production")
3201 st := LoadFirstSeen(FirstSeenPath(opt))
3202 mem := st.Holds[key]
3203 if mem == nil || mem.Window != 72*time.Hour || mem.Pending != 24*time.Hour {
3204 t.Fatalf("hold memory = %+v, want the 72h baseline with 24h pending", mem)
3205 }
3206 mem.PendingAt = mem.PendingAt.Add(-91 * time.Hour)
3207 if seen, ok := st.Channels[key]["v1"]; !ok {
3208 t.Fatalf("no first sighting recorded for v1")
3209 } else {
3210 st.Channels[key]["v1"] = seen.Add(-31 * time.Hour)
3211 }
3212 if err := st.Save(); err != nil {
3213 t.Fatalf("Save backdated clock: %v", err)
3214 }
3215 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3216 t.Fatalf("Pass 2: %v", err)
3217 }
3218 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3219 t.Fatalf("live = %q, want v1 — the ratcheted-in 24h elapsed", v)
3220 }
3221 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]; mem == nil || mem.Window != 24*time.Hour || !mem.PendingAt.IsZero() {
3222 t.Fatalf("hold memory = %+v, want the landed 24h with nothing pending", mem)
3223 }
3224 // A fresh head inside the ratcheted window: held while the pipeline reads.
3225 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("bin two"))
3226 if err := Promote(ctx, s, priv, "bab", "myapp", "production", "v2"); err != nil {
3227 t.Fatalf("Promote v2: %v", err)
3228 }
3229 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3230 t.Fatalf("Pass 3: %v", err)
3231 }
3232 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3233 t.Fatalf("live = %q, want still v1 — v2 is inside the ratcheted 24h window", v)
3234 }
3235}
3236
3237// TestPassErroredReadOnAppDefinedChannelHoldsWithoutFlag is the C1 probe: the
3238// same collapse the edge-named test below covers, on the naming that actually
3239// hides it. With the memory ratcheted DOWN to 24h against a 72h legacy
3240// resolution, no window in the map is raised above legacy — so a verdict
3241// derived from the OVERLAY finds nothing, falls to the production flag this
3242// app has never had, and adopts v2 the moment one GET of pipeline.json errors.
3243// The evidence has to be the ratchet's own liveness, not the overlay.
3244func TestPassErroredReadOnAppDefinedChannelHoldsWithoutFlag(t *testing.T) {
3245 s := NewDirStore(t.TempDir())
3246 ctx := context.Background()
3247 priv, pub := testKeys(t)
3248 opt := t.TempDir()
3249 reg := testRegistry(t)
3250 ratchetedProductionApp(t, s, ctx, priv, pub, opt, reg)
3251
3252 _, etag, err := s.Get(ctx, PipelineKey("bab", "myapp"))
3253 if err != nil {
3254 t.Fatalf("read pipeline etag: %v", err)
3255 }
3256 if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), []byte("not a wrapper"), etag); err != nil {
3257 t.Fatalf("corrupt the pipeline object: %v", err)
3258 }
3259 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3260 t.Fatalf("Pass: %v", err)
3261 }
3262 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3263 t.Fatalf("live = %q, want still v1 — an errored read on an app-defined channel must keep the hold, with no production flag in sight", v)
3264 }
3265}
3266
3267// TestPassDeletedPipelineOnAppDefinedChannelHoldsWithoutFlag: the same probe
3268// on the other keyless bucket write. Deleting pipeline.json hands the channel
3269// its legacy 72h back — LONGER than the 24h the box was enforcing, so the
3270// window itself never loosens — but the app must not fall to the flagless
3271// legacy VERDICT in the same pass either, or one DELETE adopts a build that
3272// was minutes old. It returns to the legacy path only once the deletion
3273// ratchet has run the old window out on the box's own clock.
3274func TestPassDeletedPipelineOnAppDefinedChannelHoldsWithoutFlag(t *testing.T) {
3275 s := NewDirStore(t.TempDir())
3276 ctx := context.Background()
3277 priv, pub := testKeys(t)
3278 opt := t.TempDir()
3279 reg := testRegistry(t)
3280 ratchetedProductionApp(t, s, ctx, priv, pub, opt, reg)
3281
3282 if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
3283 t.Fatalf("delete pipeline: %v", err)
3284 }
3285 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3286 t.Fatalf("Pass: %v", err)
3287 }
3288 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3289 t.Fatalf("live = %q, want still v1 — deleting the pipeline must not adopt a held head in the same pass", v)
3290 }
3291
3292 // And it does return to legacy: elapse the deletion ratchet's own cycle on
3293 // the box's clock, and the app is a legacy app again — no floor, no
3294 // pipeline evidence, the flagless legacy verdict, so v2 adopts.
3295 key := channelClockKey("bab", "myapp", "production")
3296 st := LoadFirstSeen(FirstSeenPath(opt))
3297 mem := st.Holds[key]
3298 if mem == nil {
3299 t.Fatalf("no hold memory for the deleted pipeline's channel")
3300 }
3301 if mem.DeletedAt.IsZero() {
3302 t.Fatalf("hold memory = %+v, want the deletion stamped so its ratchet can run out", mem)
3303 }
3304 mem.DeletedAt = mem.DeletedAt.Add(-91 * time.Hour)
3305 st.Holds[key] = mem
3306 if err := st.Save(); err != nil {
3307 t.Fatalf("Save backdated clock: %v", err)
3308 }
3309 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3310 t.Fatalf("Pass: %v", err)
3311 }
3312 if v, _ := LiveVersion(opt, "p.example.com"); v != "v2" {
3313 t.Fatalf("live = %q, want v2 — once the deletion ratchet has run out the app is legacy again, and a flagless legacy app holds nothing", v)
3314 }
3315}
3316
3317// TestPassReplayedSubsetPipelineCannotPoisonTheDeletionStamp: the re-review
3318// probe (PR #258, IMPORTANT-1). The deletion stamp is what carries the bake
3319// verdict across a DELETE for an app-defined channel, so anything that can
3320// leave a STALE stamp standing — one that expires unnoticed and then blocks
3321// its own re-arming — is a way to strip the app of every ratchet it has.
3322//
3323// The chain, all of it keyless bucket writes plus one historical SIGNED
3324// object anybody who ever saw the bucket can keep a copy of:
3325//
3326// 1. DELETE pipeline.json — every remembered channel is stamped.
3327// 2. PUT BACK an OLD, still-validly-signed pipeline that names only a SUBSET
3328// of the app's channels (here: edge, not production). There is no
3329// object-level replay floor — only a per-channel SetAt one — so this is
3330// accepted as a verified read.
3331// 3. Dwell until the un-named channel's stale stamp has run out.
3332// 4. DELETE again. A stamp is never restamped while non-zero, so production
3333// is left with window == legacy, nothing pending and a dead stamp: no
3334// live ratchet, no evidence, and the flagless legacy verdict adopts a
3335// build that is minutes old.
3336//
3337// The fix is that a verified read clears the stamp for EVERY remembered
3338// channel of the app, not only the ones the object names — so step 2 can
3339// only ever DISARM the machinery, never leave a fuse burning behind it, and
3340// step 4 stamps afresh.
3341func TestPassReplayedSubsetPipelineCannotPoisonTheDeletionStamp(t *testing.T) {
3342 s := NewDirStore(t.TempDir())
3343 ctx := context.Background()
3344 priv, pub := testKeys(t)
3345 opt := t.TempDir()
3346 reg := testRegistry(t)
3347
3348 // The historical object: a legitimately signed, edge-only pipeline from
3349 // before production existed. Kept aside, exactly as an attacker with a
3350 // copy of yesterday's bucket would.
3351 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{{Name: "edge"}}, "test"); err != nil {
3352 t.Fatalf("SetPipeline (historical): %v", err)
3353 }
3354 historical, _, err := s.Get(ctx, PipelineKey("bab", "myapp"))
3355 if err != nil {
3356 t.Fatalf("read the historical pipeline: %v", err)
3357 }
3358 ratchetedProductionApp(t, s, ctx, priv, pub, opt, reg)
3359
3360 // 1. Delete: production is stamped, and its held head stays held.
3361 if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
3362 t.Fatalf("delete pipeline: %v", err)
3363 }
3364 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3365 t.Fatalf("Pass after delete: %v", err)
3366 }
3367 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3368 t.Fatalf("live = %q, want still v1 after the delete", v)
3369 }
3370
3371 // 2. Replay the historical edge-only object (create-only PUT: the key is
3372 // absent after the delete, which is precisely the opening).
3373 if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), historical, ""); err != nil {
3374 t.Fatalf("replay the historical pipeline: %v", err)
3375 }
3376 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3377 t.Fatalf("Pass after the replay: %v", err)
3378 }
3379 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3380 t.Fatalf("live = %q, want still v1 — a verified pipeline is the app being pipelined, not a reason to adopt", v)
3381 }
3382 // The property that makes step 4 safe: the verified read left NO stamp
3383 // standing anywhere under the app, including on the channel it never
3384 // named.
3385 st := LoadFirstSeen(FirstSeenPath(opt))
3386 for _, name := range []string{"edge", "production"} {
3387 if mem := st.Holds[channelClockKey("bab", "myapp", name)]; mem != nil && !mem.DeletedAt.IsZero() {
3388 t.Fatalf("%s keeps a deletion stamp (%v) after a verified read — a stale stamp can expire unnoticed and then refuse to re-arm", name, mem.DeletedAt)
3389 }
3390 }
3391
3392 // 3. Dwell. Anything still stamped ages out; v2's own first sighting is
3393 // deliberately NOT aged, so the only thing that can adopt it below is the
3394 // verdict collapsing.
3395 st = LoadFirstSeen(FirstSeenPath(opt))
3396 for _, name := range []string{"edge", "production"} {
3397 if mem := st.Holds[channelClockKey("bab", "myapp", name)]; mem != nil && !mem.DeletedAt.IsZero() {
3398 mem.DeletedAt = mem.DeletedAt.Add(-91 * time.Hour)
3399 }
3400 }
3401 if err := st.Save(); err != nil {
3402 t.Fatalf("Save dwelt clock: %v", err)
3403 }
3404
3405 // 4. Delete again. The stamp re-arms, so the head is still held.
3406 if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
3407 t.Fatalf("second delete: %v", err)
3408 }
3409 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3410 t.Fatalf("Pass after the second delete: %v", err)
3411 }
3412 if v, _ := LiveVersion(opt, "p.example.com"); v != "v1" {
3413 t.Fatalf("live = %q, want STILL v1 — a replayed subset pipeline must not be able to strip the app of its deletion ratchet", v)
3414 }
3415}
3416
3417// TestPassErroredPipelineHoldsWithoutProductionFlag: the memory floor is only
3418// half a defence if the app's held head is then ADOPTED anyway. The bake
3419// VERDICT ("does the window apply at all?") must ride on the same evidence
3420// the WINDOWS do — the box's own ratchet memory — not on the production flag
3421// the console co-writes. A co-write is a bucket write the box cannot verify
3422// happened: an app pipelined before that co-write existed, an app whose flag
3423// was deleted, a direct-bucket pipeline write — all leave a flagless app with
3424// a live ratchet memory, and a legacy verdict would adopt its held head the
3425// moment one GET of pipeline.json errored. NO production flag is set here.
3426func TestPassErroredPipelineHoldsWithoutProductionFlag(t *testing.T) {
3427 s := NewDirStore(t.TempDir())
3428 ctx := context.Background()
3429 priv, pub := testKeys(t)
3430 opt := t.TempDir()
3431 reg := testRegistry(t)
3432
3433 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
3434 {Name: "edge", Bake: "24h"},
3435 }, "test"); err != nil {
3436 t.Fatalf("SetPipeline: %v", err)
3437 }
3438 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3439 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3440 t.Fatalf("Promote: %v", err)
3441 }
3442 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3443
3444 // Pass 1, pipeline verified: edge's zero baseline tightens to 24h
3445 // immediately (a tighten is never pending) — held, flag or no flag.
3446 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3447 t.Fatalf("Pass: %v", err)
3448 }
3449 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3450 t.Fatalf("live = %q, want held — a pipelined app's window applies with no production flag at all", v)
3451 }
3452
3453 // Corrupt the object in place: the next read ERRORS.
3454 _, etag, err := s.Get(ctx, PipelineKey("bab", "myapp"))
3455 if err != nil {
3456 t.Fatalf("read pipeline etag: %v", err)
3457 }
3458 if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), []byte("not a wrapper"), etag); err != nil {
3459 t.Fatalf("corrupt the pipeline object: %v", err)
3460 }
3461 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3462 t.Fatalf("Pass: %v", err)
3463 }
3464 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3465 t.Fatalf("live = %q, want STILL held — the ratchet memory IS the evidence this app is pipelined; the verdict must not fall back to the production flag", v)
3466 }
3467 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[channelClockKey("bab", "myapp", "edge")]; mem == nil || mem.Window != 24*time.Hour {
3468 t.Fatalf("hold memory = %+v, want the 24h floor still remembered — otherwise this test proves nothing", mem)
3469 }
3470}
3471
3472// TestPassDeletedPipelineHoldsWithoutProductionFlag: same law on the
3473// verified-absent path. Deleting pipeline.json is a plain bucket write; the
3474// deletion ratchet keeps the 24h window enforced for one old-window cycle,
3475// and the VERDICT must honour it without any production flag — otherwise
3476// deleting one object adopts every held head of a flagless pipelined app in
3477// the very next pass, which is precisely the collapse the ratchet exists to
3478// prevent.
3479func TestPassDeletedPipelineHoldsWithoutProductionFlag(t *testing.T) {
3480 s := NewDirStore(t.TempDir())
3481 ctx := context.Background()
3482 priv, pub := testKeys(t)
3483 opt := t.TempDir()
3484 reg := testRegistry(t)
3485
3486 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
3487 {Name: "edge", Bake: "24h"},
3488 }, "test"); err != nil {
3489 t.Fatalf("SetPipeline: %v", err)
3490 }
3491 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3492 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3493 t.Fatalf("Promote: %v", err)
3494 }
3495 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3496
3497 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3498 t.Fatalf("Pass: %v", err)
3499 }
3500 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3501 t.Fatalf("live = %q, want held — the verified pipeline tightened edge to 24h", v)
3502 }
3503
3504 if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
3505 t.Fatalf("delete pipeline: %v", err)
3506 }
3507 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3508 t.Fatalf("Pass: %v", err)
3509 }
3510 if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
3511 t.Fatalf("live = %q, want STILL held — deletion ratchets the window down over a cycle, and the verdict must ride on that memory, not on a production flag nobody set", v)
3512 }
3513 key := channelClockKey("bab", "myapp", "edge")
3514 if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]; mem == nil || mem.Window != 24*time.Hour || mem.PendingAt.IsZero() {
3515 t.Fatalf("hold memory = %+v, want 24h enforced with the legacy 0 pending", mem)
3516 }
3517
3518 // And the ratchet still LANDS: once the old window has elapsed on the
3519 // box's own clock the app is legacy again — memory at the legacy default,
3520 // no floor map, and the bare-DefaultHolds verdict adopts. The fix must not
3521 // pin a once-pipelined app to a hold forever.
3522 st := LoadFirstSeen(FirstSeenPath(opt))
3523 st.Holds[key].PendingAt = st.Holds[key].PendingAt.Add(-31 * time.Hour)
3524 if seen, ok := st.Channels[key]["v1"]; ok {
3525 st.Channels[key]["v1"] = seen.Add(-31 * time.Hour)
3526 }
3527 if err := st.Save(); err != nil {
3528 t.Fatalf("Save backdated clock: %v", err)
3529 }
3530 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3531 t.Fatalf("Pass: %v", err)
3532 }
3533 if v, _ := LiveVersion(opt, "e.example.com"); v != "v1" {
3534 t.Fatalf("live = %q, want v1 — the old window elapsed, so legacy holds (edge 0) resume and the legacy verdict adopts", v)
3535 }
3536}
3537
3538// TestPassNoPipelineNoMemoryStaysLegacy pins the other half of the deletion
3539// ratchet's contract: an app with NO pipeline and NO ratchet memory — every
3540// legacy app on every pass — keeps the bare DefaultHolds path byte-identically.
3541// Edge adopts in the first pass, and no holds memory is minted from nothing.
3542func TestPassNoPipelineNoMemoryStaysLegacy(t *testing.T) {
3543 s := NewDirStore(t.TempDir())
3544 ctx := context.Background()
3545 priv, pub := testKeys(t)
3546 opt := t.TempDir()
3547 reg := testRegistry(t)
3548
3549 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3550 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3551 t.Fatalf("Promote: %v", err)
3552 }
3553 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3554 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3555 t.Fatalf("Pass: %v", err)
3556 }
3557 if v, _ := LiveVersion(opt, "e.example.com"); v != "v1" {
3558 t.Fatalf("live = %q, want v1 — a legacy app's edge adopts immediately, untouched", v)
3559 }
3560 if holds := LoadFirstSeen(FirstSeenPath(opt)).Holds; len(holds) != 0 {
3561 t.Fatalf("holds memory = %v, want none — no pipeline was ever observed", holds)
3562 }
3563}
3564
3565// TestPassNullHoldsEntryDoesNotPanic: {"holds":{"x":null}} parses cleanly
3566// into a nil *holdMemory, and the adopt pass's floor loops iterate the map.
3567// An unguarded mem.Window dereference panics the whole pass — fleet blast
3568// radius — on both the errored-read and the verified-absent path.
3569func TestPassNullHoldsEntryDoesNotPanic(t *testing.T) {
3570 s := NewDirStore(t.TempDir())
3571 ctx := context.Background()
3572 priv, pub := testKeys(t)
3573 opt := t.TempDir()
3574 reg := testRegistry(t)
3575
3576 if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
3577 {Name: "edge", Bake: "24h"},
3578 }, "test"); err != nil {
3579 t.Fatalf("SetPipeline: %v", err)
3580 }
3581 if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
3582 t.Fatalf("SetProduction: %v", err)
3583 }
3584 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3585 if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
3586 t.Fatalf("Promote: %v", err)
3587 }
3588 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3589
3590 // A clock file whose holds map carries a null entry — legal JSON, loads
3591 // clean, nil pointer in the map.
3592 blob := `{"jitter":0,"channels":{},"holds":{"` + channelClockKey("bab", "myapp", "edge") + `":null}}`
3593 if err := os.WriteFile(FirstSeenPath(opt), []byte(blob), 0o644); err != nil {
3594 t.Fatal(err)
3595 }
3596
3597 // Errored read: corrupt the pipeline object so the memory-floor loop runs.
3598 _, etag, err := s.Get(ctx, PipelineKey("bab", "myapp"))
3599 if err != nil {
3600 t.Fatalf("read pipeline etag: %v", err)
3601 }
3602 if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), []byte("not a wrapper"), etag); err != nil {
3603 t.Fatalf("corrupt the pipeline object: %v", err)
3604 }
3605 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3606 t.Fatalf("Pass over a null holds entry (errored read): %v", err)
3607 }
3608
3609 // Verified absence: same clock shape, object deleted outright.
3610 if err := os.WriteFile(FirstSeenPath(opt), []byte(blob), 0o644); err != nil {
3611 t.Fatal(err)
3612 }
3613 if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
3614 t.Fatalf("delete pipeline: %v", err)
3615 }
3616 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3617 t.Fatalf("Pass over a null holds entry (verified absent): %v", err)
3618 }
3619}
3620
3621// The 2-3 SECOND canary fast loop must never pay a pipeline bucket read
3622// either: canary/* is outside every pipeline, so no canary-only pass has a
3623// verdict the object could change — the mirror of
3624// TestCanaryFastLoopPaysNoProductionRead. The hourly pass pays exactly one
3625// read per (account, app) with a non-canary head — not one per channel, not
3626// one per route.
3627func TestCanaryFastLoopPaysNoPipelineRead(t *testing.T) {
3628 dir := NewDirStore(t.TempDir())
3629 ctx := context.Background()
3630 priv, pub := testKeys(t)
3631 opt := t.TempDir()
3632 reg := testRegistry(t)
3633
3634 shipRelease(t, dir, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
3635 for _, ch := range []string{"canary/x", "canary/y", "edge", "stable"} {
3636 if err := Promote(ctx, dir, priv, "bab", "myapp", ch, "v1"); err != nil {
3637 t.Fatalf("Promote %s: %v", ch, err)
3638 }
3639 }
3640 addRoute(t, reg, "cx.example.com", "myapp", "canary/x", false)
3641 addRoute(t, reg, "cy.example.com", "myapp", "canary/y", false)
3642 addRoute(t, reg, "e.example.com", "myapp", "edge", false)
3643 addRoute(t, reg, "s.example.com", "myapp", "stable", false)
3644
3645 s := &prodReadStore{Store: dir, key: PipelineKey("bab", "myapp")}
3646 if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
3647 t.Fatalf("PassFilter: %v", err)
3648 }
3649 if s.n != 0 {
3650 t.Fatalf("canary-only pass read the pipeline %d times, want 0 — canary/* is outside every pipeline", s.n)
3651 }
3652
3653 // Not vacuous: the full pass, with an edge and a stable head resolved,
3654 // reads it — once for the whole app.
3655 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
3656 t.Fatalf("Pass: %v", err)
3657 }
3658 if s.n != 1 {
3659 t.Fatalf("full pass read the pipeline %d times, want exactly 1 per (account, app)", s.n)
3660 }
3661}
3662
3663// ---- WithStartUnit: the rollback must not leave the service down ----
3664
3665// startUnitFixture is the shared setup of the WithStartUnit tests: one
3666// plain instance route live on "old" (staged locally, symlink pointed), with
3667// "new" promoted at the channel head; canary/* so no bake window applies.
3668// The route carries no Unit column — this package never reads it (the
3669// named-unit shape lives in internal/host's dispatch). What these tests
3670// model is the CALLBACK CONTRACT: a restart that fails the way a named
3671// unit's does (returns the error), and a start capability handed in through
3672// WithStartUnit.
3673func startUnitFixture(t *testing.T, host string) (context.Context, *registry.Registry, Store, ed25519.PublicKey, string) {
3674 t.Helper()
3675 s := NewDirStore(t.TempDir())
3676 ctx := context.Background()
3677 priv, pub := testKeys(t)
3678 opt := t.TempDir()
3679 reg := testRegistry(t)
3680
3681 shipRelease(t, s, "bab", "console", "old", adoptPlatform, []byte("good build"))
3682 shipRelease(t, s, "bab", "console", "new", adoptPlatform, []byte("bad build"))
3683 if err := StageArtifact(opt, "bab", "old", "console", []byte("good build")); err != nil {
3684 t.Fatal(err)
3685 }
3686 addRoute(t, reg, host, "console", "canary/x", false)
3687 if err := PointLive(opt, host, "bab", "old", "console"); err != nil {
3688 t.Fatal(err)
3689 }
3690 if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "new"); err != nil {
3691 t.Fatalf("Promote new: %v", err)
3692 }
3693 return ctx, reg, s, pub, opt
3694}
3695
3696// TestFailedRestartLeavesTheUnitRUNNINGOnTheOldVersion.
3697//
3698// The rollback repointed the symlink and stopped there, which left the box
3699// with a correct symlink and no process. Worse, try-restart is a NO-OP on a
3700// unit that is not running — so the NEXT pass exited 0, stamped SetVersions,
3701// and produced exactly the lying header the whole mechanism exists to
3702// prevent, over a service that was not running at all.
3703func TestFailedRestartLeavesTheUnitRUNNINGOnTheOldVersion(t *testing.T) {
3704 host := "console.example.com"
3705 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3706
3707 var started []string
3708 restart := func(string) error { return errors.New("Job for console.service failed") }
3709 start := func(h string) error { started = append(started, h); return nil }
3710
3711 if err := Pass(ctx, reg, s, pub, opt, restart, WithStartUnit(start)); err != nil {
3712 t.Fatalf("Pass: %v", err)
3713 }
3714
3715 if v, _ := LiveVersion(opt, host); v != "old" {
3716 t.Fatalf("symlink = %q, want it rolled back to old", v)
3717 }
3718 if len(started) != 1 || started[0] != host {
3719 t.Fatalf("started = %v, want the unit started once on the restored version — a rollback that leaves the service down is not a rollback", started)
3720 }
3721 rt, err := reg.Get(host)
3722 if err != nil || rt == nil {
3723 t.Fatalf("get %s: %+v, %v", host, rt, err)
3724 }
3725 if rt.Version == "new" {
3726 t.Fatal("SetVersions stamped the failed build")
3727 }
3728}
3729
3730// TestSecondPassOverAStillBrokenBuildDoesNotStamp: the second-order failure.
3731// With the unit running again after pass 1, pass 2's try-restart is a real
3732// restart that fails again — so it rolls back again and still does not stamp.
3733func TestSecondPassOverAStillBrokenBuildDoesNotStamp(t *testing.T) {
3734 host := "console.example.com"
3735 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3736
3737 // The fake is STATEFUL, and the state is the whole defect narrative: a
3738 // failed restart leaves the unit DOWN, and try-restart on a not-running
3739 // unit is an exit-0 NO-OP. With the fix reverted (no start on rollback)
3740 // pass 1 leaves running=false, pass 2's restart returns nil, and
3741 // SetVersions stamps "new" — this test's assertion fires. An
3742 // unconditionally-erroring fake could not fail on that revert.
3743 running := true
3744 restart := func(string) error {
3745 if !running {
3746 return nil // try-restart is a no-op on a stopped unit
3747 }
3748 running = false // the failed restart left it down
3749 return errors.New("still broken")
3750 }
3751 start := func(string) error { running = true; return nil }
3752 for i := 0; i < 2; i++ {
3753 if err := Pass(ctx, reg, s, pub, opt, restart, WithStartUnit(start)); err != nil {
3754 t.Fatalf("pass %d: %v", i, err)
3755 }
3756 }
3757 rt, err := reg.Get(host)
3758 if err != nil || rt == nil {
3759 t.Fatalf("get %s: %+v, %v", host, rt, err)
3760 }
3761 if rt.Version == "new" {
3762 t.Fatal("the second pass stamped a build that has never run")
3763 }
3764 if v, _ := LiveVersion(opt, host); v != "old" {
3765 t.Fatalf("symlink after two passes = %q, want old", v)
3766 }
3767}
3768
3769// TestStartFailureStaysPoisonedAndSaysSo: when the start ALSO fails the
3770// refusal stands exactly as before — poisoned, unstamped, rolled back — and
3771// the failure is printed, because at that point the box is genuinely broken
3772// and an operator reading only "rolled the symlink back" would take a host
3773// that is serving nothing for a recovered one.
3774func TestStartFailureStaysPoisonedAndSaysSo(t *testing.T) {
3775 host := "console.example.com"
3776 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3777
3778 restart := func(string) error { return errors.New("Job for console.service failed") }
3779 start := func(string) error { return errors.New("Job for console.service failed again") }
3780
3781 var out bytes.Buffer
3782 withCapturedStdout(t, &out, func() {
3783 if err := Pass(ctx, reg, s, pub, opt, restart, WithStartUnit(start)); err != nil {
3784 t.Fatalf("Pass: %v", err)
3785 }
3786 })
3787
3788 if v, _ := LiveVersion(opt, host); v != "old" {
3789 t.Fatalf("symlink = %q, want old", v)
3790 }
3791 rt, err := reg.Get(host)
3792 if err != nil || rt == nil {
3793 t.Fatalf("get %s: %+v, %v", host, rt, err)
3794 }
3795 if rt.Version == "new" {
3796 t.Fatal("a build whose restart AND start both failed was stamped")
3797 }
3798 if !strings.Contains(out.String(), "could not start it") {
3799 t.Fatalf("printed %q, want the failed start reported", out.String())
3800 }
3801}
3802
3803// TestSuccessfulRestartNeverConsultsStart: the start capability belongs to
3804// the ROLLBACK path alone. A restart that succeeds — the every-deploy case —
3805// must not touch it, or every ordinary adoption would grow a systemctl start.
3806func TestSuccessfulRestartNeverConsultsStart(t *testing.T) {
3807 host := "console.example.com"
3808 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3809
3810 restart := func(string) error { return nil }
3811 start := func(h string) error {
3812 t.Fatalf("start consulted for %q after a SUCCESSFUL restart", h)
3813 return nil
3814 }
3815 if err := Pass(ctx, reg, s, pub, opt, restart, WithStartUnit(start)); err != nil {
3816 t.Fatalf("Pass: %v", err)
3817 }
3818 if v, _ := LiveVersion(opt, host); v != "new" {
3819 t.Fatalf("live = %q, want new", v)
3820 }
3821}
3822
3823// TestFailedRollbackStartsNothing: the start is gated on the rollback
3824// having HAPPENED, not on a previous version existing. "Link present,
3825// staged version gone" is a documented real state (materialize.go — an
3826// operator's /opt cleanup, a partially restored volume): PointLive refuses
3827// to repoint at a version directory that is not there, the symlink still
3828// names the refused build, and a start would launch exactly that build.
3829// The refusal is also no longer silent — the pass says the rollback failed.
3830func TestFailedRollbackStartsNothing(t *testing.T) {
3831 host := "console.example.com"
3832 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3833
3834 // The operator's cleanup: old's staged directory is gone, so the
3835 // rollback's PointLive will refuse. The live symlink still dangles at
3836 // old, which is how adoptHost sees cur = "old" and attempts the update.
3837 if err := os.RemoveAll(filepath.Dir(VersionPath(opt, "bab", "old", "console"))); err != nil {
3838 t.Fatal(err)
3839 }
3840
3841 restart := func(string) error { return errors.New("Job for console.service failed") }
3842 start := func(h string) error {
3843 t.Fatalf("start consulted for %q after a rollback that DID NOT HAPPEN — the symlink still names the refused build", h)
3844 return nil
3845 }
3846 var out bytes.Buffer
3847 withCapturedStdout(t, &out, func() {
3848 if err := Pass(ctx, reg, s, pub, opt, restart, WithStartUnit(start)); err != nil {
3849 t.Fatalf("Pass: %v", err)
3850 }
3851 })
3852 if !strings.Contains(out.String(), "could not roll the symlink back") {
3853 t.Fatalf("printed %q, want the failed rollback reported instead of swallowed", out.String())
3854 }
3855 rt, err := reg.Get(host)
3856 if err != nil || rt == nil {
3857 t.Fatalf("get %s: %+v, %v", host, rt, err)
3858 }
3859 if rt.Version == "new" {
3860 t.Fatal("a build whose restart failed was stamped")
3861 }
3862}
3863
3864// TestFirstAdoptionRestartFailureStartsNothing: with no previous version
3865// (cur == "") the symlink could not be rolled back — it still names the
3866// version whose restart just failed — so there is no RESTORED version to
3867// start. Starting there would run the exact build the failure refused.
3868func TestFirstAdoptionRestartFailureStartsNothing(t *testing.T) {
3869 s := NewDirStore(t.TempDir())
3870 ctx := context.Background()
3871 priv, pub := testKeys(t)
3872 opt := t.TempDir()
3873 reg := testRegistry(t)
3874 host := "console.example.com"
3875
3876 shipRelease(t, s, "bab", "console", "new", adoptPlatform, []byte("bad build"))
3877 addRoute(t, reg, host, "console", "canary/x", false)
3878 if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "new"); err != nil {
3879 t.Fatalf("Promote: %v", err)
3880 }
3881
3882 restart := func(string) error { return errors.New("Job for console.service failed") }
3883 start := func(h string) error {
3884 t.Fatalf("start consulted for %q on a first adoption — nothing was rolled back, so there is nothing restored to start", h)
3885 return nil
3886 }
3887 if err := Pass(ctx, reg, s, pub, opt, restart, WithStartUnit(start)); err != nil {
3888 t.Fatalf("Pass: %v", err)
3889 }
3890 rt, err := reg.Get(host)
3891 if err != nil || rt == nil {
3892 t.Fatalf("get %s: %+v, %v", host, rt, err)
3893 }
3894 if rt.Version == "new" {
3895 t.Fatal("a build whose restart failed was stamped")
3896 }
3897}
3898
3899// TestRefusedPromotionIsNeitherStagedNorRelayed: a refusal naming the current
3900// pointer stops adoptHost before ensureStaged, so the agent never relays a
3901// root pass for it and nothing restarts.
3902func TestRefusedPromotionIsNeitherStagedNorRelayed(t *testing.T) {
3903 host := "console.example.com"
3904 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3905 bake := t.TempDir()
3906 ptr, err := Channel(ctx, s, pub, "bab", "console", "canary/x")
3907 if err != nil {
3908 t.Fatal(err)
3909 }
3910 if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: ptr.Version,
3911 PointerPromotedAt: ptr.PromotedAt, Reason: "exited: status=2"}); err != nil {
3912 t.Fatal(err)
3913 }
3914 restart := func(h string) error { t.Fatalf("restart consulted for refused %s", h); return nil }
3915 relayed := false
3916 var out bytes.Buffer
3917 withCapturedStdout(t, &out, func() {
3918 if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake),
3919 OnStageBlocked(func(string, string, error) { relayed = true })); err != nil {
3920 t.Fatalf("Pass: %v", err)
3921 }
3922 })
3923 if v, _ := LiveVersion(opt, host); v != "old" {
3924 t.Fatalf("live = %q, want old", v)
3925 }
3926 if relayed {
3927 t.Fatal("a refused promotion was relayed to root")
3928 }
3929 // The ordering this gate exists for: the check sits BEFORE ensureStaged.
3930 // Staging succeeds in this fixture, so "relayed" alone cannot tell the
3931 // gate's position; the absence of the build and its manifest sibling can.
3932 for _, p := range []string{
3933 VersionPath(opt, "bab", "new", "console"),
3934 StagedManifestPath(opt, "bab", "new", "console"),
3935 } {
3936 if _, err := os.Stat(p); !errors.Is(err, os.ErrNotExist) {
3937 t.Fatalf("refused version was staged: stat %s = %v, want not-exist", p, err)
3938 }
3939 }
3940 if !strings.Contains(out.String(), "promote --retry") {
3941 t.Fatalf("printed %q, want the retry hint", out.String())
3942 }
3943}
3944
3945// TestChannelBackOnTheRestoredBuildClearsTheRefusal (final review F8): v2
3946// was refused and rolled back to v1, then the channel was promoted back to
3947// v1. The pass finds the box up to date, restarts nothing, and removes the
3948// v2 record that no longer blocks anything.
3949func TestChannelBackOnTheRestoredBuildClearsTheRefusal(t *testing.T) {
3950 host := "console.example.com"
3951 s := NewDirStore(t.TempDir())
3952 ctx := context.Background()
3953 priv, pub := testKeys(t)
3954 opt := t.TempDir()
3955 reg := testRegistry(t)
3956 shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("good build"))
3957 shipRelease(t, s, "bab", "console", "v2", adoptPlatform, []byte("bad build"))
3958 if err := StageArtifact(opt, "bab", "v1", "console", []byte("good build")); err != nil {
3959 t.Fatal(err)
3960 }
3961 addRoute(t, reg, host, "console", "canary/x", false)
3962 if err := PointLive(opt, host, "bab", "v1", "console"); err != nil {
3963 t.Fatal(err)
3964 }
3965 if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "v1"); err != nil {
3966 t.Fatal(err)
3967 }
3968 bake := t.TempDir()
3969 if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: "v2",
3970 PointerPromotedAt: time.Unix(1, 0).UTC(), Reason: "exited: status=2", RestoredVersion: "v1", Recovered: true}); err != nil {
3971 t.Fatal(err)
3972 }
3973 restart := func(h string) error { t.Fatalf("restart consulted for up-to-date %s", h); return nil }
3974 if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake)); err != nil {
3975 t.Fatalf("Pass: %v", err)
3976 }
3977 if v, _ := LiveVersion(opt, host); v != "v1" {
3978 t.Fatalf("live = %q, want v1", v)
3979 }
3980 if r, _ := ReadRefusal(bake, host); r != nil {
3981 t.Fatalf("stale v2 refusal survived the channel's return to v1: %+v", r)
3982 }
3983}
3984
3985// TestUpToDateKeepsARefusalThatStillBlocks: the link is on the refused build
3986// only when its rollback failed; that record still names the pointer and
3987// stays for the watcher.
3988func TestUpToDateKeepsARefusalThatStillBlocks(t *testing.T) {
3989 host := "console.example.com"
3990 ctx, reg, s, pub, opt := startUnitFixture(t, host)
3991 if err := StageArtifact(opt, "bab", "new", "console", []byte("bad build")); err != nil {
3992 t.Fatal(err)
3993 }
3994 if err := PointLive(opt, host, "bab", "new", "console"); err != nil {
3995 t.Fatal(err)
3996 }
3997 ptr, err := Channel(ctx, s, pub, "bab", "console", "canary/x")
3998 if err != nil {
3999 t.Fatal(err)
4000 }
4001 bake := t.TempDir()
4002 if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: ptr.Version,
4003 PointerPromotedAt: ptr.PromotedAt, Reason: "exited: status=2", RecoveryError: "could not roll the symlink back"}); err != nil {
4004 t.Fatal(err)
4005 }
4006 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, BakeDir(bake)); err != nil {
4007 t.Fatalf("Pass: %v", err)
4008 }
4009 if r, _ := ReadRefusal(bake, host); r == nil {
4010 t.Fatal("a refusal that still blocks the pointer was removed")
4011 }
4012}
4013
4014// TestRePromotionClearsTheBlock: a pointer re-signed for the same version
4015// (what promote --retry does) is adopted, and the stale record is removed.
4016func TestRePromotionClearsTheBlock(t *testing.T) {
4017 host := "console.example.com"
4018 ctx, reg, s, pub, opt := startUnitFixture(t, host)
4019 bake := t.TempDir()
4020 if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: "new",
4021 PointerPromotedAt: time.Unix(1, 0).UTC(), Reason: "older promotion"}); err != nil {
4022 t.Fatal(err)
4023 }
4024 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, BakeDir(bake)); err != nil {
4025 t.Fatalf("Pass: %v", err)
4026 }
4027 if v, _ := LiveVersion(opt, host); v != "new" {
4028 t.Fatalf("live = %q, want new (the record names an older promotion)", v)
4029 }
4030 if r, _ := ReadRefusal(bake, host); r != nil {
4031 t.Fatalf("stale refusal survived a successful adoption: %+v", r)
4032 }
4033}
4034