package host import ( "context" "crypto/ed25519" "encoding/json" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func TestSidecarWakeOldIdentityCannotWakeReplacement(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "") ctx := context.Background() s := release.NewDirStore(t.TempDir()) host := provision.SidecarHost("bot", "app", "acct") at := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC) inc, err := hostclaim.Incarnation("acct", "app", host, at) if err != nil { t.Fatal(err) } _, err = hostclaim.Update(ctx, s, hostclaim.Observed{Host: host, Absent: true}, func(c *hostclaim.Claim) error { c.Holder = &hostclaim.Holder{Family: "sidecar", Key: provision.SidecarRecordKey("acct", "app", "bot"), Account: "acct", App: "app", Name: "bot", CreatedAt: at, Incarnation: inc, Location: "instances/" + host + "/" + inc, Stamp: 1, Session: 1, WriterBox: "box"} c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"} c.Touched.Complete = true return nil }) if err != nil { t.Fatal(err) } owner := hostops.RegistrationOwner{Console: "console", Enrollment: "enrollment", Controller: "request"} reg := hostops.Registration{Kind: "wake", Host: host, Owner: owner, AppID: "new-app", Epoch: 1, Incarnation: inc, CreatedAt: at} key := release.SidecarWakeKey("acct", "app", "bot") effect := hostops.Effect{ID: "new-request", OperationID: "new-request", AppID: "new-app", Resource: "control-object:" + key, Action: "wake", Owner: owner, State: "complete", ResultReference: "new-request", Inputs: map[string]string{"key": key, "incarnation": inc, "request_id": "new-request", "admission_epoch": "1", "created_at": at.Format(time.RFC3339Nano)}} a := hostops.AppAdmission{V: 1, AppID: "new-app", Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{"new-request": reg}, Effects: map[string]hostops.Effect{"new-request": effect}} raw, _ := json.Marshal(a) if _, err = s.Put(ctx, "apps/acct/app/admission.json", raw, ""); err != nil { t.Fatal(err) } rt := registry.Route{Host: host, Kind: "sidecar", Slug: "bot", App: "app", Account: "acct"} mem := newSidecarWakeMemory(true) sidecarWakeChanges(ctx, nil, s, mem) if err = release.TouchSidecarWake(ctx, s, "acct", "app", "bot", "operator", "old", release.SidecarWakeIdentity{AppID: "old-app", Incarnation: inc, RequestID: "old-request"}); err != nil { t.Fatal(err) } moved, _ := sidecarWakeChanges(ctx, []registry.Route{rt}, s, mem) if len(moved) != 0 { t.Fatal("delayed old app request woke replacement") } if err = release.TouchSidecarWake(ctx, s, "acct", "app", "bot", "operator", "new", release.SidecarWakeIdentity{AppID: "new-app", Incarnation: inc, RequestID: "new-request"}); err != nil { t.Fatal(err) } moved, errs := sidecarWakeChanges(ctx, []registry.Route{rt}, s, mem) if len(errs) != 0 || len(moved) != 1 { t.Fatalf("new request: moved=%d errors=%v", len(moved), errs) } captured := mem.identities[host] pub, _, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } w, err := hostops.NewControlWriter(s, pub, time.Now) if err != nil { t.Fatal(err) } // Registration cleanup is the writer's settle, not a raw admission edit. if err = w.SettleRegistration(ctx, "acct", "app", "new-app", "new-request", time.Now()); err != nil { t.Fatal(err) } if cleaned, err := hostops.ReadAdmission(ctx, s, "acct", "app", "new-app"); err != nil || len(cleaned.Value.Registrations) != 0 { t.Fatalf("registration not settled: %v %+v", err, cleaned.Value.Registrations) } delayed := newSidecarWakeMemory(true) sidecarWakeChanges(ctx, nil, s, delayed) moved, errs = sidecarWakeChanges(ctx, []registry.Route{rt}, s, delayed) if len(errs) != 0 || len(moved) != 1 { t.Fatalf("completed wake lost after registration cleanup: moved=%d errors=%v", len(moved), errs) } obs, err := hostclaim.Read(ctx, s, host) if err != nil { t.Fatal(err) } _, err = hostclaim.Update(ctx, s, obs, func(c *hostclaim.Claim) error { c.Holder.CreatedAt = c.Holder.CreatedAt.Add(time.Hour) c.Holder.Incarnation, _ = hostclaim.Incarnation(c.Holder.Account, c.Holder.App, c.Host, c.Holder.CreatedAt) c.Holder.Location = "instances/" + host + "/" + c.Holder.Incarnation return nil }) if err != nil { t.Fatal(err) } if err = verifySidecarWake(ctx, s, rt, captured); err == nil { t.Fatal("queued request survived replacement before dispatch") } } func TestSidecarWakeFlagOffIgnoresEnvironment(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") ctx := t.Context() s := release.NewDirStore(t.TempDir()) mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, nil, s, mem) rt := registry.Route{Host: provision.SidecarHost("bot", "app", "acct"), Kind: "sidecar", Slug: "bot", App: "app", Account: "acct"} if err := release.TouchSidecarWake(ctx, s, "acct", "app", "bot", "operator", "wake"); err != nil { t.Fatal(err) } woke := make(chan registry.Route, 1) moved, errs := sidecarWakeLeg(ctx, []registry.Route{rt}, s, mem, func(r registry.Route) error { woke <- r; return nil }) if len(moved) != 1 || len(errs) != 0 { t.Fatalf("flag-off legacy wake: moved=%v errors=%v", moved, errs) } select { case got := <-woke: if got.Host != rt.Host || got.WakeIncarnation != "" { t.Fatalf("legacy wake changed: %+v", got) } case <-time.After(time.Second): t.Fatal("legacy dispatch did not wake") } }