| 1 | package release |
| 2 | |
| 3 | // The layout intent's WIRE SHAPE (isolated-exec design §8.2): what lands in |
| 4 | // control/layout.json, what a box makes of it for its own label, and that a |
| 5 | // schema this binary does not understand is refused rather than guessed at. |
| 6 | |
| 7 | import ( |
| 8 | "context" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | func TestLayoutIntentRoundTripAndScope(t *testing.T) { |
| 13 | s := NewDirStore(t.TempDir()) |
| 14 | ctx := context.Background() |
| 15 | |
| 16 | if _, found, err := LoadLayoutIntent(ctx, s, "kass-1"); err != nil || found { |
| 17 | t.Fatalf("absent object: found=%v err=%v", found, err) |
| 18 | } |
| 19 | |
| 20 | stamped, err := AppendLayoutIntent(ctx, s, LayoutIntent{Mode: LayoutModeNested, Scope: LayoutScope{Box: "kass-1"}}) |
| 21 | if err != nil { |
| 22 | t.Fatalf("append: %v", err) |
| 23 | } |
| 24 | if stamped.ID == "" || stamped.RequestedAt == "" { |
| 25 | t.Fatalf("append did not return the stamped id and timestamp: %+v", stamped) |
| 26 | } |
| 27 | |
| 28 | got, found, err := LoadLayoutIntent(ctx, s, "kass-1") |
| 29 | if err != nil || !found || got.Mode != LayoutModeNested { |
| 30 | t.Fatalf("round trip: %+v %v %v", got, found, err) |
| 31 | } |
| 32 | if got.ID != stamped.ID || got.RequestedAt != stamped.RequestedAt { |
| 33 | t.Fatalf("stored request = %+v, want the returned stamped values %+v", got, stamped) |
| 34 | } |
| 35 | if !got.Applies("kass-1") || got.Applies("kass-2") { |
| 36 | t.Error("box scope must match only the named box") |
| 37 | } |
| 38 | if _, found, err := LoadLayoutIntent(ctx, s, "kass-2"); err != nil || found { |
| 39 | t.Fatalf("a box-scoped intent must not apply to another box: found=%v err=%v", found, err) |
| 40 | } |
| 41 | |
| 42 | // A later fleet-wide request (empty scope) applies everywhere, including |
| 43 | // kass-1, and it is newer, so it is the one that wins there too. |
| 44 | second, err := AppendLayoutIntent(ctx, s, LayoutIntent{Mode: LayoutModeFlat}) |
| 45 | if err != nil { |
| 46 | t.Fatalf("second append: %v", err) |
| 47 | } |
| 48 | if second.ID == stamped.ID { |
| 49 | t.Fatalf("second append reused the first request's id %q", second.ID) |
| 50 | } |
| 51 | got, found, err = LoadLayoutIntent(ctx, s, "kass-1") |
| 52 | if err != nil || !found || got.Mode != LayoutModeFlat { |
| 53 | t.Fatalf("the newest applying request must win: %+v %v %v", got, found, err) |
| 54 | } |
| 55 | if !got.Applies("anything") { |
| 56 | t.Error("an empty scope is the fleet") |
| 57 | } |
| 58 | |
| 59 | // A newer schema is an error, not a guess. The store has no raw put, so |
| 60 | // the fixture is written by deleting the key and putting the body fresh |
| 61 | // (create-only). |
| 62 | if err := s.Delete(ctx, LayoutIntentKey()); err != nil { |
| 63 | t.Fatalf("delete: %v", err) |
| 64 | } |
| 65 | if _, err := s.Put(ctx, LayoutIntentKey(), []byte(`{"schema":2,"requests":[{"mode":"nested"}]}`), ""); err != nil { |
| 66 | t.Fatalf("put newer-schema fixture: %v", err) |
| 67 | } |
| 68 | if _, _, err := LoadLayoutIntent(ctx, s, "kass-1"); err == nil { |
| 69 | t.Fatal("a newer schema must be an error, not a guess") |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func TestValidLayoutMode(t *testing.T) { |
| 74 | for _, m := range []string{LayoutModeFlat, LayoutModeNested} { |
| 75 | if !ValidLayoutMode(m) { |
| 76 | t.Errorf("ValidLayoutMode(%q) = false, want true", m) |
| 77 | } |
| 78 | } |
| 79 | for _, m := range []string{LayoutModeNone, "bogus"} { |
| 80 | if ValidLayoutMode(m) { |
| 81 | t.Errorf("ValidLayoutMode(%q) = true, want false", m) |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestAppendLayoutIntentRefusesAnInvalidMode(t *testing.T) { |
| 87 | s := NewDirStore(t.TempDir()) |
| 88 | stamped, err := AppendLayoutIntent(context.Background(), s, LayoutIntent{Mode: "bogus"}) |
| 89 | if err == nil { |
| 90 | t.Fatal("an invalid mode must be refused, not written") |
| 91 | } |
| 92 | if stamped != (LayoutIntent{}) { |
| 93 | t.Fatalf("a refused append must return the zero value, got %+v", stamped) |
| 94 | } |
| 95 | if _, found, err := LoadLayoutIntent(context.Background(), s, "any"); err != nil || found { |
| 96 | t.Fatalf("a refused append must leave the object absent: found=%v err=%v", found, err) |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | // TestLayoutFloorIsTheFirstReleaseWhereNestedRowsWake pins the console's |
| 101 | // downgrade floor to the release history. v0.20.0 is the first tag whose |
| 102 | // unit set carries data-instances.mount (git ls-tree v0.19.0 and v0.20.0 |
| 103 | // internal/host/units/, checked 2026-10-06), but a nested row cannot wake |
| 104 | // on v0.20.0 to v0.24.2: their carlos-edge.service still carries |
| 105 | // RestrictSUIDSGID=, which makes the agent's openat2 fail with ENOSYS, and |
| 106 | // every nested open goes through it. Plan 4 (7b7d3026) dropped the line; |
| 107 | // no tag contains it yet, so the floor is the next release, v0.25.0, which |
| 108 | // must be cut from a main that contains 7b7d3026. Moving either constant |
| 109 | // means re-checking the tags, not editing this test to match. |
| 110 | func TestLayoutFloorIsTheFirstReleaseWhereNestedRowsWake(t *testing.T) { |
| 111 | if LayoutFloor != (SemVer{Major: 0, Minor: 25, Patch: 0}) { |
| 112 | t.Fatalf("LayoutFloor = %+v, want v0.25.0 (the first release carrying 7b7d3026's edge openat2 fix)", LayoutFloor) |
| 113 | } |
| 114 | for v, pre := range map[string]bool{ |
| 115 | "v0.19.0": true, "v0.20.0": true, "v0.24.2": true, "0.24.9": true, |
| 116 | "v0.25.0": false, "v0.26.0": false, "v1.0.0": false, |
| 117 | // Not a version: nobody can place it against the floor, so it is refused. |
| 118 | "dev": true, "": true, "v0.25": true, "abc123": true, |
| 119 | } { |
| 120 | if got := PreLayout(v); got != pre { |
| 121 | t.Errorf("PreLayout(%q) = %v, want %v", v, got, pre) |
| 122 | } |
| 123 | } |
| 124 | // BelowFloor is the placeable half of PreLayout: an unparseable tag is |
| 125 | // false, and the caller decides what that means for its own rule. |
| 126 | for v, below := range map[string]bool{"v0.19.0": true, "v0.24.2": true, "v0.25.0": false, "dev": false, "": false} { |
| 127 | if got := BelowFloor(v, LayoutFloor); got != below { |
| 128 | t.Errorf("BelowFloor(%q, LayoutFloor) = %v, want %v", v, got, below) |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func TestRecordLayoutIntentAppendsWithForceAndScope(t *testing.T) { |
| 134 | s := NewDirStore(t.TempDir()) |
| 135 | ctx := context.Background() |
| 136 | first, err := RecordLayoutIntent(ctx, s, LayoutModeNested, "kass-1", true) |
| 137 | if err != nil || first.ID == "" || first.Mode != LayoutModeNested || first.Scope.Box != "kass-1" || !first.Force { |
| 138 | t.Fatalf("first = %+v %v", first, err) |
| 139 | } |
| 140 | second, err := RecordLayoutIntent(ctx, s, LayoutModeFlat, "", false) |
| 141 | if err != nil || second.ID == first.ID || second.Mode != LayoutModeFlat || second.Scope.Box != "" || second.Force { |
| 142 | t.Fatalf("second = %+v %v", second, err) |
| 143 | } |
| 144 | got, found, err := LoadLayoutIntent(ctx, s, "kass-1") |
| 145 | if err != nil || !found || got.ID != second.ID { |
| 146 | t.Fatalf("kass-1 sees %+v (found %v, err %v), want the newest request %s", got, found, err, second.ID) |
| 147 | } |
| 148 | if _, err := RecordLayoutIntent(ctx, s, "sideways", "", false); err == nil { |
| 149 | t.Fatal("bad mode accepted") |
| 150 | } |
| 151 | } |
| 152 | |