package release import ( "context" "encoding/json" "errors" "os" "testing" "time" ) // TestSidecarWakeKeyIsAnAppSibling pins the key's shape: a SIBLING prefix // under the app's own account-scoped prefix, deliberately not a key under // sidecars/ — provision.ParseSidecarKey's "exactly one non-empty segment // after sidecars/" rule stays exactly as strict as it reads only if nothing // else ever lands under that prefix. func TestSidecarWakeKeyIsAnAppSibling(t *testing.T) { got, want := SidecarWakeKey("bdm", "eleven", "bot"), "apps/bdm/eleven/sidecar-wake/bot.json" if got != want { t.Fatalf("SidecarWakeKey = %q, want %q", got, want) } } // TestSidecarWakeJSONShape pins the wire shape against the exact example the // design settled on, so a field rename or a json-tag typo fails loudly // instead of silently breaking every box's etag comparison (which never // parses the body at all) while still breaking a human reading the object // during an incident. func TestSidecarWakeJSONShape(t *testing.T) { w := SidecarWake{ Schema: 1, Generation: 3, At: "2026-08-17T23:05:00Z", By: "member:bdm", Reason: "operator wake", } raw, err := json.Marshal(w) if err != nil { t.Fatalf("marshal: %v", err) } const want = `{"schema":1,"generation":3,"at":"2026-08-17T23:05:00Z","by":"member:bdm","reason":"operator wake"}` if string(raw) != want { t.Fatalf("SidecarWake JSON = %s, want %s", raw, want) } var back SidecarWake if err := json.Unmarshal(raw, &back); err != nil { t.Fatalf("unmarshal: %v", err) } if back != w { t.Fatalf("round trip = %+v, want %+v", back, w) } } // sidecarWakeEtag reads name's wake etag, "" for absent — the exact // observation a box's pass makes, so a test asserting on it asserts on the // signal itself rather than on this file's internals. func sidecarWakeEtag(t *testing.T, s Store, acct, app, name string) string { t.Helper() _, etag, err := s.Get(context.Background(), SidecarWakeKey(acct, app, name)) if err != nil { if errors.Is(err, os.ErrNotExist) { return "" } t.Fatalf("read sidecar wake: %v", err) } return etag } func TestTouchSidecarWakeCreatesAndMoves(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() if err := TouchSidecarWake(ctx, s, "bdm", "eleven", "bot", "member:bdm", "operator wake"); err != nil { t.Fatalf("first TouchSidecarWake: %v", err) } raw, first, err := s.Get(ctx, SidecarWakeKey("bdm", "eleven", "bot")) if err != nil { t.Fatalf("Get after first touch: %v", err) } var w SidecarWake if err := json.Unmarshal(raw, &w); err != nil { t.Fatalf("decode sidecar wake: %v", err) } if w.Schema != 1 { t.Fatalf("Schema = %d, want 1", w.Schema) } if w.Generation != 1 { t.Fatalf("Generation = %d, want 1 on first touch", w.Generation) } if w.By != "member:bdm" || w.Reason != "operator wake" { t.Fatalf("By/Reason = %q/%q, want the values passed in", w.By, w.Reason) } if _, err := time.Parse(time.RFC3339, w.At); err != nil { t.Fatalf("At %q is not RFC3339: %v", w.At, err) } // The etag is the whole point: a second request must be a DIFFERENT // object, and Generation must climb, or the box's comparison sees // nothing and nothing wakes. if err := TouchSidecarWake(ctx, s, "bdm", "eleven", "bot", "member:bdm", "second wake"); err != nil { t.Fatalf("second TouchSidecarWake: %v", err) } second := sidecarWakeEtag(t, s, "bdm", "eleven", "bot") if second == first { t.Fatalf("etag %q unchanged after a second request — the wake signal never fires", second) } raw2, _, err := s.Get(ctx, SidecarWakeKey("bdm", "eleven", "bot")) if err != nil { t.Fatalf("Get after second touch: %v", err) } var w2 SidecarWake if err := json.Unmarshal(raw2, &w2); err != nil { t.Fatalf("decode sidecar wake: %v", err) } if w2.Generation != 2 { t.Fatalf("Generation after second touch = %d, want 2", w2.Generation) } } // racingSidecarWakePutStore lands a different writer's value at key just // before forwarding the caller's Put, which then fails CAS against the etag // the caller read — two operators (or a person and a retrying CLI) asking // for a wake at the same moment. Mirrors racingRestartPutStore exactly. type racingSidecarWakePutStore struct { Store key string done bool } func (r *racingSidecarWakePutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { if key == r.key && !r.done { r.done = true other := []byte(`{"schema":1,"generation":1,"at":"2026-01-01T00:00:00Z","by":"member:other","reason":"racing"}`) if _, err := r.Store.Put(ctx, key, other, ifMatch); err != nil { return "", err } } return r.Store.Put(ctx, key, body, ifMatch) } func TestTouchSidecarWakeCASConflictIsSuccess(t *testing.T) { inner := NewDirStore(t.TempDir()) ctx := context.Background() racing := &racingSidecarWakePutStore{Store: inner, key: SidecarWakeKey("bdm", "eleven", "bot")} if err := TouchSidecarWake(ctx, racing, "bdm", "eleven", "bot", "member:bdm", "operator wake"); err != nil { t.Fatalf("TouchSidecarWake on a lost CAS race: %v, want nil (the request already fired)", err) } if !racing.done { t.Fatal("the racing writer never ran — this test proved nothing") } } func TestTouchSidecarWakeRefusesInvalidNames(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() if err := TouchSidecarWake(ctx, s, "../etc", "eleven", "bot", "member:bdm", "x"); err == nil { t.Fatal("TouchSidecarWake accepted an invalid account") } if err := TouchSidecarWake(ctx, s, "bdm", "../etc", "bot", "member:bdm", "x"); err == nil { t.Fatal("TouchSidecarWake accepted an invalid app") } if err := TouchSidecarWake(ctx, s, "bdm", "eleven", "../etc", "member:bdm", "x"); err == nil { t.Fatal("TouchSidecarWake accepted an invalid sidecar name") } }