package console // The seam between the two halves of instance provisioning: the console // verbs that write records to the bucket (apiinstances.go) and the agent // pass that turns them into routes (internal/provision). They share no code // path and no process — only key shapes and an account sqid — which is // exactly the kind of agreement that breaks silently. import ( "context" "net/http" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // TestAPIInstanceReconcileSeam is the seam spec §13 names: the console's // verbs and the agent's reconcile pass, joined by nothing but the bucket. // Every literal that could drift between them is computed, never typed — // the account segment comes from console's own accountSqid (domains.Sqid), // the record key from provision.RecordKey, the route's paths from the // provisioner's shared helpers — because the failure this pins is silent: // a console writing apps//... and an agent parsing apps//... // both "work", and instances simply never appear. // // It runs on a NON-ops account deliberately: ops's sqid is hardcoded known // on the agent side (domains.OpsSqid), so an ops-only test would pass with // the sqid agreement completely broken. func TestAPIInstanceReconcileSeam(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "alice@keymail.dev", "code-inst-seam") now := time.Now() const acctID int64 = 4242 sqid := accountSqid(acctID) if err := fx.st.CreateAccount(acctID, sqid, "Tito", "alice@keymail.dev", now); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(acctID, "titogo", "alice@keymail.dev", now); err != nil { t.Fatal(err) } // The account manifest is how the agent learns this sqid exists at all // (ParseRecordKey refuses keys under an unknown account). if err := WriteAccountManifest(context.Background(), fx.rel, acctID, sqid, "Tito", now); err != nil { t.Fatal(err) } if err := provision.PutManifest(context.Background(), fx.rel, sqid, "titogo", titoManifest()); err != nil { t.Fatal(err) } u := instancesURL(fx, sqid, "titogo") const host = "acme.go.tito.io" if status, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"`+host+`","region":"ie"}`); status != http.StatusCreated { t.Fatalf("create = %d %s", status, out) } // A hand-added neighbour in the SAME scope: not provisioned, so the // removal phase has no authority over it however the rest of the pass // goes. dataDir := t.TempDir() neighbour := registry.Route{ Host: "hand.go.tito.io", Slug: "hand", Kind: "instance", Socket: provision.DefaultSocket("hand.go.tito.io"), DBPath: provision.DefaultDBPath(dataDir, "hand.go.tito.io"), App: "titogo", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", Status: "ready", } reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) if err := reg.Upsert(neighbour); err != nil { t.Fatal(err) } before, err := reg.Get(neighbour.Host) if err != nil || before == nil { t.Fatalf("neighbour route: %v", err) } cfg := provision.ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: dataDir} if err := provision.Reconcile(context.Background(), fx.rel, reg, cfg); err != nil { t.Fatalf("reconcile: %v", err) } got, err := reg.Get(host) if err != nil { t.Fatal(err) } if got == nil { t.Fatalf("the console's record never materialized as a route — the sqid or key shapes disagree") } want := registry.Route{ Host: host, Slug: "acme", Kind: "instance", Socket: provision.DefaultSocket(host), DBPath: provision.DefaultDBPath(dataDir, host), App: "titogo", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", Hibernate: true, Provisioned: true, Seed: `["seed"]`, Status: "ready", CreatedAt: got.CreatedAt, } if *got != want { t.Fatalf("minted route =\n %+v\nwant\n %+v", *got, want) } // And back the other way: the DELETE verb is what retires the route. if status, _, out := rawReq(t, http.MethodDelete, u+"/"+host, token, ""); status != http.StatusOK { t.Fatalf("delete = %d %s", status, out) } if err := provision.Reconcile(context.Background(), fx.rel, reg, cfg); err != nil { t.Fatalf("reconcile after delete: %v", err) } if gone, err := reg.Get(host); err != nil { t.Fatal(err) } else if gone != nil { t.Fatalf("the route survived its record's deletion: %+v", gone) } after, err := reg.Get(neighbour.Host) if err != nil || after == nil { t.Fatalf("the hand-added neighbour was removed by a pass that never minted it: %v", err) } if *after != *before { t.Fatalf("the hand-added neighbour changed under reconciliation:\n got %+v\nwant %+v", *after, *before) } }