carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net"
8 "net/http"
9 "os"
10 "path/filepath"
11 "strings"
12 "syscall"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/caps"
17)
18
19func TestStagePointLiveRoundTrip(t *testing.T) {
20 opt := t.TempDir()
21 if err := StageArtifact(opt, "bab", "v1abc", "myapp", []byte("the binary")); err != nil {
22 t.Fatalf("StageArtifact: %v", err)
23 }
24 p := VersionPath(opt, "bab", "v1abc", "myapp")
25 st, err := os.Stat(p)
26 if err != nil {
27 t.Fatalf("staged file: %v", err)
28 }
29 if st.Mode().Perm() != 0o755 {
30 t.Fatalf("staged mode = %v, want 0755 (+x survives umask)", st.Mode().Perm())
31 }
32 // An unmanaged host reads as "" with no error.
33 if v, err := LiveVersion(opt, "h.example.com"); err != nil || v != "" {
34 t.Fatalf("LiveVersion before pointing = %q, %v", v, err)
35 }
36 if err := PointLive(opt, "h.example.com", "bab", "v1abc", "myapp"); err != nil {
37 t.Fatalf("PointLive: %v", err)
38 }
39 v, err := LiveVersion(opt, "h.example.com")
40 if err != nil || v != "v1abc" {
41 t.Fatalf("LiveVersion = %q, %v; want v1abc", v, err)
42 }
43 // Repointing at an unstaged version is refused (no dangling links).
44 if err := PointLive(opt, "h.example.com", "bab", "vGhost", "myapp"); err == nil {
45 t.Fatalf("PointLive accepted an unstaged version")
46 }
47 // The refusal left the link untouched.
48 if v, _ := LiveVersion(opt, "h.example.com"); v != "v1abc" {
49 t.Fatalf("live link moved to %q after refused repoint", v)
50 }
51 // A second staged version swaps atomically.
52 if err := StageArtifact(opt, "bab", "v2def", "myapp", []byte("newer binary")); err != nil {
53 t.Fatalf("StageArtifact v2: %v", err)
54 }
55 if err := PointLive(opt, "h.example.com", "bab", "v2def", "myapp"); err != nil {
56 t.Fatalf("PointLive v2: %v", err)
57 }
58 if v, _ := LiveVersion(opt, "h.example.com"); v != "v2def" {
59 t.Fatalf("LiveVersion = %q, want v2def", v)
60 }
61}
62
63func TestValidVersion(t *testing.T) {
64 for v, want := range map[string]bool{
65 "abc1234": true, "1.2.3": true, "a_b-c.d": true,
66 "": false, ".": false, "..": false, "a/b": false, "a b": false,
67 } {
68 if got := ValidVersion(v); got != want {
69 t.Errorf("ValidVersion(%q) = %v, want %v", v, got, want)
70 }
71 }
72}
73
74func TestOptDirEnvOverride(t *testing.T) {
75 t.Setenv("CARLOS_OPT_DIR", "/x/y")
76 if d := OptDir(); d != "/x/y" {
77 t.Fatalf("OptDir = %q, want /x/y", d)
78 }
79 t.Setenv("CARLOS_OPT_DIR", "")
80 if d := OptDir(); d != "/opt/carlos" {
81 t.Fatalf("OptDir = %q, want /opt/carlos", d)
82 }
83}
84
85func TestProbeAndWaitHealthyVersion(t *testing.T) {
86 sock := filepath.Join(t.TempDir(), "h.sock")
87 ln, err := net.Listen("unix", sock)
88 if err != nil {
89 if errors.Is(err, os.ErrPermission) || strings.Contains(err.Error(), "operation not permitted") {
90 t.Skipf("unix sockets unavailable in this sandbox: %v", err)
91 }
92 t.Fatalf("listen: %v", err)
93 }
94 mux := http.NewServeMux()
95 mux.HandleFunc("/api/version", func(w http.ResponseWriter, r *http.Request) {
96 w.Write([]byte(`{"build":"v1abc"}`))
97 })
98 srv := &http.Server{Handler: mux}
99 go srv.Serve(ln)
100 defer srv.Close()
101
102 build, ok := ProbeVersion(sock)
103 if !ok || build != "v1abc" {
104 t.Fatalf("ProbeVersion = %q, %v; want v1abc, true", build, ok)
105 }
106 if err := WaitHealthyVersion(sock, "v1abc", 3*time.Second); err != nil {
107 t.Fatalf("WaitHealthyVersion: %v", err)
108 }
109 // A wrong expected build times out with the mismatch surfaced.
110 if err := WaitHealthyVersion(sock, "vOther", 400*time.Millisecond); err == nil {
111 t.Fatalf("WaitHealthyVersion accepted the wrong build")
112 }
113 // A dead socket probes false.
114 if _, ok := ProbeVersion(filepath.Join(t.TempDir(), "dead.sock")); ok {
115 t.Fatalf("ProbeVersion answered on a dead socket")
116 }
117}
118
119// TestStagedManifestPathIsASiblingForBothAccountShapes pins the exact path
120// StageManifest/ReadStagedManifest agree on: the ops account keeps the
121// pre-accounts shape byte for byte (<opt>/versions/<ver>/<app>...), and every
122// other account gets a level in between — the same split VersionPath already
123// makes, so the manifest is always a SIBLING of the binary, never a new
124// directory shape.
125func TestStagedManifestPathIsASiblingForBothAccountShapes(t *testing.T) {
126 opt := t.TempDir()
127 if got, want := StagedManifestPath(opt, OpsAccount, "v1", "myapp"),
128 filepath.Join(opt, "versions", "v1", "myapp.manifest.json"); got != want {
129 t.Fatalf("ops path = %q, want %q", got, want)
130 }
131 if got, want := StagedManifestPath(opt, "acme", "v1", "myapp"),
132 filepath.Join(opt, "versions", "v1", "acme", "myapp.manifest.json"); got != want {
133 t.Fatalf("non-ops path = %q, want %q", got, want)
134 }
135 // It is always exactly VersionPath + ".manifest.json" — a sibling, not a
136 // new directory.
137 if got, want := StagedManifestPath(opt, "acme", "v1", "myapp"), VersionPath(opt, "acme", "v1", "myapp")+".manifest.json"; got != want {
138 t.Fatalf("sibling path = %q, want %q", got, want)
139 }
140}
141
142// TestReadStagedManifestIsNilNilWhenAbsent pins the three-state contract:
143// absence is (nil, nil) — "unknown", not "none" — so a caller can tell a box
144// staged before this feature existed apart from a manifest that legitimately
145// declares no capabilities.
146func TestReadStagedManifestIsNilNilWhenAbsent(t *testing.T) {
147 opt := t.TempDir()
148 m, err := ReadStagedManifest(opt, OpsAccount, "v1", "myapp")
149 if err != nil || m != nil {
150 t.Fatalf("ReadStagedManifest on an absent sibling = %+v, %v; want nil, nil", m, err)
151 }
152}
153
154// TestStageManifestRoundTripsCapabilities exercises the plain write/read
155// path a fresh ensureStaged download takes, including that a re-stage
156// OVERWRITES rather than appending (the reviewer's named concern).
157func TestStageManifestRoundTripsCapabilities(t *testing.T) {
158 opt := t.TempDir()
159 // StageManifest is always called once the version directory exists (via
160 // StageArtifact, or because the binary is already staged in the
161 // back-fill path) — it is a sibling writer, not a directory creator.
162 if err := StageArtifact(opt, OpsAccount, "v1abc", "keymail", []byte("binary")); err != nil {
163 t.Fatalf("StageArtifact: %v", err)
164 }
165 m := Manifest{
166 Schema: 1, App: "keymail", Version: "v1abc", Kind: "binary",
167 Artifacts: map[string]Artifact{"linux-arm64": {File: "linux-arm64", SHA256: strings.Repeat("ab", 32), Size: 12}},
168 Capabilities: &caps.Set{V: 1, Command: "relay", Daemon: true, RuntimeDir: "keymail"},
169 }
170 raw, err := json.MarshalIndent(&m, "", " ")
171 if err != nil {
172 t.Fatalf("marshal: %v", err)
173 }
174 if err := StageManifest(opt, OpsAccount, "v1abc", "keymail", raw); err != nil {
175 t.Fatalf("StageManifest: %v", err)
176 }
177 got, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail")
178 if err != nil {
179 t.Fatalf("ReadStagedManifest: %v", err)
180 }
181 if got == nil || got.App != "keymail" || got.Version != "v1abc" {
182 t.Fatalf("got = %+v, want App/Version to round-trip", got)
183 }
184 if got.Capabilities == nil || got.Capabilities.Command != "relay" || !got.Capabilities.Daemon || got.Capabilities.RuntimeDir != "keymail" {
185 t.Fatalf("Capabilities = %+v, did not round-trip", got.Capabilities)
186 }
187
188 // Re-staging the SAME version overwrites, it does not append: a stale
189 // half of an old sibling left dangling after the rename would otherwise
190 // corrupt the JSON.
191 m2 := m
192 m2.Capabilities = &caps.Set{V: 1, Daemon: true}
193 raw2, err := json.MarshalIndent(&m2, "", " ")
194 if err != nil {
195 t.Fatalf("marshal: %v", err)
196 }
197 if err := StageManifest(opt, OpsAccount, "v1abc", "keymail", raw2); err != nil {
198 t.Fatalf("re-stage: %v", err)
199 }
200 got2, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail")
201 if err != nil {
202 t.Fatalf("ReadStagedManifest after re-stage: %v", err)
203 }
204 if got2.Capabilities.Command != "" || !got2.Capabilities.Daemon {
205 t.Fatalf("re-stage did not overwrite: %+v", got2.Capabilities)
206 }
207}
208
209// TestStageManifestCreatesTheAccountDir pins issue #58: a version staged by
210// a pre-capability binary has NO <acct>/ subdirectory at all (the bare
211// no-account layout StageArtifact(optDir, "", ...) produces — exactly what
212// the old binary wrote). Back-filling a sibling for that version under a
213// real account must not assume the directory already exists; StageManifest
214// has to create it itself, the way StageArtifact already does for the
215// binary, or the write fails ENOENT on every affected box, forever (seen
216// live, hourly, on both production boxes).
217func TestStageManifestCreatesTheAccountDir(t *testing.T) {
218 // Tighten the umask so the account dir StageManifest creates would come
219 // out non-traversable (0700) if the Chmod fix didn't run — otherwise the
220 // perm assertion below would pass even with the umask-masking bug back.
221 old := syscall.Umask(0o077)
222 defer syscall.Umask(old)
223
224 opt := t.TempDir()
225 // Pre-capability layout: versions/<ver>/<app>, no account level at all.
226 if err := StageArtifact(opt, "", "v1abc", "myapp", []byte("binary")); err != nil {
227 t.Fatalf("StageArtifact: %v", err)
228 }
229 if err := StageManifest(opt, "bad", "v1abc", "myapp", []byte("{}")); err != nil {
230 t.Fatalf("StageManifest: %v", err)
231 }
232 p := StagedManifestPath(opt, "bad", "v1abc", "myapp")
233 st, err := os.Stat(p)
234 if err != nil {
235 t.Fatalf("staged manifest: %v", err)
236 }
237 if st.Mode().Perm() != 0o644 {
238 t.Fatalf("staged manifest mode = %v, want 0644", st.Mode().Perm())
239 }
240 dirSt, err := os.Stat(filepath.Dir(p))
241 if err != nil {
242 t.Fatalf("staged account dir: %v", err)
243 }
244 if dirSt.Mode().Perm() != 0o755 {
245 t.Fatalf("staged account dir mode = %v, want 0755", dirSt.Mode().Perm())
246 }
247}
248
249// TestReadStagedManifestRunsTheValidationGate pins the Task-2-review
250// backstop: a stale sibling written by a NEWER binary — one that understood
251// a "v" this binary does not — must refuse cleanly here. This particular
252// "v" is caught by caps.Set's own DECODER (UnmarshalJSON's v-range check)
253// during ReadStagedManifest's json.Unmarshal, before validateManifest is
254// even reached — see TestReadStagedManifestRefusesASemanticallyInvalidSiblingEvenThoughItDecodes
255// below for the sibling that reaches validateManifest's caps.ValidCapabilities
256// call specifically. Written directly to disk (bypassing StageManifest's
257// own marshal), exactly as a newer binary's bytes would arrive.
258func TestReadStagedManifestRunsTheValidationGate(t *testing.T) {
259 opt := t.TempDir()
260 raw := []byte(`{
261 "schema": 1,
262 "app": "keymail",
263 "version": "v1abc",
264 "kind": "binary",
265 "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } },
266 "capabilities": { "v": 2, "daemon": true }
267}`)
268 if err := os.MkdirAll(filepath.Dir(StagedManifestPath(opt, OpsAccount, "v1abc", "keymail")), 0o755); err != nil {
269 t.Fatal(err)
270 }
271 if err := os.WriteFile(StagedManifestPath(opt, OpsAccount, "v1abc", "keymail"), raw, 0o644); err != nil {
272 t.Fatal(err)
273 }
274 if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil {
275 t.Fatal("a sibling declaring a v this binary does not speak was accepted")
276 }
277}
278
279// TestReadStagedManifestRefusesASemanticallyInvalidSiblingEvenThoughItDecodes
280// is the mutation-killing companion to the test above (2026-08-03 review,
281// killing test 1): runtime_dir "systemd" decodes CLEANLY — RuntimeDir is a
282// plain string field with no charset or reserved-word check in caps.Set's
283// decoder — and is refused only by ValidCapabilities' reserved-name check,
284// which ReadStagedManifest reaches exclusively through its validateManifest
285// call. A mutant that deletes that call (while leaving json.Unmarshal in
286// place) would let this exact sibling through; this test is the one that
287// catches it.
288func TestReadStagedManifestRefusesASemanticallyInvalidSiblingEvenThoughItDecodes(t *testing.T) {
289 opt := t.TempDir()
290 raw := []byte(`{
291 "schema": 1,
292 "app": "keymail",
293 "version": "v1abc",
294 "kind": "binary",
295 "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } },
296 "capabilities": { "v": 1, "runtime_dir": "systemd" }
297}`)
298 p := StagedManifestPath(opt, OpsAccount, "v1abc", "keymail")
299 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
300 t.Fatal(err)
301 }
302 if err := os.WriteFile(p, raw, 0o644); err != nil {
303 t.Fatal(err)
304 }
305 if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil {
306 t.Fatal("a sibling with a reserved runtime_dir (refused only by ValidCapabilities, via validateManifest) was accepted")
307 }
308}
309
310// TestReadStagedManifestRefusesAnIdentityMismatch pins I3 (2026-08-03
311// review): the sibling's OWN App/Version fields must match the path it was
312// read from, in both directions. Without this, a sibling at keymail's path
313// whose body claims a different app or version is trusted against the
314// PATH's identity — so a sibling written for (or by) the wrong app would
315// have its capabilities validated and materialized under keymail's name.
316func TestReadStagedManifestRefusesAnIdentityMismatch(t *testing.T) {
317 opt := t.TempDir()
318 write := func(bodyApp, bodyVer string) {
319 t.Helper()
320 raw := []byte(`{
321 "schema": 1,
322 "app": "` + bodyApp + `",
323 "version": "` + bodyVer + `",
324 "kind": "binary",
325 "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } }
326}`)
327 p := StagedManifestPath(opt, OpsAccount, "v1abc", "keymail")
328 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
329 t.Fatal(err)
330 }
331 if err := os.WriteFile(p, raw, 0o644); err != nil {
332 t.Fatal(err)
333 }
334 }
335
336 // Wrong app: a sibling at keymail's path whose body claims "console".
337 write("console", "v1abc")
338 if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil {
339 t.Fatal("a sibling naming a different app than its path was accepted")
340 }
341
342 // Wrong version: same app, a different version in the body.
343 write("keymail", "vOTHER")
344 if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil {
345 t.Fatal("a sibling naming a different version than its path was accepted")
346 }
347}
348
349// modStamp writes key and pins its mtime — candidate ranking reads nothing
350// else.
351func modStamp(t *testing.T, s *DirStore, dir, key string, mod time.Time) {
352 t.Helper()
353 if _, err := s.Put(context.Background(), key, []byte(`x`), ""); err != nil {
354 t.Fatal(err)
355 }
356 if err := os.Chtimes(filepath.Join(dir, filepath.FromSlash(key)), mod, mod); err != nil {
357 t.Fatal(err)
358 }
359}
360
361// TestVersionCandidatesRanked: with a mod-aware store the candidates are the
362// newest max versions by each manifest object's own stamp — the mod order
363// here deliberately DISAGREES with version-name order, so a selection that
364// quietly ranked by name would fail — and a version whose ship never wrote a
365// manifest ranks by its newest artifact, so an aborted ship surfaces (as the
366// caller's error row) rather than vanishing.
367func TestVersionCandidatesRanked(t *testing.T) {
368 dir := t.TempDir()
369 s := NewDirStore(dir)
370 base := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC)
371 for v, mod := range map[string]time.Time{
372 "aa": base.Add(3 * time.Hour), // newest ship, first lexically too
373 "zz": base.Add(2 * time.Hour), // second-newest despite sorting last by name
374 "cc": base.Add(1 * time.Hour),
375 "bb": base, // oldest — the one the cap must cut
376 } {
377 modStamp(t, s, dir, ManifestKey("bab", "kass", v), mod)
378 // An artifact NEWER than its own manifest must not outrank the
379 // manifest's stamp.
380 modStamp(t, s, dir, ArtifactKey("bab", "kass", v, "linux-arm64"), mod.Add(30*time.Minute))
381 }
382 // bb's ARTIFACT ends up newer than cc's MANIFEST: a ranking that took
383 // the max over all of a version's objects instead of letting the
384 // manifest's own stamp win would rank bb (100m) over cc (60m) and evict
385 // cc from the want below — this line is what pins the rule.
386 bbArtifact := filepath.Join(dir, filepath.FromSlash(ArtifactKey("bab", "kass", "bb", "linux-arm64")))
387 if err := os.Chtimes(bbArtifact, base.Add(100*time.Minute), base.Add(100*time.Minute)); err != nil {
388 t.Fatal(err)
389 }
390 // A ship that died before manifest.json: artifact only, newest of all.
391 modStamp(t, s, dir, ArtifactKey("bab", "kass", "dead1", "linux-arm64"), base.Add(4*time.Hour))
392
393 got, ranked, err := VersionCandidates(context.Background(), s, "bab", "kass", 4)
394 if err != nil {
395 t.Fatal(err)
396 }
397 if !ranked {
398 t.Fatal("DirStore is a ModLister — candidates must come back ranked")
399 }
400 want := []string{"dead1", "aa", "zz", "cc"}
401 if len(got) != len(want) {
402 t.Fatalf("candidates = %v, want %v", got, want)
403 }
404 for i := range want {
405 if got[i] != want[i] {
406 t.Fatalf("candidates = %v, want %v", got, want)
407 }
408 }
409}
410
411// plainStore hides ListMod behind the bare interface, the shape every test
412// fake has.
413type plainStore struct{ Store }
414
415// TestVersionCandidatesFallback: a store without ListMod answers every
416// version, unranked and uncapped, in listing order — the caller's
417// fetch-everything path, byte-identical to the pre-ModLister behavior.
418func TestVersionCandidatesFallback(t *testing.T) {
419 dir := t.TempDir()
420 s := NewDirStore(dir)
421 base := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC)
422 for i, v := range []string{"v1", "v2", "v3"} {
423 modStamp(t, s, dir, ManifestKey("bab", "kass", v), base.Add(time.Duration(i)*time.Hour))
424 }
425 got, ranked, err := VersionCandidates(context.Background(), plainStore{s}, "bab", "kass", 2)
426 if err != nil {
427 t.Fatal(err)
428 }
429 if ranked {
430 t.Fatal("a bare Store cannot rank")
431 }
432 want := []string{"v1", "v2", "v3"} // listing order, all of them — no cap
433 if len(got) != len(want) {
434 t.Fatalf("candidates = %v, want %v", got, want)
435 }
436 for i := range want {
437 if got[i] != want[i] {
438 t.Fatalf("candidates = %v, want %v", got, want)
439 }
440 }
441}
442