| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "testing" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | func TestSystemPathsAreDerivedNotTrusted(t *testing.T) { |
| 12 | if got := systemCachePath("/data", "v1.2.3"); got != "/data/system/router/v1.2.3/carlos" { |
| 13 | t.Fatalf("cache path = %s", got) |
| 14 | } |
| 15 | if got := systemRequestPath("/data"); got != "/data/system/router/requested.json" { |
| 16 | t.Fatalf("request path = %s", got) |
| 17 | } |
| 18 | if got := systemRootDir("/opt/carlos"); got != "/opt/carlos/system/router" { |
| 19 | t.Fatalf("root dir = %s", got) |
| 20 | } |
| 21 | for _, bad := range []string{"", ".", "..", "../x", "a/b", ".hidden", "v1 2", string(make([]byte, 65))} { |
| 22 | if validSystemVersion(bad) { |
| 23 | t.Errorf("validSystemVersion(%q) = true", bad) |
| 24 | } |
| 25 | } |
| 26 | for _, good := range []string{"v0.18.0", "66587fa", "a", "release-2026.09.06_rc1"} { |
| 27 | if !validSystemVersion(good) { |
| 28 | t.Errorf("validSystemVersion(%q) = false", good) |
| 29 | } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestWriteFileDurableLeavesWholeFileOrNothing(t *testing.T) { |
| 34 | dir := t.TempDir() |
| 35 | p := filepath.Join(dir, "journal.json") |
| 36 | if err := writeFileDurable(p, []byte("one"), 0o644); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | if err := writeFileDurable(p, []byte("two"), 0o644); err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | b, _ := os.ReadFile(p) |
| 43 | if string(b) != "two" { |
| 44 | t.Fatalf("content = %q", b) |
| 45 | } |
| 46 | if _, err := os.Stat(p + ".tmp"); !errors.Is(err, os.ErrNotExist) { |
| 47 | t.Fatal("temp left behind") |
| 48 | } |
| 49 | st, _ := os.Stat(p) |
| 50 | if st.Mode().Perm() != 0o644 { |
| 51 | t.Fatalf("mode = %v", st.Mode().Perm()) |
| 52 | } |
| 53 | // A directory that does not exist is an error, not a silent no-op. |
| 54 | if err := writeFileDurable(filepath.Join(dir, "no", "such", "f"), []byte("x"), 0o644); err == nil { |
| 55 | t.Fatal("wrote into a missing directory") |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestSystemStateReadersRoundTrip(t *testing.T) { |
| 60 | opt := t.TempDir() |
| 61 | root := systemRootDir(opt) |
| 62 | if err := os.MkdirAll(root, 0o755); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | if r, err := readSystemResult(opt); err != nil || r != nil { |
| 66 | t.Fatalf("missing result: %v %v", r, err) |
| 67 | } |
| 68 | seen, err := readSystemSeen(opt) |
| 69 | if err != nil || len(seen) != 0 { |
| 70 | t.Fatalf("missing seen: %v %v", seen, err) |
| 71 | } |
| 72 | if a, err := readSystemApplied(opt); err != nil || a != nil { |
| 73 | t.Fatalf("missing applied: %v %v", a, err) |
| 74 | } |
| 75 | if j, err := readSystemJournal(opt); err != nil || j != nil { |
| 76 | t.Fatalf("missing journal: %v %v", j, err) |
| 77 | } |
| 78 | var nilResult *systemResult |
| 79 | if nilResult.matches("stable", "v1", time.Time{}) || nilResult.terminal() { |
| 80 | t.Fatal("nil *systemResult must answer matches/terminal as false, not panic") |
| 81 | } |
| 82 | at := time.Date(2026, 9, 6, 9, 0, 0, 0, time.UTC) |
| 83 | res := systemResult{Channel: "stable", Version: "v1", PromotedAt: at, Outcome: systemOutcomeRolledBack, Reason: "gate", At: at} |
| 84 | if err := writeJSONDurable(filepath.Join(root, "result.json"), res, 0o644); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | got, err := readSystemResult(opt) |
| 88 | if err != nil || !got.matches("stable", "v1", at) || !got.terminal() { |
| 89 | t.Fatalf("result = %+v err=%v", got, err) |
| 90 | } |
| 91 | if got.matches("canary", "v1", at) || got.matches("stable", "v2", at) || got.matches("stable", "v1", at.Add(time.Second)) { |
| 92 | t.Fatal("matches ignores part of the tuple") |
| 93 | } |
| 94 | got.Outcome = systemOutcomeDeferred |
| 95 | if got.terminal() { |
| 96 | t.Fatal("deferred is not terminal") |
| 97 | } |
| 98 | got.Outcome = systemOutcomeNoted |
| 99 | if got.terminal() { |
| 100 | t.Fatal("noted is not terminal for the leg: the version may still differ next time") |
| 101 | } |
| 102 | seen["stable"] = at |
| 103 | if err := writeJSONDurable(filepath.Join(root, "seen.json"), seen, 0o644); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | seen2, _ := readSystemSeen(opt) |
| 107 | if !seen2["stable"].Equal(at) { |
| 108 | t.Fatalf("seen = %v", seen2) |
| 109 | } |
| 110 | j := systemJournal{Phase: systemPhaseInstalled, Target: "v1", PromotedAt: at.Add(-time.Hour), PrevBuild: "v0", UnitsAdded: []string{"a.path"}, StartedAt: at} |
| 111 | if err := writeJSONDurable(filepath.Join(root, "journal.json"), j, 0o600); err != nil { |
| 112 | t.Fatal(err) |
| 113 | } |
| 114 | j2, err := readSystemJournal(opt) |
| 115 | if err != nil || j2.Phase != systemPhaseInstalled || j2.UnitsAdded[0] != "a.path" || !j2.promoted().Equal(at.Add(-time.Hour)) { |
| 116 | t.Fatalf("journal = %+v err=%v", j2, err) |
| 117 | } |
| 118 | // A journal from before the field existed still names its apply. |
| 119 | old := &systemJournal{Phase: systemPhaseInstalled, Target: "v1", StartedAt: at} |
| 120 | if !old.promoted().Equal(at) { |
| 121 | t.Fatalf("promoted = %v, want the started_at a journal without promoted_at has", old.promoted()) |
| 122 | } |
| 123 | applied := systemApplied{Version: "v1", Channel: "stable", At: at} |
| 124 | if err := writeJSONDurable(filepath.Join(root, "applied.json"), applied, 0o644); err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
| 127 | a2, err := readSystemApplied(opt) |
| 128 | if err != nil || a2.Version != "v1" || a2.Channel != "stable" || !a2.At.Equal(at) { |
| 129 | t.Fatalf("applied = %+v err=%v", a2, err) |
| 130 | } |
| 131 | } |
| 132 | |