package console import ( "context" "fmt" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // layoutDowngradeBlocked judges observed state: a target at or above the // floor is always allowed; below it, every box must be fresh, carry a // layout report, and report no nested, fenced or conflict row. func TestLayoutDowngradeBlockedJudgesEveryBoxsReport(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Now() flat := &status.LayoutReport{Mode: "flat", Rows: map[string]status.RowLayout{"a.example.com": {Layout: "flat"}, "x.example.com": {Layout: "external"}}} seedLayoutBox(t, fx.rel, "ie", now, flat) if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); got != "" { t.Fatalf("all flat: %q, want allowed", got) } seedLayoutBox(t, fx.rel, "uk", now, &status.LayoutReport{Mode: "nested", Rows: map[string]status.RowLayout{ "b.example.com": {Layout: "nested"}, "c.example.com": {Layout: "flat"}, "d.example.com": {Layout: "fenced", Reason: "mid-move"}, }}) got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0") for _, want := range []string{"v0.19.0 predates the instance layout", "uk: b.example.com is nested", "uk: d.example.com is fenced", "carlos system layout revert"} { if !strings.Contains(got, want) { t.Errorf("reason %q lacks %q", got, want) } } if strings.Contains(got, "c.example.com") || strings.Contains(got, "ie") { t.Errorf("reason %q names a flat row or a flat box", got) } // At or above the floor nothing is judged. for _, v := range []string{"v0.25.0", "v0.26.1"} { if got := fx.srv.layoutDowngradeBlocked(ctx, v); got != "" { t.Errorf("%s: %q, want allowed", v, got) } } // A box that reports no layout at all is unknown and blocks, unless // the release it reports running parses below the floor: that build // has no layout pass, so its silence is flat. Today's fleet, on // v0.21.0 to v0.24.2, keeps its rollback (and its named update). for _, v := range []string{"v0.18.0", "v0.20.0", "v0.21.0", "v0.24.2"} { seedBox(t, fx, "au", "linux-arm64", "stable", v) if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); strings.Contains(got, "au ") { t.Errorf("a %s box with no layout field blocked: %q", v, got) } } // Silence at or above the floor is unknown: that build reports a // layout when it has an activator, and says "unmanaged" when it has // none, so no report at all means a box the console cannot vouch for. seedBox(t, fx, "au", "linux-arm64", "stable", "v0.25.0") if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); !strings.Contains(got, "au reports no layout field") { t.Errorf("no layout field on the floor release: %q", got) } seedBox(t, fx, "au", "linux-arm64", "stable", "dev") if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); !strings.Contains(got, "au reports no layout field") { t.Errorf("no layout field on an unplaceable release: %q", got) } // So does a stale one, whatever its last report said. seedLayoutBox(t, fx.rel, "au", now.Add(-20*time.Minute), flat) if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); !strings.Contains(got, "au is stale") { t.Errorf("stale: %q", got) } } // Both system doors call the guard before a pointer moves: the rollback // door against the release the pointer would go back to, the update door // against a named version. A non-nested fleet rolls back as before. func TestSystemDoorsRefuseAPreLayoutMoveWhileABoxIsNested(t *testing.T) { fx := newConsoleFixture(t) owner := cliToken(t, fx, "paul@keymail.dev", "code-layout-guard") ctx := context.Background() sg := release.RootSigner(fx.priv) rel := func(v string) release.SystemRelease { return release.SystemRelease{Version: v, Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey(v, "linux-arm64"), SHA256: strings.Repeat("a", 64)}}} } if _, err := release.PromoteSystem(ctx, fx.rel, sg, "router", "canary", rel("v0.19.0"), release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } time.Sleep(1100 * time.Millisecond) if _, err := release.PromoteSystem(ctx, fx.rel, sg, "router", "canary", rel("v0.25.0"), release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } seedLayoutBox(t, fx.rel, "ie", time.Now(), &status.LayoutReport{Mode: "nested", Rows: map[string]status.RowLayout{"a.example.com": {Layout: "nested"}}}) st, out := postJSON(t, fx.ts.URL+"/api/cli/system/rollback", map[string]string{"channel": "canary"}, owner) if st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "ie: a.example.com is nested") { t.Fatalf("rollback to v0.19.0 with a nested row = %d %v, want 409 naming the row", st, out) } p, _, err := release.GetSystemPointer(ctx, fx.rel, fx.pub, "router", "canary") if err != nil || p.Current.Version != "v0.25.0" { t.Fatalf("pointer moved anyway: %+v %v", p, err) } st, out = postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary", "version": "v0.19.0"}, owner) if st != http.StatusConflict { t.Fatalf("update to v0.19.0 with a nested row = %d %v, want 409", st, out) } // A version at the floor or above is not the guard's business; the // request goes on to the job (and fails there, which is not what this // test is about). The fixture has no SystemFeed, so the job asks the // vendor feed: the version must be one that can never be published, or // the job succeeds once it is (v0.26.0 was, on 2026-10-13), moves the // pointer, and the rollback below lands on the wrong version. if st, _ := postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary", "version": "v99.0.0"}, owner); st == http.StatusConflict { t.Error("update to v99.0.0 was refused by the layout guard") } // Every row flat: the rollback goes through. seedLayoutBox(t, fx.rel, "ie", time.Now(), &status.LayoutReport{Mode: "flat", Rows: map[string]status.RowLayout{"a.example.com": {Layout: "flat"}}}) time.Sleep(1100 * time.Millisecond) if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/rollback", map[string]string{"channel": "canary"}, owner); st != http.StatusOK || str(t, out, "version") != "v0.19.0" { t.Fatalf("rollback with every row flat = %d %v", st, out) } } // `layout migrate` is refused while any box in scope runs below the floor, // is stale, reports no release, or (for a named box) has never reported; // it goes through once every box in scope runs the floor release. A // revert is never judged this way. func TestSystemLayoutMigrateRefusedBelowTheFloor(t *testing.T) { fx := newConsoleFixture(t) owner := cliToken(t, fx, "paul@keymail.dev", "code-layout-floor") seedBox(t, fx, "ie", "linux-arm64", "stable", "v0.24.2") seedBox(t, fx, "uk", "linux-arm64", "stable", "v0.25.0") st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, owner) if st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "ie runs v0.24.2") || strings.Contains(str(t, out, "error"), "uk") { t.Fatalf("fleet migrate with a box below the floor = %d %v, want 409 naming ie only", st, out) } if _, found, _ := release.LoadLayoutIntents(context.Background(), fx.rel); found { t.Fatal("a refused migrate recorded an intent") } if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested", "box": "ie"}, owner); st != http.StatusConflict { t.Errorf("box migrate on the low box = %d %v, want 409", st, out) } if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested", "box": "au"}, owner); st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "box au has not reported") { t.Errorf("box migrate on an unknown box = %d %v, want 409 saying it has not reported", st, out) } if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested", "box": "uk"}, owner); st != http.StatusOK { t.Errorf("box migrate on the floor-release box = %d %v, want 200", st, out) } // A revert is not judged by the floor: the low box can always go flat. if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "flat", "box": "ie"}, owner); st != http.StatusOK { t.Errorf("revert on the low box = %d %v, want 200", st, out) } seedBox(t, fx, "ie", "linux-arm64", "stable", "v0.25.0") if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, owner); st != http.StatusOK { t.Errorf("fleet migrate with every box on the floor release = %d %v, want 200", st, out) } // An unplaceable release (a dev box on a sha) blocks while it reports // no layout, and passes once its layout report is there: the report // is proof the build carries the pass. seedBox(t, fx, "ie", "linux-arm64", "stable", "abc1234") if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, owner); st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "ie runs abc1234, which cannot be placed against v0.25.0, and reports no layout") { t.Errorf("fleet migrate with an unplaceable release and no layout = %d %v, want 409", st, out) } b := status.CollectBox(nil, status.Probes{Release: "abc1234", Platform: "linux-arm64", SystemChannel: "stable", Layout: &status.LayoutReport{Mode: "none", Rows: map[string]status.RowLayout{"a.example.com": {Layout: "flat"}}}}, time.Now()) b.Label = "ie" if err := status.WriteBox(context.Background(), fx.rel, b); err != nil { t.Fatal(err) } if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/layout", map[string]string{"mode": "nested"}, owner); st != http.StatusOK { t.Errorf("fleet migrate with a sha box that reports its layout = %d %v, want 200", st, out) } } // Today's fleet: every box on v0.21.0 to v0.24.2, none reporting a layout // field (those builds have no pass). Rollback and a named update to a // pre-floor release both go through, because nothing on such a box can be // nested. A box on a sha is a build that might carry the pass, so its // silence still blocks. func TestTodaysFleetWithNoLayoutFieldKeepsRollbackAndUpdate(t *testing.T) { fx := newConsoleFixture(t) owner := cliToken(t, fx, "paul@keymail.dev", "code-layout-todays-fleet") ctx := context.Background() for i, v := range []string{"v0.21.0", "v0.22.2", "v0.24.2", "v0.24.2"} { seedBox(t, fx, fmt.Sprintf("box-%d", i), "linux-arm64", "stable", v) } sg := release.RootSigner(fx.priv) rel := func(v string) release.SystemRelease { return release.SystemRelease{Version: v, Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey(v, "linux-arm64"), SHA256: strings.Repeat("a", 64)}}} } if _, err := release.PromoteSystem(ctx, fx.rel, sg, "router", "stable", rel("v0.23.0"), release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } time.Sleep(1100 * time.Millisecond) if _, err := release.PromoteSystem(ctx, fx.rel, sg, "router", "stable", rel("v0.24.2"), release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.23.0"); got != "" { t.Fatalf("today's fleet blocked: %q", got) } time.Sleep(1100 * time.Millisecond) if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/rollback", map[string]string{"channel": "stable"}, owner); st != http.StatusOK || str(t, out, "version") != "v0.23.0" { t.Fatalf("rollback on today's fleet = %d %v, want 200 onto v0.23.0", st, out) } // A named pre-floor update passes the guard (and goes on to the job, // which fails on this fixture's absent feed; not what this is about). if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "stable", "version": "v0.24.2"}, owner); st == http.StatusConflict { t.Fatalf("named update on today's fleet refused by the layout guard: %v", out) } // A box whose release is a sha is a build that might carry the pass, // so its silence blocks. seedBox(t, fx, "box-9", "linux-arm64", "stable", "abc1234") if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.23.0"); !strings.Contains(got, "box-9 reports no layout field") { t.Fatalf("a sha box with no layout field: %q", got) } } // A box that is nested by default (no recorded intent) is judged exactly // like one under an explicit intent: its nested rows block a pre-floor // pointer move, and a box the default has not reached (flat rows, a // blocked reason) does not. The guards never read the intent, so the // default needs no rule of its own; this pins that. func TestLayoutGuardsTreatNestedByDefaultLikeAnIntent(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Now() seedLayoutBox(t, fx.rel, "ie", now, &status.LayoutReport{Mode: "nested", Default: true, Rows: map[string]status.RowLayout{"a.example.com": {Layout: "nested"}}}) if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); !strings.Contains(got, "ie: a.example.com is nested") { t.Fatalf("nested by default did not block: %q", got) } seedLayoutBox(t, fx.rel, "ie", now, &status.LayoutReport{Mode: "none", DefaultBlocked: "isolation not ready: probe fixture absent", Rows: map[string]status.RowLayout{"a.example.com": {Layout: "flat"}}}) if got := fx.srv.layoutDowngradeBlocked(ctx, "v0.19.0"); got != "" { t.Fatalf("a box the default has not reached blocked: %q", got) } }