package release import ( "context" "encoding/json" "errors" "net" "net/http" "os" "path/filepath" "strings" "syscall" "testing" "time" "github.com/carlosframework/platform/internal/caps" ) func TestStagePointLiveRoundTrip(t *testing.T) { opt := t.TempDir() if err := StageArtifact(opt, "bab", "v1abc", "myapp", []byte("the binary")); err != nil { t.Fatalf("StageArtifact: %v", err) } p := VersionPath(opt, "bab", "v1abc", "myapp") st, err := os.Stat(p) if err != nil { t.Fatalf("staged file: %v", err) } if st.Mode().Perm() != 0o755 { t.Fatalf("staged mode = %v, want 0755 (+x survives umask)", st.Mode().Perm()) } // An unmanaged host reads as "" with no error. if v, err := LiveVersion(opt, "h.example.com"); err != nil || v != "" { t.Fatalf("LiveVersion before pointing = %q, %v", v, err) } if err := PointLive(opt, "h.example.com", "bab", "v1abc", "myapp"); err != nil { t.Fatalf("PointLive: %v", err) } v, err := LiveVersion(opt, "h.example.com") if err != nil || v != "v1abc" { t.Fatalf("LiveVersion = %q, %v; want v1abc", v, err) } // Repointing at an unstaged version is refused (no dangling links). if err := PointLive(opt, "h.example.com", "bab", "vGhost", "myapp"); err == nil { t.Fatalf("PointLive accepted an unstaged version") } // The refusal left the link untouched. if v, _ := LiveVersion(opt, "h.example.com"); v != "v1abc" { t.Fatalf("live link moved to %q after refused repoint", v) } // A second staged version swaps atomically. if err := StageArtifact(opt, "bab", "v2def", "myapp", []byte("newer binary")); err != nil { t.Fatalf("StageArtifact v2: %v", err) } if err := PointLive(opt, "h.example.com", "bab", "v2def", "myapp"); err != nil { t.Fatalf("PointLive v2: %v", err) } if v, _ := LiveVersion(opt, "h.example.com"); v != "v2def" { t.Fatalf("LiveVersion = %q, want v2def", v) } } func TestValidVersion(t *testing.T) { for v, want := range map[string]bool{ "abc1234": true, "1.2.3": true, "a_b-c.d": true, "": false, ".": false, "..": false, "a/b": false, "a b": false, } { if got := ValidVersion(v); got != want { t.Errorf("ValidVersion(%q) = %v, want %v", v, got, want) } } } func TestOptDirEnvOverride(t *testing.T) { t.Setenv("CARLOS_OPT_DIR", "/x/y") if d := OptDir(); d != "/x/y" { t.Fatalf("OptDir = %q, want /x/y", d) } t.Setenv("CARLOS_OPT_DIR", "") if d := OptDir(); d != "/opt/carlos" { t.Fatalf("OptDir = %q, want /opt/carlos", d) } } func TestProbeAndWaitHealthyVersion(t *testing.T) { sock := filepath.Join(t.TempDir(), "h.sock") ln, err := net.Listen("unix", sock) if err != nil { if errors.Is(err, os.ErrPermission) || strings.Contains(err.Error(), "operation not permitted") { t.Skipf("unix sockets unavailable in this sandbox: %v", err) } t.Fatalf("listen: %v", err) } mux := http.NewServeMux() mux.HandleFunc("/api/version", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"build":"v1abc"}`)) }) srv := &http.Server{Handler: mux} go srv.Serve(ln) defer srv.Close() build, ok := ProbeVersion(sock) if !ok || build != "v1abc" { t.Fatalf("ProbeVersion = %q, %v; want v1abc, true", build, ok) } if err := WaitHealthyVersion(sock, "v1abc", 3*time.Second); err != nil { t.Fatalf("WaitHealthyVersion: %v", err) } // A wrong expected build times out with the mismatch surfaced. if err := WaitHealthyVersion(sock, "vOther", 400*time.Millisecond); err == nil { t.Fatalf("WaitHealthyVersion accepted the wrong build") } // A dead socket probes false. if _, ok := ProbeVersion(filepath.Join(t.TempDir(), "dead.sock")); ok { t.Fatalf("ProbeVersion answered on a dead socket") } } // TestStagedManifestPathIsASiblingForBothAccountShapes pins the exact path // StageManifest/ReadStagedManifest agree on: the ops account keeps the // pre-accounts shape byte for byte (/versions//...), and every // other account gets a level in between — the same split VersionPath already // makes, so the manifest is always a SIBLING of the binary, never a new // directory shape. func TestStagedManifestPathIsASiblingForBothAccountShapes(t *testing.T) { opt := t.TempDir() if got, want := StagedManifestPath(opt, OpsAccount, "v1", "myapp"), filepath.Join(opt, "versions", "v1", "myapp.manifest.json"); got != want { t.Fatalf("ops path = %q, want %q", got, want) } if got, want := StagedManifestPath(opt, "acme", "v1", "myapp"), filepath.Join(opt, "versions", "v1", "acme", "myapp.manifest.json"); got != want { t.Fatalf("non-ops path = %q, want %q", got, want) } // It is always exactly VersionPath + ".manifest.json" — a sibling, not a // new directory. if got, want := StagedManifestPath(opt, "acme", "v1", "myapp"), VersionPath(opt, "acme", "v1", "myapp")+".manifest.json"; got != want { t.Fatalf("sibling path = %q, want %q", got, want) } } // TestReadStagedManifestIsNilNilWhenAbsent pins the three-state contract: // absence is (nil, nil) — "unknown", not "none" — so a caller can tell a box // staged before this feature existed apart from a manifest that legitimately // declares no capabilities. func TestReadStagedManifestIsNilNilWhenAbsent(t *testing.T) { opt := t.TempDir() m, err := ReadStagedManifest(opt, OpsAccount, "v1", "myapp") if err != nil || m != nil { t.Fatalf("ReadStagedManifest on an absent sibling = %+v, %v; want nil, nil", m, err) } } // TestStageManifestRoundTripsCapabilities exercises the plain write/read // path a fresh ensureStaged download takes, including that a re-stage // OVERWRITES rather than appending (the reviewer's named concern). func TestStageManifestRoundTripsCapabilities(t *testing.T) { opt := t.TempDir() // StageManifest is always called once the version directory exists (via // StageArtifact, or because the binary is already staged in the // back-fill path) — it is a sibling writer, not a directory creator. if err := StageArtifact(opt, OpsAccount, "v1abc", "keymail", []byte("binary")); err != nil { t.Fatalf("StageArtifact: %v", err) } m := Manifest{ Schema: 1, App: "keymail", Version: "v1abc", Kind: "binary", Artifacts: map[string]Artifact{"linux-arm64": {File: "linux-arm64", SHA256: strings.Repeat("ab", 32), Size: 12}}, Capabilities: &caps.Set{V: 1, Command: "relay", Daemon: true, RuntimeDir: "keymail"}, } raw, err := json.MarshalIndent(&m, "", " ") if err != nil { t.Fatalf("marshal: %v", err) } if err := StageManifest(opt, OpsAccount, "v1abc", "keymail", raw); err != nil { t.Fatalf("StageManifest: %v", err) } got, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail") if err != nil { t.Fatalf("ReadStagedManifest: %v", err) } if got == nil || got.App != "keymail" || got.Version != "v1abc" { t.Fatalf("got = %+v, want App/Version to round-trip", got) } if got.Capabilities == nil || got.Capabilities.Command != "relay" || !got.Capabilities.Daemon || got.Capabilities.RuntimeDir != "keymail" { t.Fatalf("Capabilities = %+v, did not round-trip", got.Capabilities) } // Re-staging the SAME version overwrites, it does not append: a stale // half of an old sibling left dangling after the rename would otherwise // corrupt the JSON. m2 := m m2.Capabilities = &caps.Set{V: 1, Daemon: true} raw2, err := json.MarshalIndent(&m2, "", " ") if err != nil { t.Fatalf("marshal: %v", err) } if err := StageManifest(opt, OpsAccount, "v1abc", "keymail", raw2); err != nil { t.Fatalf("re-stage: %v", err) } got2, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail") if err != nil { t.Fatalf("ReadStagedManifest after re-stage: %v", err) } if got2.Capabilities.Command != "" || !got2.Capabilities.Daemon { t.Fatalf("re-stage did not overwrite: %+v", got2.Capabilities) } } // TestStageManifestCreatesTheAccountDir pins issue #58: a version staged by // a pre-capability binary has NO / subdirectory at all (the bare // no-account layout StageArtifact(optDir, "", ...) produces — exactly what // the old binary wrote). Back-filling a sibling for that version under a // real account must not assume the directory already exists; StageManifest // has to create it itself, the way StageArtifact already does for the // binary, or the write fails ENOENT on every affected box, forever (seen // live, hourly, on both production boxes). func TestStageManifestCreatesTheAccountDir(t *testing.T) { // Tighten the umask so the account dir StageManifest creates would come // out non-traversable (0700) if the Chmod fix didn't run — otherwise the // perm assertion below would pass even with the umask-masking bug back. old := syscall.Umask(0o077) defer syscall.Umask(old) opt := t.TempDir() // Pre-capability layout: versions//, no account level at all. if err := StageArtifact(opt, "", "v1abc", "myapp", []byte("binary")); err != nil { t.Fatalf("StageArtifact: %v", err) } if err := StageManifest(opt, "bad", "v1abc", "myapp", []byte("{}")); err != nil { t.Fatalf("StageManifest: %v", err) } p := StagedManifestPath(opt, "bad", "v1abc", "myapp") st, err := os.Stat(p) if err != nil { t.Fatalf("staged manifest: %v", err) } if st.Mode().Perm() != 0o644 { t.Fatalf("staged manifest mode = %v, want 0644", st.Mode().Perm()) } dirSt, err := os.Stat(filepath.Dir(p)) if err != nil { t.Fatalf("staged account dir: %v", err) } if dirSt.Mode().Perm() != 0o755 { t.Fatalf("staged account dir mode = %v, want 0755", dirSt.Mode().Perm()) } } // TestReadStagedManifestRunsTheValidationGate pins the Task-2-review // backstop: a stale sibling written by a NEWER binary — one that understood // a "v" this binary does not — must refuse cleanly here. This particular // "v" is caught by caps.Set's own DECODER (UnmarshalJSON's v-range check) // during ReadStagedManifest's json.Unmarshal, before validateManifest is // even reached — see TestReadStagedManifestRefusesASemanticallyInvalidSiblingEvenThoughItDecodes // below for the sibling that reaches validateManifest's caps.ValidCapabilities // call specifically. Written directly to disk (bypassing StageManifest's // own marshal), exactly as a newer binary's bytes would arrive. func TestReadStagedManifestRunsTheValidationGate(t *testing.T) { opt := t.TempDir() raw := []byte(`{ "schema": 1, "app": "keymail", "version": "v1abc", "kind": "binary", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } }, "capabilities": { "v": 2, "daemon": true } }`) if err := os.MkdirAll(filepath.Dir(StagedManifestPath(opt, OpsAccount, "v1abc", "keymail")), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(StagedManifestPath(opt, OpsAccount, "v1abc", "keymail"), raw, 0o644); err != nil { t.Fatal(err) } if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil { t.Fatal("a sibling declaring a v this binary does not speak was accepted") } } // TestReadStagedManifestRefusesASemanticallyInvalidSiblingEvenThoughItDecodes // is the mutation-killing companion to the test above (2026-08-03 review, // killing test 1): runtime_dir "systemd" decodes CLEANLY — RuntimeDir is a // plain string field with no charset or reserved-word check in caps.Set's // decoder — and is refused only by ValidCapabilities' reserved-name check, // which ReadStagedManifest reaches exclusively through its validateManifest // call. A mutant that deletes that call (while leaving json.Unmarshal in // place) would let this exact sibling through; this test is the one that // catches it. func TestReadStagedManifestRefusesASemanticallyInvalidSiblingEvenThoughItDecodes(t *testing.T) { opt := t.TempDir() raw := []byte(`{ "schema": 1, "app": "keymail", "version": "v1abc", "kind": "binary", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } }, "capabilities": { "v": 1, "runtime_dir": "systemd" } }`) p := StagedManifestPath(opt, OpsAccount, "v1abc", "keymail") if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(p, raw, 0o644); err != nil { t.Fatal(err) } if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil { t.Fatal("a sibling with a reserved runtime_dir (refused only by ValidCapabilities, via validateManifest) was accepted") } } // TestReadStagedManifestRefusesAnIdentityMismatch pins I3 (2026-08-03 // review): the sibling's OWN App/Version fields must match the path it was // read from, in both directions. Without this, a sibling at keymail's path // whose body claims a different app or version is trusted against the // PATH's identity — so a sibling written for (or by) the wrong app would // have its capabilities validated and materialized under keymail's name. func TestReadStagedManifestRefusesAnIdentityMismatch(t *testing.T) { opt := t.TempDir() write := func(bodyApp, bodyVer string) { t.Helper() raw := []byte(`{ "schema": 1, "app": "` + bodyApp + `", "version": "` + bodyVer + `", "kind": "binary", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } } }`) p := StagedManifestPath(opt, OpsAccount, "v1abc", "keymail") if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(p, raw, 0o644); err != nil { t.Fatal(err) } } // Wrong app: a sibling at keymail's path whose body claims "console". write("console", "v1abc") if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil { t.Fatal("a sibling naming a different app than its path was accepted") } // Wrong version: same app, a different version in the body. write("keymail", "vOTHER") if _, err := ReadStagedManifest(opt, OpsAccount, "v1abc", "keymail"); err == nil { t.Fatal("a sibling naming a different version than its path was accepted") } } // modStamp writes key and pins its mtime — candidate ranking reads nothing // else. func modStamp(t *testing.T, s *DirStore, dir, key string, mod time.Time) { t.Helper() if _, err := s.Put(context.Background(), key, []byte(`x`), ""); err != nil { t.Fatal(err) } if err := os.Chtimes(filepath.Join(dir, filepath.FromSlash(key)), mod, mod); err != nil { t.Fatal(err) } } // TestVersionCandidatesRanked: with a mod-aware store the candidates are the // newest max versions by each manifest object's own stamp — the mod order // here deliberately DISAGREES with version-name order, so a selection that // quietly ranked by name would fail — and a version whose ship never wrote a // manifest ranks by its newest artifact, so an aborted ship surfaces (as the // caller's error row) rather than vanishing. func TestVersionCandidatesRanked(t *testing.T) { dir := t.TempDir() s := NewDirStore(dir) base := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC) for v, mod := range map[string]time.Time{ "aa": base.Add(3 * time.Hour), // newest ship, first lexically too "zz": base.Add(2 * time.Hour), // second-newest despite sorting last by name "cc": base.Add(1 * time.Hour), "bb": base, // oldest — the one the cap must cut } { modStamp(t, s, dir, ManifestKey("bab", "kass", v), mod) // An artifact NEWER than its own manifest must not outrank the // manifest's stamp. modStamp(t, s, dir, ArtifactKey("bab", "kass", v, "linux-arm64"), mod.Add(30*time.Minute)) } // bb's ARTIFACT ends up newer than cc's MANIFEST: a ranking that took // the max over all of a version's objects instead of letting the // manifest's own stamp win would rank bb (100m) over cc (60m) and evict // cc from the want below — this line is what pins the rule. bbArtifact := filepath.Join(dir, filepath.FromSlash(ArtifactKey("bab", "kass", "bb", "linux-arm64"))) if err := os.Chtimes(bbArtifact, base.Add(100*time.Minute), base.Add(100*time.Minute)); err != nil { t.Fatal(err) } // A ship that died before manifest.json: artifact only, newest of all. modStamp(t, s, dir, ArtifactKey("bab", "kass", "dead1", "linux-arm64"), base.Add(4*time.Hour)) got, ranked, err := VersionCandidates(context.Background(), s, "bab", "kass", 4) if err != nil { t.Fatal(err) } if !ranked { t.Fatal("DirStore is a ModLister — candidates must come back ranked") } want := []string{"dead1", "aa", "zz", "cc"} if len(got) != len(want) { t.Fatalf("candidates = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("candidates = %v, want %v", got, want) } } } // plainStore hides ListMod behind the bare interface, the shape every test // fake has. type plainStore struct{ Store } // TestVersionCandidatesFallback: a store without ListMod answers every // version, unranked and uncapped, in listing order — the caller's // fetch-everything path, byte-identical to the pre-ModLister behavior. func TestVersionCandidatesFallback(t *testing.T) { dir := t.TempDir() s := NewDirStore(dir) base := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC) for i, v := range []string{"v1", "v2", "v3"} { modStamp(t, s, dir, ManifestKey("bab", "kass", v), base.Add(time.Duration(i)*time.Hour)) } got, ranked, err := VersionCandidates(context.Background(), plainStore{s}, "bab", "kass", 2) if err != nil { t.Fatal(err) } if ranked { t.Fatal("a bare Store cannot rank") } want := []string{"v1", "v2", "v3"} // listing order, all of them — no cap if len(got) != len(want) { t.Fatalf("candidates = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("candidates = %v, want %v", got, want) } } }