package console import ( "context" "errors" "fmt" "net/http" "net/url" "path/filepath" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/caps" "github.com/carlosframework/platform/internal/plans" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // atRunningCap turns plans on and leaves the fixture's ops account with // exactly capN running things against a cap of capN (capN >= the free // plan's 3). func atRunningCap(t *testing.T, fx *consoleFixture, capN int) *Account { t.Helper() fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct, err := fx.st.AccountBySqid(fx.sqid) if err != nil || acct == nil { t.Fatalf("ops account: %v", err) } for i := 0; ; i++ { n, err := fx.srv.runningThings(context.Background(), acct) if err != nil { t.Fatal(err) } if n > capN { capN = n } if n == capN { break } capsApp(t, fx, acct, fmt.Sprintf("filler%02d", i)) } if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &plans.Limits{RunningThings: capN}, "test", "ops@example.com", planNow); err != nil { t.Fatal(err) } return acct } func raiseRunningCap(t *testing.T, fx *consoleFixture, acct *Account, by int) { t.Helper() n, err := fx.srv.runningThings(context.Background(), acct) if err != nil { t.Fatal(err) } if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &plans.Limits{RunningThings: n + by}, "test", "ops@example.com", planNow); err != nil { t.Fatal(err) } } func TestCreateAppRefusedAtTheRunningThingsCap(t *testing.T) { fx := newConsoleFixture(t) acct := atRunningCap(t, fx, 3) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-create") status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "onemore"}, token) if status != http.StatusConflict || out["code"] != "cap" || out["error"] == nil { t.Fatalf("CLI create at the cap = %d %v, want 409 cap", status, out) } c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-cap-create-web") if st, _, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"webmore"}}); st != http.StatusSeeOther { t.Fatalf("browser create at the cap = %d, want the flash redirect", st) } for _, app := range []string{"onemore", "webmore"} { if has, _ := fx.st.HasAppIn(acct.ID, app); has { t.Fatalf("%s was created past the cap", app) } } } func TestCreateAppBelowTheCapStillWorks(t *testing.T) { fx := newConsoleFixture(t) acct := atRunningCap(t, fx, 3) raiseRunningCap(t, fx, acct, 1) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-below") if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "fits"}, token); status != http.StatusOK { t.Fatalf("create below the cap = %d %v", status, out) } } func TestTemporaryAppCountsTowardRunningThings(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.TemporaryApps = true atRunningCap(t, fx, 3) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-temp") status, out := createTemporary(t, fx, token, "tmp-capcheck", 3600) if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("temporary create at the running cap = %d %v, want 409 cap", status, out) } } func TestRestoreRefusedAtTheRunningThingsCap(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-restore") if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/kass/delete", map[string]string{}, token); status != http.StatusOK { t.Fatalf("delete kass = %d %v", status, out) } atRunningCap(t, fx, 3) status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/kass/restore", map[string]string{}, token) if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("CLI restore at the cap = %d %v, want 409 cap", status, out) } c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-cap-restore-web") if st, _, _ := postForm(t, c, fx.u("/trash/restore"), url.Values{"csrf": {csrf}, "app": {"kass"}}); st != http.StatusSeeOther { t.Fatalf("browser restore at the cap = %d, want the flash redirect", st) } if markerOf(t, fx, "kass") == nil { t.Fatal("a refused restore removed the trash marker") } } func TestRunningThingsCapIsExactUnderConcurrentCreates(t *testing.T) { fx := newConsoleFixture(t) acct := atRunningCap(t, fx, 3) raiseRunningCap(t, fx, acct, 1) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-race") var mu sync.Mutex created := 0 var wg sync.WaitGroup for i := range 8 { wg.Add(1) go func() { defer wg.Done() status, _ := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": fmt.Sprintf("race%d", i)}, token) if status == http.StatusOK { mu.Lock() created++ mu.Unlock() } }() } wg.Wait() if created != 1 { t.Fatalf("%d creates succeeded for one free slot, want exactly 1", created) } } func TestCreateAppWithoutACatalogUnchanged(t *testing.T) { fx := newConsoleFixture(t) acct, _ := fx.st.AccountBySqid(fx.sqid) for i := range 5 { capsApp(t, fx, acct, fmt.Sprintf("many%d", i)) } token := cliToken(t, fx, "paul@keymail.dev", "code-cap-nocat") if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "stillfine"}, token); status != http.StatusOK { t.Fatalf("create with no catalog = %d %v", status, out) } } // sponsorCustomDomains keeps the cap as atRunningCap left it and adds custom // domains, so a first-route attach meets the running-things cap rather than // the custom-domain gate. func sponsorCustomDomains(t *testing.T, fx *consoleFixture, acct *Account) { t.Helper() row, err := fx.st.AccountPlan(acct.ID) if err != nil || row == nil || row.Sponsored == nil { t.Fatalf("plan row: %+v, %v", row, err) } sp := *row.Sponsored sp.CustomDomains = true if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &sp, "test", "ops@example.com", planNow); err != nil { t.Fatal(err) } } func TestInstanceCreateRefusedAtTheRunningThingsCap(t *testing.T) { fx, token := instanceFixture(t, "code-cap-instance") enableInstances(t, fx, "titogo", titoManifest()) atRunningCap(t, fx, 3) status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, `{"host":"acme.go.tito.io","region":"ie"}`) if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) { t.Fatalf("instance create at the cap = %d %s, want 409 cap", status, out) } if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io"); err == nil { t.Fatal("an instance record was written past the cap") } } func TestFirstRouteAttachRefusedAtTheRunningThingsCap(t *testing.T) { fx, token := instanceFixture(t, "code-cap-firstroute") shipVersion(t, fx, "titogo", "v1") seedChannel(t, fx, "titogo", "canary/live", "v1") acct := atRunningCap(t, fx, 3) sponsorCustomDomains(t, fx, acct) status, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), token, `{"host":"shop.example.com","kind":"static"}`) if status != http.StatusConflict { t.Fatalf("first-route attach at the cap = %d %s, want 409", status, out) } if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "shop.example.com"); err == nil { t.Fatal("a first-route record was written past the cap") } } func TestSidecarCreateRefusedAtTheSidecarCap(t *testing.T) { fx, token := instanceFixture(t, "code-cap-sidecar") enableSidecars(t, fx, "titogo", titoSidecarManifest()) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct, _ := fx.st.AccountBySqid("bab") capsSidecar(t, fx, acct, "titogo", "first") status, _, out := rawReq(t, http.MethodPost, sidecarsURL(fx, "bab", "titogo"), token, `{"name":"bot","region":"ie"}`) if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) { t.Fatalf("second sidecar on free = %d %s, want 409 cap", status, out) } if _, _, err := fx.rel.Get(context.Background(), provision.SidecarRecordKey("bab", "titogo", "bot")); err == nil { t.Fatal("a sidecar record was written past the cap") } } func TestInstanceAndSidecarCreateWithoutACatalogUnchanged(t *testing.T) { fx, token := instanceFixture(t, "code-cap-inst-nocat") enableInstances(t, fx, "titogo", titoManifest()) if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, `{"host":"acme.go.tito.io","region":"ie"}`); status != http.StatusCreated { t.Fatalf("instance create with no catalog = %d %s", status, out) } enableSidecars(t, fx, "titogo", titoSidecarManifest()) acct, _ := fx.st.AccountBySqid("bab") capsSidecar(t, fx, acct, "titogo", "first") if status, _, out := rawReq(t, http.MethodPost, sidecarsURL(fx, "bab", "titogo"), token, `{"name":"bot","region":"ie"}`); status != http.StatusCreated { t.Fatalf("sidecar create with no catalog = %d %s", status, out) } } func accountCount(t *testing.T, fx *consoleFixture) int { t.Helper() all, err := fx.st.Accounts() if err != nil { t.Fatal(err) } return len(all) } func TestNewAccountRefusedForASecondFreeAccount(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) before := accountCount(t, fx) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct") status, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "second"}, token) if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("CLI second free account = %d %v, want 409 cap", status, out) } c := signInPaul(t, fx) st, loc, _ := postForm(t, c, fx.ts.URL+"/accounts/new", url.Values{"csrf": {sessionToken(t, fx, c)}, "name": {"thirdtry"}}) if st != http.StatusSeeOther || loc != "/accounts/new" { t.Fatalf("browser second free account = %d -> %q, want the flash redirect back to /accounts/new", st, loc) } if after := accountCount(t, fx); after != before { t.Fatalf("accounts %d -> %d: one was minted past the cap", before, after) } } func TestFirstAccountIsNeverRefused(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) signInAs(t, fx, "newcomer@keymail.dev", "code-cap-first") accts, err := fx.st.AccountsFor("newcomer@keymail.dev") if err != nil || len(accts) != 1 { t.Fatalf("first sign-in with plans on: %d accounts, %v; want the auto-minted one", len(accts), err) } } func TestNewAccountAllowedWhenTheOtherIsPaid(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) ops, _ := fx.st.AccountBySqid(fx.sqid) if _, err := fx.st.SetAccountPlan(ops.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct-paid") if status, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "second"}, token); status != http.StatusOK { t.Fatalf("a first free account beside a paid one = %d %v", status, out) } } func TestNewAccountWithoutACatalogUnchanged(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct-nocat") for _, name := range []string{"one", "two"} { if status, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": name}, token); status != http.StatusOK { t.Fatalf("account %s with no catalog = %d %v", name, status, out) } } } func TestFleetCreateRefusedOnAPlanWithoutFleets(t *testing.T) { fx, token := fleetFixture(t, "code-cap-fleet") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) status, out := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": "acme", "bucket": "bkt-1"}, token) if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("fleet create on free = %d %v, want 409 cap", status, out) } } func TestCustomDomainRefusedOnAFreePlan(t *testing.T) { fx, member := instanceFixture(t, "code-cap-domain") shipVersion(t, fx, "titogo", "v1") seedChannel(t, fx, "titogo", "canary/live", "v1") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) for name, body := range map[string]string{ "alias": `{"host":"tickets.example.com","route":"go.tito.io"}`, "first route": `{"host":"fresh.example.com","kind":"static"}`, } { code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, body) if code != http.StatusConflict { t.Errorf("%s on free = %d %s, want 409", name, code, out) } } claimAbsent(t, fx, "tickets.example.com") claimAbsent(t, fx, "fresh.example.com") } func TestFleetsAndDomainsWithoutACatalogUnchanged(t *testing.T) { fx, member := instanceFixture(t, "code-cap-domain-nocat") if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, `{"host":"tickets.example.com","route":"go.tito.io"}`); code != http.StatusOK { t.Fatalf("alias attach with no catalog = %d %s", code, out) } fx2, token := fleetFixture(t, "code-cap-fleet-nocat") if status, out := postJSON(t, fleetsURL(fx2, fx2.sqid)+"/new", map[string]string{"name": "acme", "bucket": "bkt-1"}, token); status != http.StatusOK { t.Fatalf("fleet create with no catalog = %d %v", status, out) } } // Review Focus 5: an address under the deployment's apps domain is the // account's own platform address, not a custom domain, so free plans keep it. func TestPlatformHostnamesStayFreeOnFreePlans(t *testing.T) { fx, member := instanceFixture(t, "code-cap-platform") fx.srv.cfg.AppsDomain = "apps.example.com" fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct, _ := fx.st.AccountBySqid("bab") if fx.srv.customDomain("shop.bab.apps.example.com", acct.Sqid) { t.Fatal("an address under the apps domain was treated as a custom domain") } if !fx.srv.customDomain("shop.example.com", acct.Sqid) { t.Fatal("a customer domain was not treated as a custom domain") } if ref := fx.srv.customDomainRefusal(acct, "shop.bab.apps.example.com"); ref != nil { t.Fatalf("free plan refused its own platform address: %+v", ref) } _ = member } // Astra plans-3a finding 2: concurrent creates by one person who owns only // paid accounts must not mint more free accounts than the allowance. func TestFreeAccountCapIsExactUnderConcurrentCreates(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) ops, _ := fx.st.AccountBySqid(fx.sqid) if _, err := fx.st.SetAccountPlan(ops.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct-race") var mu sync.Mutex created := 0 var wg sync.WaitGroup for i := range 6 { wg.Add(1) go func() { defer wg.Done() status, _ := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": fmt.Sprintf("race%d", i)}, token) if status == http.StatusOK { mu.Lock() created++ mu.Unlock() } }() } wg.Wait() if created != 1 { t.Fatalf("%d free accounts minted concurrently, want exactly 1", created) } } // ---- Plans 3b: things that never sleep ---- const daemonBlock = `{"v":1,"command":"relay","daemon":true}` func shipDaemon(t *testing.T, fx *consoleFixture, token, version string) (int, map[string]any) { t.Helper() return postShip(t, fx.ts.URL+"/api/cli/apps/bab/relayapp/releases", token, []shipPart{metaPartCaps(t, version, "binary", daemonBlock), {name: "linux-arm64", body: []byte("daemon-binary-bytes")}}) } func daemonFixture(t *testing.T, code string) (*consoleFixture, string) { t.Helper() fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "relayapp", "paul@keymail.dev", planNow); err != nil { t.Fatal(err) } return fx, cliToken(t, fx, "paul@keymail.dev", code) } func TestShipRefusesDaemonUnderAPlan(t *testing.T) { fx, token := daemonFixture(t, "code-cap-daemon") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) status, out := shipDaemon(t, fx, token, "dmn00001") if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("daemon ship under a plan = %d %v, want 409 cap", status, out) } if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "relayapp", "dmn00001"); err == nil { t.Fatal("a daemon release was written under a plan") } } func TestShipRefusesDaemonEvenOnReship(t *testing.T) { fx, token := daemonFixture(t, "code-cap-daemon-reship") if status, out := shipDaemon(t, fx, token, "dmn00002"); status != http.StatusCreated { t.Fatalf("setup ship with no catalog = %d %v", status, out) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) if status, out := shipDaemon(t, fx, token, "dmn00002"); status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("identical daemon re-ship under a plan = %d %v, want 409 cap", status, out) } } func TestShipDaemonWithoutACatalogUnchanged(t *testing.T) { fx, token := daemonFixture(t, "code-cap-daemon-nocat") if status, out := shipDaemon(t, fx, token, "dmn00003"); status != http.StatusCreated { t.Fatalf("daemon ship with no catalog = %d %v", status, out) } } func TestShipWithoutDaemonUnderAPlanUnchanged(t *testing.T) { fx, token := daemonFixture(t, "code-cap-nodaemon") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/relayapp/releases", token, []shipPart{metaPartCaps(t, "plain001", "binary", `{"v":1,"command":"relay"}`), {name: "linux-arm64", body: []byte("plain-binary-bytes")}}) if status != http.StatusCreated { t.Fatalf("ship without daemon under a plan = %d %v", status, out) } } func sidecarHibernate(t *testing.T, fx *consoleFixture) *bool { t.Helper() m, err := provision.GetSidecarManifest(context.Background(), fx.rel, "bab", "titogo") if err != nil { t.Fatal(err) } return m.Hibernate } func TestSidecarEnableRefusesHibernateFalseUnderAPlan(t *testing.T) { fx, token := instanceFixture(t, "code-cap-alwayson") enableSidecars(t, fx, "titogo", titoSidecarManifest()) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), token, `{"hibernate":false}`) if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) { t.Fatalf("hibernate:false under a plan = %d %s, want 409 cap", status, out) } if h := sidecarHibernate(t, fx); h != nil && !*h { t.Fatal("the manifest was set to stay awake under a plan") } } func TestSidecarEnableKeepsAnExistingSettingWhenOmitted(t *testing.T) { fx, token := instanceFixture(t, "code-cap-alwayson-keep") m := titoSidecarManifest() off := false m.Hibernate = &off enableSidecars(t, fx, "titogo", m) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) if status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), token, `{"idle_seconds":120}`); status != http.StatusOK { t.Fatalf("enable without a hibernate field = %d %s, want 200", status, out) } if h := sidecarHibernate(t, fx); h == nil || *h { t.Fatal("an existing setting was changed by an enable that did not name it") } } func TestSidecarEnableHibernateFalseWithoutACatalogUnchanged(t *testing.T) { fx, token := instanceFixture(t, "code-cap-alwayson-nocat") enableSidecars(t, fx, "titogo", titoSidecarManifest()) if status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), token, `{"hibernate":false}`); status != http.StatusOK { t.Fatalf("hibernate:false with no catalog = %d %s, want 200", status, out) } } func setSchedule(t *testing.T, fx *consoleFixture, token, body string) int { t.Helper() status, _ := postBody(t, fx.ts.URL+"/api/cli/apps/bab/titogo/schedules", token, body) return status } func TestScheduleRefusedBelowThePlanMinimum(t *testing.T) { fx, token := scheduleFixture(t, "code-cap-schedule") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) for _, body := range []string{ `{"name":"often","every":"5m","path":"/jobs/x"}`, `{"name":"often","cron":"*/1 * * * *","path":"/jobs/x"}`, } { status, out := postBody(t, fx.ts.URL+"/api/cli/apps/bab/titogo/schedules", token, body) if status != http.StatusConflict || !strings.Contains(string(out), `"code":"cap"`) { t.Errorf("%s on free = %d %s, want 409 cap", body, status, out) } } if _, etag := declaredSchedules(t, fx, "titogo"); etag != "" { t.Fatal("a refused schedule was written") } for _, body := range []string{ `{"name":"quarter","every":"15m","path":"/jobs/x"}`, `{"name":"quartercron","cron":"*/15 * * * *","path":"/jobs/y"}`, } { if status := setSchedule(t, fx, token, body); status != http.StatusOK { t.Errorf("%s on free = %d, want 200", body, status) } } } func TestScheduleMinimumOnAPaidPlan(t *testing.T) { fx, token := scheduleFixture(t, "code-cap-schedule-paid") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) ops, _ := fx.st.AccountBySqid(fx.sqid) if _, err := fx.st.SetAccountPlan(ops.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil { t.Fatal(err) } if status := setSchedule(t, fx, token, `{"name":"minutely","every":"1m","path":"/jobs/x"}`); status != http.StatusOK { t.Fatalf("every 1m on personal = %d, want 200", status) } } func TestScheduleWithoutACatalogUnchanged(t *testing.T) { fx, token := scheduleFixture(t, "code-cap-schedule-nocat") if status := setSchedule(t, fx, token, `{"name":"minutely","every":"1m","path":"/jobs/x"}`); status != http.StatusOK { t.Fatalf("every 1m with no catalog = %d, want 200", status) } } // ---- Plans 3b: own boxes (Paul, 2026-10-24) ---- func placeOnFleet(t *testing.T, fx *consoleFixture, app string) { t.Helper() addFleet(t, fx, "ownbox", fx.sqid) placeOnNamedFleet(t, fx, app, "ownbox") } // addFleet registers a fleet owned by account in the fixture's registry. func addFleet(t *testing.T, fx *consoleFixture, name, account string) { t.Helper() reg, err := registry.Open(fx.regPath) if err != nil { t.Fatal(err) } defer reg.Close() if err := reg.UpsertFleet(registry.Fleet{Name: name, Account: account, Bucket: name + "-bucket"}); err != nil { t.Fatal(err) } } // placeOnNamedFleet stores app's placement as fleet/, whether or not // that fleet exists or is the account's own. func placeOnNamedFleet(t *testing.T, fx *consoleFixture, app, name string) { t.Helper() ops, _ := fx.st.AccountBySqid(fx.sqid) if err := fx.st.SetAppPlacement(ops.ID, app, registry.PlacementTarget(name)); err != nil { t.Fatal(err) } } // Astra plans-3b finding 5: the fleet/ shape is not ownership. Only a // fleet the account owns exempts an app; a missing or foreign one does not, // and a registry that cannot be read is an error, not a pass. func TestOwnBoxesNeedsAFleetTheAccountOwns(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct, _ := fx.st.AccountBySqid(fx.sqid) addFleet(t, fx, "mine", fx.sqid) addFleet(t, fx, "theirs", "zzz") for _, tc := range []struct { fleet string own bool }{{"mine", true}, {"theirs", false}, {"nowhere", false}} { placeOnNamedFleet(t, fx, "kass", tc.fleet) got, err := fx.srv.onOwnBoxes(acct, "kass") if err != nil || got != tc.own { t.Errorf("onOwnBoxes on fleet/%s = %v, %v, want %v", tc.fleet, got, err, tc.own) } if err := fx.srv.checkDaemon(acct, "kass"); (err == nil) != tc.own { t.Errorf("checkDaemon on fleet/%s = %v, want refused=%v", tc.fleet, err, !tc.own) } } placeOnNamedFleet(t, fx, "kass", "mine") fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "missing", "registry.db") if _, err := fx.srv.onOwnBoxes(acct, "kass"); err == nil { t.Error("onOwnBoxes with an unreadable registry = nil error, want the failure") } if err := fx.srv.checkDaemon(acct, "kass"); err == nil || isPlanCap(err) { t.Errorf("checkDaemon with an unreadable registry = %v, want a non-cap error", err) } } func TestDaemonAllowedOnOwnBoxes(t *testing.T) { fx, token := daemonFixture(t, "code-cap-daemon-fleet") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) placeOnFleet(t, fx, "relayapp") if status, out := shipDaemon(t, fx, token, "dmn00010"); status != http.StatusCreated { t.Fatalf("daemon ship for an app on its own fleet = %d %v, want 201", status, out) } } func TestAlwaysOnSidecarsAllowedOnOwnBoxes(t *testing.T) { fx, token := instanceFixture(t, "code-cap-alwayson-fleet") enableSidecars(t, fx, "titogo", titoSidecarManifest()) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) placeOnFleet(t, fx, "titogo") if status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), token, `{"hibernate":false}`); status != http.StatusOK { t.Fatalf("hibernate:false for an app on its own fleet = %d %s, want 200", status, out) } } func TestFrequentSchedulesAllowedOnOwnBoxes(t *testing.T) { fx, token := scheduleFixture(t, "code-cap-schedule-fleet") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) placeOnFleet(t, fx, "titogo") if status := setSchedule(t, fx, token, `{"name":"minutely","every":"1m","path":"/jobs/x"}`); status != http.StatusOK { t.Fatalf("every 1m for an app on its own fleet = %d, want 200", status) } } // Astra plans-3b finding 3: a manifest set to stay awake before plans were // on must not let a new always-on worker be created on shared boxes. func TestSidecarCreateRefusedUnderAnAlwaysOnManifest(t *testing.T) { fx, token := instanceFixture(t, "code-cap-create-alwayson") m := titoSidecarManifest() off := false m.Hibernate = &off enableSidecars(t, fx, "titogo", m) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) status, _, out := rawReq(t, http.MethodPost, sidecarsURL(fx, "bab", "titogo"), token, `{"name":"bot","region":"ie"}`) if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) { t.Fatalf("new worker under an always-on manifest on shared boxes = %d %s, want 409 cap", status, out) } } // Astra plans-3b finding 4: an app placed on its fleet whose existing // sidecars still run on shared boxes may not be switched to stay awake. func TestAlwaysOnRefusedWhileSidecarsRemainOnSharedBoxes(t *testing.T) { fx, token := instanceFixture(t, "code-cap-alwayson-moved") enableSidecars(t, fx, "titogo", titoSidecarManifest()) if status, _, out := rawReq(t, http.MethodPost, sidecarsURL(fx, "bab", "titogo"), token, `{"name":"bot","region":"ie"}`); status != http.StatusCreated { t.Fatalf("setup sidecar on shared boxes = %d %s", status, out) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) placeOnFleet(t, fx, "titogo") status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), token, `{"hibernate":false}`) if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) { t.Fatalf("hibernate:false while a sidecar runs on shared boxes = %d %s, want 409 cap", status, out) } } // Astra plans-3b finding 2: a daemon release shipped before plans were on // must not be promoted on shared boxes under a plan. func TestPromoteRefusesADaemonReleaseUnderAPlan(t *testing.T) { fx, token := daemonFixture(t, "code-cap-daemon-promote") if status, out := shipDaemon(t, fx, token, "dmn00020"); status != http.StatusCreated { t.Fatalf("setup daemon ship with no catalog = %d %v", status, out) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/relayapp/promote", map[string]string{"version": "dmn00020", "channel": "canary/x"}, token) if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("promote a daemon release under a plan = %d %v, want 409 cap", status, out) } placeOnFleet(t, fx, "relayapp") if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/relayapp/promote", map[string]string{"version": "dmn00020", "channel": "canary/x"}, token); status != http.StatusOK { t.Fatalf("promote a daemon release on its own fleet = %d %v, want 200", status, out) } } // shipDaemonVersion writes a release declaring the daemon capability // straight to the bucket, the state a ship from before plans were on leaves. func shipDaemonVersion(t *testing.T, fx *consoleFixture, app, version string) { t.Helper() m := release.Manifest{ Schema: 1, App: app, Version: version, Kind: "binary", BuiltAt: "2026-07-03T10:00:00Z", Artifacts: map[string]release.Artifact{"linux-amd64": {File: app}}, Capabilities: &caps.Set{V: caps.Version, Daemon: true}, } if err := release.Ship(context.Background(), fx.rel, release.OpsAccount, m, map[string][]byte{"linux-amd64": []byte("binary " + version)}); err != nil { t.Fatal(err) } } func TestBrowserPromoteAndCanaryRefuseADaemonReleaseUnderAPlan(t *testing.T) { fx := newConsoleFixture(t) shipDaemonVersion(t, fx, "kass", "dmn1") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) postForm(t, c, fx.u("/apps/kass/promote"), url.Values{"csrf": {csrf}, "channel": {"canary/x"}, "version": {"dmn1"}, "confirmed": {"1"}}) if p := fx.channelVersion(t, "kass", "canary/x"); p.Version == "dmn1" { t.Fatal("the browser promoted a daemon release onto shared boxes under a plan") } postForm(t, c, fx.u("/apps/kass/canary"), url.Values{"csrf": {csrf}, "name": {"rc"}, "version": {"dmn1"}}) if _, _, err := fx.rel.Get(context.Background(), release.ChannelKey(release.OpsAccount, "kass", "canary/rc")); err == nil { t.Fatal("a canary of a daemon release was created on shared boxes under a plan") } placeOnFleet(t, fx, "kass") postForm(t, c, fx.u("/apps/kass/canary"), url.Values{"csrf": {csrf}, "name": {"rc"}, "version": {"dmn1"}}) if p := fx.channelVersion(t, "kass", "canary/rc"); p.Version != "dmn1" { t.Fatalf("canary/rc = %+v on its own fleet, want dmn1", p) } } // A daemon proposal made before plans were on meets the plan at execution. func TestPendingPromoteOfADaemonReleaseRefusedUnderAPlan(t *testing.T) { fx, c, csrf := memberFixture(t) pipelinedApp(t, fx, "widget", "v2", release.PipelineChannel{Name: "edge"}, release.PipelineChannel{Name: "production", Rules: release.ChannelRules{PromoteApprovals: 2}}) shipDaemonVersion(t, fx, "widget", "dmn2") seedChannel(t, fx, "widget", "edge", "dmn2") seedChannel(t, fx, "widget", "production", "v2") if status, loc, body := postGated(t, fx, c, fx.u("/apps/widget/promote"), url.Values{"csrf": {csrf}, "channel": {"production"}, "version": {"dmn2"}, "confirmed": {"1"}}); status != http.StatusSeeOther { t.Fatalf("proposal = %d -> %q:\n%s", status, loc, body) } pending, err := fx.st.PendingChangesFor(OpsAccountID, "widget", time.Now()) if err != nil || len(pending) != 1 { t.Fatalf("pending = %v, %v, want exactly one", pending, err) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) kim := newBrowser(t) fx.setUser("kim@keymail.dev") signUp(t, fx, kim, "code-kim-daemon-pending") postGated(t, fx, kim, fx.u("/apps/widget/pending/"+pending[0].ID+"/confirm"), url.Values{"csrf": {sessionToken(t, fx, kim)}}) if got := fx.channelVersion(t, "widget", "production"); got.Version != "v2" { t.Fatalf("production serves %q after confirming a daemon proposal under a plan, want v2", got.Version) } } // Astra plans-3b round 2 finding 1: a sidecar record that cannot be read is // a worker nobody checked, so always-on is not granted past it. func TestAlwaysOnRefusedPastAnUnreadableSidecarRecord(t *testing.T) { fx, token := instanceFixture(t, "code-cap-alwayson-unreadable") enableSidecars(t, fx, "titogo", titoSidecarManifest()) if _, err := fx.rel.Put(context.Background(), provision.SidecarRecordKey(release.OpsAccount, "titogo", "bot"), []byte("{not json"), ""); err != nil { t.Fatal(err) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) placeOnFleet(t, fx, "titogo") status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), token, `{"hibernate":false}`) if status == http.StatusOK || status == http.StatusNoContent { t.Fatalf("hibernate:false past an unreadable sidecar record = %d %s, want a refusal", status, out) } m, err := provision.GetSidecarManifest(context.Background(), fx.rel, release.OpsAccount, "titogo") if err != nil || m == nil || !m.HibernateOrDefault() { t.Fatalf("manifest after the refusal = %+v, %v, want hibernation still on", m, err) } } // daemonInHistory leaves canary/x serving an ordinary release with a daemon // release one rollback back, the state promotions made before plans were on // leave. func daemonInHistory(t *testing.T, fx *consoleFixture, app string) { t.Helper() shipDaemonVersion(t, fx, app, "dmn3") shipVersion(t, fx, app, "plain4") seedChannel(t, fx, app, "canary/x", "dmn3") seedChannel(t, fx, app, "canary/x", "plain4") } // Astra plans-3b round 2 finding 2: a rollback can put back a daemon // release that no channel serves any more, so it meets the rule too. func TestRollbackToADaemonReleaseRefusedUnderAPlan(t *testing.T) { fx := newConsoleFixture(t) daemonInHistory(t, fx, "kass") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) token := cliToken(t, fx, "paul@keymail.dev", "code-cap-daemon-rollback") status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/kass/rollback", map[string]string{"channel": "canary/x"}, token) if status != http.StatusConflict || out["code"] != "cap" { t.Fatalf("CLI rollback onto a daemon release under a plan = %d %v, want 409 cap", status, out) } c := signInPaul(t, fx) postForm(t, c, fx.u("/apps/kass/rollback"), url.Values{"csrf": {sessionToken(t, fx, c)}, "channel": {"canary/x"}, "confirmed": {"1"}}) if p := fx.channelVersion(t, "kass", "canary/x"); p.Version != "plain4" { t.Fatalf("canary/x = %s after rollbacks refused under a plan, want plain4", p.Version) } placeOnFleet(t, fx, "kass") if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/kass/rollback", map[string]string{"channel": "canary/x"}, token); status != http.StatusOK { t.Fatalf("CLI rollback onto a daemon release on its own fleet = %d %v, want 200", status, out) } } // The guard re-reads the pointer at write time, so a target that changed // after the door's check is still held to the rule. func TestRollbackDaemonGuardReadsThePointerAtWriteTime(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "plain2") seedChannel(t, fx, "kass", "canary/x", "plain2") acct, _ := fx.st.AccountBySqid(fx.sqid) ctx := context.Background() if guard, err := fx.srv.rollbackDaemonGuard(ctx, acct, "kass", "canary/x"); err != nil || guard != nil { t.Fatalf("guard with plans off = %v, %v, want none", guard != nil, err) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) guard, err := fx.srv.rollbackDaemonGuard(ctx, acct, "kass", "canary/x") if err != nil || guard == nil { t.Fatalf("guard under a plan = %v, %v, want one", guard != nil, err) } daemonInHistory(t, fx, "kass") // the pointer moves after the door's check if err := guard(nil); !isPlanCap(err) { t.Fatalf("guard after the pointer moved onto a daemon target = %v, want a cap", err) } placeOnFleet(t, fx, "kass") if guard, err := fx.srv.rollbackDaemonGuard(ctx, acct, "kass", "canary/x"); err != nil || guard != nil { t.Fatalf("guard on its own fleet = %v, %v, want none", guard != nil, err) } } // A pending rollback onto a daemon release is checked at execution too. func TestPendingRollbackToADaemonReleaseRefusedUnderAPlan(t *testing.T) { fx, c, csrf := memberFixture(t) pipelinedApp(t, fx, "widget", "v2", release.PipelineChannel{Name: "edge"}, release.PipelineChannel{Name: "production", Rules: release.ChannelRules{PromoteApprovals: 2}}) shipDaemonVersion(t, fx, "widget", "dmn5") seedChannel(t, fx, "widget", "production", "dmn5") seedChannel(t, fx, "widget", "production", "v2") if status, loc, body := postGated(t, fx, c, fx.u("/apps/widget/rollback"), url.Values{"csrf": {csrf}, "channel": {"production"}, "confirmed": {"1"}}); status != http.StatusSeeOther { t.Fatalf("rollback proposal = %d -> %q:\n%s", status, loc, body) } pending, err := fx.st.PendingChangesFor(OpsAccountID, "widget", time.Now()) if err != nil || len(pending) != 1 { t.Fatalf("pending = %v, %v, want exactly one", pending, err) } fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) kim := newBrowser(t) fx.setUser("kim@keymail.dev") signUp(t, fx, kim, "code-kim-daemon-rollback") postGated(t, fx, kim, fx.u("/apps/widget/pending/"+pending[0].ID+"/confirm"), url.Values{"csrf": {sessionToken(t, fx, kim)}}) if got := fx.channelVersion(t, "widget", "production"); got.Version != "v2" { t.Fatalf("production serves %q after confirming a daemon rollback under a plan, want v2", got.Version) } } // Astra plans-3b round 3 finding 1: with plans off a rollback reads no // manifest, so one that cannot be read does not stop it. func TestRollbackWithoutACatalogNeedsNoManifest(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "old1") shipVersion(t, fx, "kass", "new2") seedChannel(t, fx, "kass", "canary/x", "old1") seedChannel(t, fx, "kass", "canary/x", "new2") if err := fx.rel.Delete(context.Background(), release.ManifestKey(release.OpsAccount, "kass", "old1")); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-rollback-nomanifest") if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/kass/rollback", map[string]string{"channel": "canary/x"}, token); status != http.StatusOK { t.Fatalf("CLI rollback with plans off and the target's manifest unreadable = %d %v, want 200", status, out) } c := signInPaul(t, fx) postForm(t, c, fx.u("/apps/kass/rollback"), url.Values{"csrf": {sessionToken(t, fx, c)}, "channel": {"canary/x"}, "confirmed": {"1"}}) if p := fx.channelVersion(t, "kass", "canary/x"); p.Version != "new2" { t.Fatalf("canary/x = %s after a second rollback with plans off, want new2", p.Version) } } // Round 3 finding 2: a guard that cannot read is a failed plan check (502), // not a deploy refusal. func TestRollbackGuardReadFailureIsAPlanCheckFailure(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "old1") shipVersion(t, fx, "kass", "new2") seedChannel(t, fx, "kass", "canary/x", "old1") seedChannel(t, fx, "kass", "canary/x", "new2") fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) if err := fx.rel.Delete(context.Background(), release.ManifestKey(release.OpsAccount, "kass", "old1")); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-rollback-guardfail") status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/kass/rollback", map[string]string{"channel": "canary/x"}, token) if status != http.StatusBadGateway || out["error"] != planCheckFailedCopy { t.Fatalf("CLI rollback with an unreadable target manifest under a plan = %d %v, want 502 %q", status, out, planCheckFailedCopy) } acct, _ := fx.st.AccountBySqid(fx.sqid) guard, err := fx.srv.rollbackDaemonGuard(context.Background(), acct, "kass", "canary/x") if err != nil || guard == nil { t.Fatalf("guard = %v, %v", guard != nil, err) } var ge *planGuardError if gerr := guard(nil); !errors.As(gerr, &ge) { t.Fatalf("guard read failure = %v, want a *planGuardError", gerr) } }