diff --git a/internal/serve/api/feed_history_extra_test.go b/internal/serve/api/feed_history_extra_test.go
new file mode 100644
index 0000000..7a78866
--- /dev/null
+++ b/internal/serve/api/feed_history_extra_test.go
@@ -0,0 +1,323 @@
+package api
+
+import (
+ "strconv"
+ "testing"
+ "time"
+)
+
+// TestExtractTS_RFC3339 covers the RFC3339 (seconds-precision) branch.
+func TestExtractTS_RFC3339(t *testing.T) {
+ want := time.Date(2026, 4, 21, 12, 0, 0, 0, time.UTC)
+ got := extractTS(map[string]any{
+ "ctm_timestamp": want.Format(time.RFC3339),
+ })
+ if !got.Equal(want) {
+ t.Errorf("extractTS RFC3339 = %v, want %v", got, want)
+ }
+}
+
+// TestExtractTS_RFC3339Nano covers the nano-precision fallback branch.
+func TestExtractTS_RFC3339Nano(t *testing.T) {
+ want := time.Date(2026, 4, 21, 12, 0, 0, 123456789, time.UTC)
+ got := extractTS(map[string]any{
+ "ctm_timestamp": want.Format(time.RFC3339Nano),
+ })
+ if !got.Equal(want) {
+ t.Errorf("extractTS RFC3339Nano = %v, want %v", got, want)
+ }
+}
+
+// TestExtractTS_Missing covers the "no ctm_timestamp" → zero time branch.
+func TestExtractTS_Missing(t *testing.T) {
+ got := extractTS(map[string]any{"other_field": "abc"})
+ if !got.IsZero() {
+ t.Errorf("extractTS missing = %v, want zero", got)
+ }
+}
+
+// TestExtractTS_WrongType covers the type-assertion-failure branch
+// (ctm_timestamp present but not a string).
+func TestExtractTS_WrongType(t *testing.T) {
+ got := extractTS(map[string]any{"ctm_timestamp": 12345})
+ if !got.IsZero() {
+ t.Errorf("extractTS wrong-type = %v, want zero", got)
+ }
+}
+
+// TestExtractTS_BadFormat covers the "string but unparseable" branch:
+// neither RFC3339 nor RFC3339Nano accepts → zero time.
+func TestExtractTS_BadFormat(t *testing.T) {
+ got := extractTS(map[string]any{"ctm_timestamp": "not-a-timestamp"})
+ if !got.IsZero() {
+ t.Errorf("extractTS bad-format = %v, want zero", got)
+ }
+}
+
+// TestNestedBool covers all branches of nestedBool: missing top key,
+// non-map intermediate, missing leaf, leaf-not-bool, leaf-true.
+func TestNestedBool(t *testing.T) {
+ m := map[string]any{
+ "a": map[string]any{
+ "b": map[string]any{
+ "isit": true,
+ },
+ "scalar": "x",
+ },
+ }
+
+ if !nestedBool(m, "a", "b", "isit") {
+ t.Errorf("nestedBool(a.b.isit) = false, want true")
+ }
+ // missing top key
+ if nestedBool(m, "missing", "x") {
+ t.Errorf("nestedBool(missing.x) = true, want false")
+ }
+ // intermediate is not a map
+ if nestedBool(m, "a", "scalar", "deeper") {
+ t.Errorf("nestedBool(a.scalar.deeper) = true, want false")
+ }
+ // leaf missing
+ if nestedBool(m, "a", "b", "missing") {
+ t.Errorf("nestedBool(a.b.missing) = true, want false")
+ }
+ // leaf present but wrong type
+ m2 := map[string]any{"flag": "true-as-string"}
+ if nestedBool(m2, "flag") {
+ t.Errorf("nestedBool wrong-type = true, want false")
+ }
+ // no path: returns whether root coerces to bool — root is map → false
+ if nestedBool(m) {
+ t.Errorf("nestedBool empty path on map = true, want false")
+ }
+}
+
+// TestSummariseHistoryInput_NoToolInput exercises the "tool_input
+// missing or wrong type" early return.
+func TestSummariseHistoryInput_NoToolInput(t *testing.T) {
+ if got := summariseHistoryInput(map[string]any{}, "Bash"); got != "" {
+ t.Errorf("summariseHistoryInput no input = %q, want \"\"", got)
+ }
+ if got := summariseHistoryInput(map[string]any{"tool_input": "not-a-map"}, "Bash"); got != "" {
+ t.Errorf("summariseHistoryInput wrong-type = %q, want \"\"", got)
+ }
+}
+
+// TestSummariseHistoryInput_KnownToolPath returns the well-known
+// primary input field via truncateToolInputField.
+func TestSummariseHistoryInput_KnownToolPath(t *testing.T) {
+ raw := map[string]any{
+ "tool_input": map[string]any{
+ "command": "echo hello",
+ },
+ }
+ if got := summariseHistoryInput(raw, "Bash"); got != "echo hello" {
+ t.Errorf("summariseHistoryInput Bash = %q, want \"echo hello\"", got)
+ }
+}
+
+// TestSummariseHistoryInput_FallbackJSON exercises the json.Marshal
+// fallback when the tool isn't well-known.
+func TestSummariseHistoryInput_FallbackJSON(t *testing.T) {
+ raw := map[string]any{
+ "tool_input": map[string]any{
+ "foo": "bar",
+ },
+ }
+ got := summariseHistoryInput(raw, "UnknownTool")
+ // Marshaled JSON should round-trip back something containing the key.
+ if got == "" {
+ t.Errorf("summariseHistoryInput fallback = \"\", want non-empty JSON")
+ }
+}
+
+// TestSummariseHistoryResponse covers each switch arm of the response
+// summariser: missing, string, map.output, map.is_error+error,
+// map.is_error+no-error, map with arbitrary keys, empty map, and the
+// "wrong type" default-fall-through.
+func TestSummariseHistoryResponse(t *testing.T) {
+ t.Run("missing key", func(t *testing.T) {
+ if got := summariseHistoryResponse(map[string]any{}); got != "" {
+ t.Errorf("missing = %q, want \"\"", got)
+ }
+ })
+ t.Run("string response", func(t *testing.T) {
+ raw := map[string]any{"tool_response": "ok"}
+ if got := summariseHistoryResponse(raw); got != "ok" {
+ t.Errorf("string = %q, want \"ok\"", got)
+ }
+ })
+ t.Run("string response truncated", func(t *testing.T) {
+ long := make([]byte, historyInputMax+50)
+ for i := range long {
+ long[i] = 'x'
+ }
+ raw := map[string]any{"tool_response": string(long)}
+ got := summariseHistoryResponse(raw)
+ if len(got) == 0 || len(got) > historyInputMax {
+ t.Errorf("string truncated len=%d, want <= %d and > 0", len(got), historyInputMax)
+ }
+ })
+ t.Run("map output single line", func(t *testing.T) {
+ raw := map[string]any{"tool_response": map[string]any{"output": "hello"}}
+ if got := summariseHistoryResponse(raw); got != "hello" {
+ t.Errorf("map.output single-line = %q, want \"hello\"", got)
+ }
+ })
+ t.Run("map output multi-line takes first line", func(t *testing.T) {
+ raw := map[string]any{"tool_response": map[string]any{"output": "first\nsecond\nthird"}}
+ if got := summariseHistoryResponse(raw); got != "first" {
+ t.Errorf("map.output multiline = %q, want \"first\"", got)
+ }
+ })
+ t.Run("map is_error with message", func(t *testing.T) {
+ raw := map[string]any{
+ "tool_response": map[string]any{
+ "is_error": true,
+ "error": "boom",
+ },
+ }
+ if got := summariseHistoryResponse(raw); got != "boom" {
+ t.Errorf("map is_error+error = %q, want \"boom\"", got)
+ }
+ })
+ t.Run("map is_error with no message", func(t *testing.T) {
+ raw := map[string]any{
+ "tool_response": map[string]any{
+ "is_error": true,
+ },
+ }
+ if got := summariseHistoryResponse(raw); got != "error" {
+ t.Errorf("map is_error+no-error = %q, want \"error\"", got)
+ }
+ })
+ t.Run("map empty falls through to keys empty", func(t *testing.T) {
+ raw := map[string]any{"tool_response": map[string]any{}}
+ if got := summariseHistoryResponse(raw); got != "" {
+ t.Errorf("empty map = %q, want \"\"", got)
+ }
+ })
+ t.Run("map arbitrary keys → bracketed list", func(t *testing.T) {
+ raw := map[string]any{
+ "tool_response": map[string]any{
+ "foo": "x",
+ "bar": "y",
+ },
+ }
+ got := summariseHistoryResponse(raw)
+ // Map iteration order is random, but the wrapper format is
+ // stable: starts with "[" and ends with "]".
+ if len(got) < 2 || got[0] != '[' || got[len(got)-1] != ']' {
+ t.Errorf("arbitrary keys = %q, want bracketed list", got)
+ }
+ })
+ t.Run("unsupported response type", func(t *testing.T) {
+ raw := map[string]any{"tool_response": 42}
+ if got := summariseHistoryResponse(raw); got != "" {
+ t.Errorf("unsupported = %q, want \"\"", got)
+ }
+ })
+}
+
+// TestTruncateHistory covers the trim-and-truncate helper directly.
+func TestTruncateHistory(t *testing.T) {
+ if got := truncateHistory(" hello "); got != "hello" {
+ t.Errorf("trim only = %q, want \"hello\"", got)
+ }
+ short := "abcdef"
+ if got := truncateHistory(short); got != "abcdef" {
+ t.Errorf("short pass-through = %q, want %q", got, short)
+ }
+ long := make([]byte, historyInputMax+10)
+ for i := range long {
+ long[i] = 'x'
+ }
+ got := truncateHistory(string(long))
+ if len(got) != historyInputMax {
+ t.Errorf("truncated len = %d, want %d", len(got), historyInputMax)
+ }
+}
+
+// TestSplitIDExt and TestIDLessThanExt exercise the cursor-id parser
+// and comparator end-to-end including malformed inputs.
+func TestSplitIDExt(t *testing.T) {
+ cases := []struct {
+ id string
+ wantNS int64
+ wantSeq uint64
+ }{
+ {"1700000000-3", 1700000000, 3},
+ {"42-0", 42, 0},
+ {"", 0, 0}, // no '-' → zeroes
+ {"not-a-cursor", 0, 0}, // first segment unparseable, but '-' found
+ {"123-notnum", 123, 0}, // seq unparseable
+ }
+ for _, c := range cases {
+ ns, seq := splitIDExt(c.id)
+ if ns != c.wantNS || seq != c.wantSeq {
+ t.Errorf("splitIDExt(%q) = (%d, %d), want (%d, %d)",
+ c.id, ns, seq, c.wantNS, c.wantSeq)
+ }
+ }
+}
+
+func TestIDLessThanExt(t *testing.T) {
+ // older nano → less.
+ if !idLessThanExt("100-0", "200-0") {
+ t.Error("100-0 < 200-0 should be true")
+ }
+ // equal nano → seq decides.
+ if !idLessThanExt("100-0", "100-1") {
+ t.Error("100-0 < 100-1 should be true")
+ }
+ if idLessThanExt("200-0", "100-9") {
+ t.Error("200-0 < 100-9 should be false")
+ }
+ // equal ids → not less.
+ if idLessThanExt("100-1", "100-1") {
+ t.Error("equal ids should not be less")
+ }
+}
+
+// TestSynthEvent_BadJSON covers synthEvent's "json.Unmarshal failed"
+// branch (returns ok=false).
+func TestSynthEvent_BadJSON(t *testing.T) {
+ if _, ok := synthEvent("alpha", []byte("not-json")); ok {
+ t.Error("synthEvent should return false on invalid JSON")
+ }
+}
+
+// TestSynthEvent_NoToolName covers the "tool_name missing" early return.
+func TestSynthEvent_NoToolName(t *testing.T) {
+ line := []byte(`{"foo":"bar"}`)
+ if _, ok := synthEvent("alpha", line); ok {
+ t.Error("synthEvent should return false when tool_name is missing")
+ }
+}
+
+// TestSynthEvent_Happy verifies the synthesised envelope: id is
+// derived from ctm_timestamp, type is tool_call, payload contains the
+// session+tool.
+func TestSynthEvent_Happy(t *testing.T) {
+ ts := time.Date(2026, 4, 21, 12, 0, 0, 0, time.UTC)
+ line := []byte(`{
+ "tool_name":"Bash",
+ "tool_input":{"command":"echo hi"},
+ "tool_response":{"output":"hi","is_error":false},
+ "ctm_timestamp":"` + ts.Format(time.RFC3339) + `"
+ }`)
+ ev, ok := synthEvent("alpha", line)
+ if !ok {
+ t.Fatal("synthEvent returned false on valid line")
+ }
+ if ev.Session != "alpha" {
+ t.Errorf("Session = %q, want alpha", ev.Session)
+ }
+ if ev.Type != "tool_call" {
+ t.Errorf("Type = %q, want tool_call", ev.Type)
+ }
+ wantID := strconv.FormatInt(ts.UnixNano(), 10) + "-0"
+ if ev.ID != wantID {
+ t.Errorf("ID = %q, want %q", ev.ID, wantID)
+ }
+}
diff --git a/internal/serve/server_more_test.go b/internal/serve/server_more_test.go
new file mode 100644
index 0000000..8541e5a
--- /dev/null
+++ b/internal/serve/server_more_test.go
@@ -0,0 +1,392 @@
+package serve
+
+import (
+ "errors"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/RandomCodeSpace/ctm/internal/serve/api"
+ "github.com/RandomCodeSpace/ctm/internal/serve/ingest"
+ "github.com/RandomCodeSpace/ctm/internal/serve/store"
+ "github.com/RandomCodeSpace/ctm/internal/session"
+)
+
+// fakeCostStore is a tiny store.CostStore stub whose Range/Totals
+// return a fixed error. Used to drive costSourceAdapter's err
+// branches. Insert/Close are no-ops for the few tests that touch
+// them (none here, but they're satisfied by interface).
+type fakeCostStore struct {
+ rangeErr error
+ totalsErr error
+ rangePts []store.Point
+ totals store.Totals
+}
+
+func (f *fakeCostStore) Insert([]store.Point) error { return nil }
+func (f *fakeCostStore) Range(session string, since, until time.Time) ([]store.Point, error) {
+ if f.rangeErr != nil {
+ return nil, f.rangeErr
+ }
+ return f.rangePts, nil
+}
+func (f *fakeCostStore) Totals(since time.Time) (store.Totals, error) {
+ if f.totalsErr != nil {
+ return store.Totals{}, f.totalsErr
+ }
+ return f.totals, nil
+}
+func (f *fakeCostStore) Close() error { return nil }
+
+// TestCostSourceAdapter_RangeError covers the "Range returns err"
+// fast-fail branch in costSourceAdapter.Range.
+func TestCostSourceAdapter_RangeError(t *testing.T) {
+ wantErr := errors.New("boom")
+ a := costSourceAdapter{s: &fakeCostStore{rangeErr: wantErr}}
+ pts, err := a.Range("alpha", time.Now().Add(-time.Hour), time.Now())
+ if err == nil || !errors.Is(err, wantErr) {
+ t.Errorf("Range err = %v, want %v", err, wantErr)
+ }
+ if pts != nil {
+ t.Errorf("Range pts = %v, want nil", pts)
+ }
+}
+
+// TestCostSourceAdapter_TotalsError covers the "Totals returns err"
+// fast-fail branch.
+func TestCostSourceAdapter_TotalsError(t *testing.T) {
+ wantErr := errors.New("boom")
+ a := costSourceAdapter{s: &fakeCostStore{totalsErr: wantErr}}
+ got, err := a.Totals(time.Now().Add(-time.Hour))
+ if err == nil || !errors.Is(err, wantErr) {
+ t.Errorf("Totals err = %v, want %v", err, wantErr)
+ }
+ if got != (api.CostTotals{}) {
+ t.Errorf("Totals = %+v, want zero", got)
+ }
+}
+
+// TestLogsUUIDResolver_NilProjOrEmptyArgs covers the early-return
+// guards on both Resolve methods.
+func TestLogsUUIDResolver_NilProjOrEmptyArgs(t *testing.T) {
+ // nil projection.
+ r := logsUUIDResolver{proj: nil}
+ if _, ok := r.ResolveName("alpha"); ok {
+ t.Errorf("ResolveName(nil proj) should return false")
+ }
+ if _, ok := r.ResolveUUID("uuid"); ok {
+ t.Errorf("ResolveUUID(nil proj) should return false")
+ }
+
+ // empty projection but non-empty proj — empty arg short-circuits.
+ dir := t.TempDir()
+ path := filepath.Join(dir, "sessions.json")
+ writeSessionsJSON(t, path)
+ proj := ingest.New(path, &fakeTmuxClient{})
+ proj.Reload()
+ r2 := logsUUIDResolver{proj: proj}
+ if _, ok := r2.ResolveName(""); ok {
+ t.Errorf("ResolveName(\"\") should return false")
+ }
+ if _, ok := r2.ResolveUUID(""); ok {
+ t.Errorf("ResolveUUID(\"\") should return false")
+ }
+}
+
+// TestLogsUUIDResolver_ResolveName_NoUUIDStored covers the
+// "session exists but UUID empty" branch.
+func TestLogsUUIDResolver_ResolveName_NoUUIDStored(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "sessions.json")
+ // Session has no UUID set.
+ writeSessionsJSON(t, path, &session.Session{Name: "alpha", Mode: "safe"})
+ proj := ingest.New(path, &fakeTmuxClient{})
+ proj.Reload()
+
+ r := logsUUIDResolver{proj: proj}
+ if _, ok := r.ResolveName("alpha"); ok {
+ t.Errorf("ResolveName for empty-UUID session should return false")
+ }
+}
+
+// TestLogsUUIDResolver_ResolveUUID_WorkdirFallbackHit covers the
+// production workdir-derived fallback: when the requested uuid isn't
+// in the projection's direct uuid map, ResolveUUID globs
+// ~/.claude/projects/
/.jsonl, derives the session from the
+// dirname (slashes → dashes), and returns its name.
+func TestLogsUUIDResolver_ResolveUUID_WorkdirFallbackHit(t *testing.T) {
+ // Sandbox HOME so UserHomeDir + Glob hit our fake tree only.
+ homeDir := t.TempDir()
+ t.Setenv("HOME", homeDir)
+ // On Linux UserHomeDir consults $HOME, on macOS too; this is enough
+ // for CI and dev. Skip if UserHomeDir surprises us.
+ if got, err := os.UserHomeDir(); err != nil || got != homeDir {
+ t.Skipf("UserHomeDir didn't honour HOME override: got=%q err=%v", got, err)
+ }
+
+ // Real workdir for the session.
+ workdir := "/srv/projects/codeiq"
+ // Claude projects layout: ~/.claude/projects//.jsonl
+ dirName := strings.ReplaceAll(workdir, "/", "-")
+ projDir := filepath.Join(homeDir, ".claude", "projects", dirName)
+ if err := os.MkdirAll(projDir, 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ const uuid = "ffffffff-0000-0000-0000-000000000001"
+ if err := os.WriteFile(filepath.Join(projDir, uuid+".jsonl"), []byte{}, 0o600); err != nil {
+ t.Fatalf("write jsonl: %v", err)
+ }
+
+ // Projection has the session WITHOUT this uuid (e.g. session
+ // rotated to a fresh claude session_id but the old transcript
+ // still exists on disk). The direct loop misses, the workdir
+ // fallback should still resolve to "codeiq" via the dirName.
+ dir := t.TempDir()
+ sessionsPath := filepath.Join(dir, "sessions.json")
+ writeSessionsJSON(t, sessionsPath, &session.Session{
+ Name: "codeiq",
+ UUID: "current-uuid-different",
+ Workdir: workdir,
+ })
+ proj := ingest.New(sessionsPath, &fakeTmuxClient{})
+ proj.Reload()
+
+ r := logsUUIDResolver{proj: proj}
+ got, ok := r.ResolveUUID(uuid)
+ if !ok {
+ t.Fatalf("ResolveUUID(orphan uuid via workdir fallback) returned false")
+ }
+ if got != "codeiq" {
+ t.Errorf("ResolveUUID = %q, want codeiq", got)
+ }
+}
+
+// TestLogsUUIDResolver_ResolveUUID_FallbackHitButNoSessionMatch covers
+// the case where the glob matches a single file but no projection
+// session has a workdir whose dashed form equals the dirname → false.
+func TestLogsUUIDResolver_ResolveUUID_FallbackHitButNoSessionMatch(t *testing.T) {
+ homeDir := t.TempDir()
+ t.Setenv("HOME", homeDir)
+ if got, err := os.UserHomeDir(); err != nil || got != homeDir {
+ t.Skipf("UserHomeDir didn't honour HOME override: got=%q err=%v", got, err)
+ }
+
+ dirName := "-srv-projects-codeiq"
+ projDir := filepath.Join(homeDir, ".claude", "projects", dirName)
+ if err := os.MkdirAll(projDir, 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ const uuid = "fffffffe-0000-0000-0000-000000000001"
+ if err := os.WriteFile(filepath.Join(projDir, uuid+".jsonl"), []byte{}, 0o600); err != nil {
+ t.Fatalf("write jsonl: %v", err)
+ }
+
+ // Projection has a session but its workdir doesn't match the dashed
+ // form ("/totally/different").
+ dir := t.TempDir()
+ sessionsPath := filepath.Join(dir, "sessions.json")
+ writeSessionsJSON(t, sessionsPath, &session.Session{
+ Name: "alpha",
+ UUID: "u-alpha",
+ Workdir: "/totally/different",
+ })
+ proj := ingest.New(sessionsPath, &fakeTmuxClient{})
+ proj.Reload()
+
+ r := logsUUIDResolver{proj: proj}
+ if got, ok := r.ResolveUUID(uuid); ok {
+ t.Errorf("ResolveUUID = (%q, true), want false (no workdir match)", got)
+ }
+}
+
+// TestSessionSourceAdapter_LastCheckpointAt_RealRepo covers the
+// "cps[0].TS parses cleanly → return non-zero time" success branch
+// of LastCheckpointAt. Requires git in PATH; skipped if absent.
+func TestSessionSourceAdapter_LastCheckpointAt_RealRepo(t *testing.T) {
+ if _, err := exec.LookPath("git"); err != nil {
+ t.Skipf("git not in PATH: %v", err)
+ }
+
+ // Build a real git repo with one checkpoint commit so
+ // CheckpointsCache.Get returns a populated slice with an RFC3339 TS.
+ repoDir := t.TempDir()
+ gitInit(t, repoDir)
+ if err := os.WriteFile(filepath.Join(repoDir, "README.md"), []byte("hi\n"), 0o644); err != nil {
+ t.Fatalf("write file: %v", err)
+ }
+ gitRun(t, repoDir, "add", ".")
+ gitRun(t, repoDir, "commit", "-m", "checkpoint: pre-yolo 2026-04-21T12:00:00")
+
+ // Wire a projection with this workdir and a session source adapter.
+ sessionsPath := filepath.Join(t.TempDir(), "sessions.json")
+ writeSessionsJSON(t, sessionsPath, &session.Session{
+ Name: "alpha", UUID: "u-alpha", Workdir: repoDir,
+ })
+ proj := ingest.New(sessionsPath, &fakeTmuxClient{})
+ proj.Reload()
+
+ a := sessionSourceAdapter{proj: proj, cpCache: api.NewCheckpointsCache()}
+ ts, ok := a.LastCheckpointAt("alpha")
+ if !ok {
+ t.Fatalf("LastCheckpointAt returned ok=false on real checkpointed repo")
+ }
+ if ts.IsZero() {
+ t.Errorf("LastCheckpointAt ts is zero, want non-zero")
+ }
+}
+
+// gitInit / gitRun are minimal shell-out helpers for the repo fixture.
+// They mirror the helpers in internal/serve/git/checkpoints_test.go.
+func gitInit(t *testing.T, dir string) {
+ t.Helper()
+ gitRun(t, dir, "init", "-q")
+ gitRun(t, dir, "config", "user.email", "test@example.com")
+ gitRun(t, dir, "config", "user.name", "Test")
+ // Some hosts default to gpgsign=true; force off so commits don't
+ // hang waiting for a signing agent we don't have.
+ gitRun(t, dir, "config", "commit.gpgsign", "false")
+}
+
+// TestNew_DefaultsAllUnset covers the four `if path == ""` branches in
+// New() that fall through to config.SessionsPath / TmuxConfPath /
+// Dir()/logs / /tmp/ctm-statusline. Sandboxes HOME so the cost db is
+// written into a temp tree rather than the dev's real ~/.config/ctm.
+func TestNew_DefaultsAllUnset(t *testing.T) {
+ homeDir := t.TempDir()
+ t.Setenv("HOME", homeDir)
+ if got, err := os.UserHomeDir(); err != nil || got != homeDir {
+ t.Skipf("UserHomeDir didn't honour HOME override: got=%q err=%v", got, err)
+ }
+ // config.Dir uses UserHomeDir → ~/.config/ctm. Pre-create so
+ // OpenCostStore doesn't trip on the parent.
+ if err := os.MkdirAll(filepath.Join(homeDir, ".config", "ctm"), 0o755); err != nil {
+ t.Fatalf("mkdir config dir: %v", err)
+ }
+
+ port := pickFreePort(t)
+ srv, err := New(Options{
+ Port: port,
+ Version: "vDef",
+ Token: testToken,
+ // Intentionally leave SessionsPath / TmuxConfPath / LogDir /
+ // StatuslineDumpDir empty — the empty-path branches inside
+ // New() should fall through to config defaults.
+ })
+ if err != nil {
+ t.Fatalf("New defaults: %v", err)
+ }
+ t.Cleanup(func() { _ = srv.listener.Close(); _ = srv.cost.Close() })
+
+ if !strings.HasPrefix(srv.logDir, homeDir) {
+ t.Errorf("logDir = %q, want under %q", srv.logDir, homeDir)
+ }
+}
+
+// TestNew_PortInUseByNonCtm covers the "port bound by foreign listener"
+// branch (lines 170-176). We bind a vanilla TCP listener first, then
+// call New() against the same port — it should fail with the bind
+// error wrapped, NOT ErrAlreadyRunning (probeIsCtmServe sees a non-ctm
+// listener).
+func TestNew_PortInUseByNonCtm(t *testing.T) {
+ port := pickFreePort(t)
+ // Bind a TCP listener that doesn't speak ctm-serve's healthz.
+ ln, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port))
+ if err != nil {
+ t.Fatalf("bind decoy: %v", err)
+ }
+ t.Cleanup(func() { _ = ln.Close() })
+
+ _, err = New(Options{
+ Port: port,
+ Version: "vClash",
+ Token: testToken,
+ SessionsPath: filepath.Join(t.TempDir(), "sessions.json"),
+ TmuxConfPath: filepath.Join(t.TempDir(), "tmux.conf"),
+ LogDir: filepath.Join(t.TempDir(), "logs"),
+ StatuslineDumpDir: filepath.Join(t.TempDir(), "statusline"),
+ })
+ if err == nil {
+ t.Fatal("New on busy port returned nil error, want bind failure")
+ }
+ if errors.Is(err, ErrAlreadyRunning) {
+ t.Errorf("err = ErrAlreadyRunning; want non-ctm bind-failure wrap. got=%v", err)
+ }
+}
+
+// TestEventsSessionRoute exercises the GET /events/session/{name}
+// closure registered in registerRoutes — the line counted at 779. We
+// can't keep the SSE connection open for long, but a 200 + initial
+// retry frame is enough to record coverage on the route.
+func TestEventsSessionRoute(t *testing.T) {
+ port := pickFreePort(t)
+ srv, err := New(Options{
+ Port: port,
+ Version: "vSSE",
+ Token: testToken,
+ SessionsPath: filepath.Join(t.TempDir(), "sessions.json"),
+ TmuxConfPath: filepath.Join(t.TempDir(), "tmux.conf"),
+ LogDir: filepath.Join(t.TempDir(), "logs"),
+ StatuslineDumpDir: filepath.Join(t.TempDir(), "statusline"),
+ })
+ if err != nil {
+ t.Fatalf("New: %v", err)
+ }
+ t.Cleanup(func() { _ = srv.listener.Close(); _ = srv.cost.Close() })
+
+ mux := http.NewServeMux()
+ srv.registerRoutes(mux)
+
+ // httptest.NewRecorder doesn't support Flush detection, but for SSE
+ // we just want the handler entered and response headers written.
+ // Use a real test server so the underlying Hijack/Flush works.
+ ts := httptest.NewServer(mux)
+ t.Cleanup(ts.Close)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/events/session/alpha", nil)
+ if err != nil {
+ t.Fatalf("NewRequest: %v", err)
+ }
+ req.Header.Set("Authorization", "Bearer "+testToken)
+
+ // Use a client with an aggressive timeout so the long-lived SSE
+ // loop returns control to the test promptly.
+ client := &http.Client{Timeout: 200 * time.Millisecond}
+ resp, err := client.Do(req)
+ if err != nil {
+ // A timeout-on-read is fine — the route was hit. Surface only
+ // non-timeout failures.
+ if !strings.Contains(err.Error(), "Timeout") &&
+ !strings.Contains(err.Error(), "deadline exceeded") &&
+ !strings.Contains(err.Error(), "context canceled") {
+ t.Fatalf("Do: %v", err)
+ }
+ return
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("status = %d, want 200", resp.StatusCode)
+ }
+}
+
+func gitRun(t *testing.T, dir string, args ...string) {
+ t.Helper()
+ cmd := exec.Command("git", args...)
+ cmd.Dir = dir
+ // Force a fully self-contained env so user-level git config (eg.
+ // commit signing, hooks) can't leak in from the dev's machine.
+ cmd.Env = append(os.Environ(),
+ "GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com",
+ "GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com",
+ "GIT_CONFIG_GLOBAL=/dev/null",
+ )
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("git %v: %v\n%s", args, err, out)
+ }
+}
diff --git a/internal/session/state_extra_test.go b/internal/session/state_extra_test.go
new file mode 100644
index 0000000..5778c56
--- /dev/null
+++ b/internal/session/state_extra_test.go
@@ -0,0 +1,242 @@
+package session_test
+
+import (
+ "os"
+ "path/filepath"
+ "sort"
+ "testing"
+ "time"
+
+ "github.com/RandomCodeSpace/ctm/internal/session"
+)
+
+// TestStoreUpdateHealth covers the happy path: the session's
+// LastHealthStatus + LastHealthAt are stamped, persisted, and visible
+// via Get on the next call.
+func TestStoreUpdateHealth(t *testing.T) {
+ st := newStore(t)
+ if err := st.Save(session.New("alpha", "/work", "safe")); err != nil {
+ t.Fatalf("Save: %v", err)
+ }
+ before := time.Now().UTC().Add(-time.Second)
+ if err := st.UpdateHealth("alpha", "ok"); err != nil {
+ t.Fatalf("UpdateHealth: %v", err)
+ }
+ got, err := st.Get("alpha")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if got.LastHealthStatus != "ok" {
+ t.Errorf("LastHealthStatus = %q, want ok", got.LastHealthStatus)
+ }
+ if got.LastHealthAt.Before(before) {
+ t.Errorf("LastHealthAt = %v, want >= %v", got.LastHealthAt, before)
+ }
+ // Subsequent update overwrites.
+ if err := st.UpdateHealth("alpha", "degraded"); err != nil {
+ t.Fatalf("UpdateHealth degraded: %v", err)
+ }
+ got2, _ := st.Get("alpha")
+ if got2.LastHealthStatus != "degraded" {
+ t.Errorf("LastHealthStatus after second update = %q, want degraded", got2.LastHealthStatus)
+ }
+ if !got2.LastHealthAt.After(got.LastHealthAt) && !got2.LastHealthAt.Equal(got.LastHealthAt) {
+ // Within the same wall-clock tick they may compare equal; only
+ // fail if we somehow went backwards.
+ t.Errorf("LastHealthAt regressed: %v < %v", got2.LastHealthAt, got.LastHealthAt)
+ }
+}
+
+// TestStoreUpdateHealth_Missing exercises the "session not found" branch.
+func TestStoreUpdateHealth_Missing(t *testing.T) {
+ st := newStore(t)
+ if err := st.UpdateHealth("ghost", "ok"); err == nil {
+ t.Error("expected error updating health on missing session")
+ }
+}
+
+// TestStoreUpdateAttached covers the happy path: LastAttachedAt is
+// stamped to a non-zero, recent UTC timestamp.
+func TestStoreUpdateAttached(t *testing.T) {
+ st := newStore(t)
+ if err := st.Save(session.New("alpha", "/work", "safe")); err != nil {
+ t.Fatalf("Save: %v", err)
+ }
+ before := time.Now().UTC().Add(-time.Second)
+ if err := st.UpdateAttached("alpha"); err != nil {
+ t.Fatalf("UpdateAttached: %v", err)
+ }
+ got, err := st.Get("alpha")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if got.LastAttachedAt.IsZero() {
+ t.Error("LastAttachedAt is zero, want non-zero after UpdateAttached")
+ }
+ if got.LastAttachedAt.Before(before) {
+ t.Errorf("LastAttachedAt = %v, want >= %v", got.LastAttachedAt, before)
+ }
+}
+
+// TestStoreUpdateAttached_Missing exercises the "session not found" branch.
+func TestStoreUpdateAttached_Missing(t *testing.T) {
+ st := newStore(t)
+ if err := st.UpdateAttached("ghost"); err == nil {
+ t.Error("expected error updating attached on missing session")
+ }
+}
+
+// TestStoreUpdateMode_Missing covers the not-found branch in UpdateMode
+// (UpdateMode happy path is already covered by TestStoreUpdateMode).
+func TestStoreUpdateMode_Missing(t *testing.T) {
+ st := newStore(t)
+ if err := st.UpdateMode("ghost", "yolo"); err == nil {
+ t.Error("expected error updating mode on missing session")
+ }
+}
+
+// TestStoreNames_Empty + Populated covers the Names() exposure used by
+// shell completions.
+func TestStoreNames(t *testing.T) {
+ st := newStore(t)
+
+ // Empty store first.
+ got, err := st.Names()
+ if err != nil {
+ t.Fatalf("Names empty: %v", err)
+ }
+ if len(got) != 0 {
+ t.Errorf("Names empty = %v, want []", got)
+ }
+
+ // Populate.
+ st.Save(session.New("alpha", "/work", "safe"))
+ st.Save(session.New("beta", "/work", "safe"))
+ st.Save(session.New("gamma", "/work", "yolo"))
+
+ got, err = st.Names()
+ if err != nil {
+ t.Fatalf("Names: %v", err)
+ }
+ sort.Strings(got)
+ want := []string{"alpha", "beta", "gamma"}
+ if len(got) != len(want) {
+ t.Fatalf("Names len = %d, want %d (%v)", len(got), len(want), got)
+ }
+ for i, n := range want {
+ if got[i] != n {
+ t.Errorf("Names[%d] = %q, want %q", i, got[i], n)
+ }
+ }
+}
+
+// TestStoreDelete_Missing covers Delete's "session not found" branch.
+func TestStoreDelete_Missing(t *testing.T) {
+ st := newStore(t)
+ if err := st.Delete("ghost"); err == nil {
+ t.Error("expected error deleting missing session")
+ }
+}
+
+// TestStoreRename_Missing covers Rename's "session not found" branch.
+func TestStoreRename_Missing(t *testing.T) {
+ st := newStore(t)
+ if err := st.Rename("ghost", "newname"); err == nil {
+ t.Error("expected error renaming missing session")
+ }
+}
+
+// TestStoreRename_Conflict covers the "newName already exists" branch.
+func TestStoreRename_Conflict(t *testing.T) {
+ st := newStore(t)
+ st.Save(session.New("alpha", "/work", "safe"))
+ st.Save(session.New("beta", "/work", "safe"))
+ if err := st.Rename("alpha", "beta"); err == nil {
+ t.Error("expected error renaming over existing session")
+ }
+ // Both should still exist.
+ if _, err := st.Get("alpha"); err != nil {
+ t.Errorf("alpha should still exist after failed rename: %v", err)
+ }
+ if _, err := st.Get("beta"); err != nil {
+ t.Errorf("beta should still exist after failed rename: %v", err)
+ }
+}
+
+// TestStoreRename_InvalidNewName covers the ValidateName failure branch.
+func TestStoreRename_InvalidNewName(t *testing.T) {
+ st := newStore(t)
+ st.Save(session.New("alpha", "/work", "safe"))
+ if err := st.Rename("alpha", "bad/name"); err == nil {
+ t.Error("expected error renaming to invalid name")
+ }
+}
+
+// TestStoreBackup_NoFile covers backupLocked's os.IsNotExist branch:
+// before any Save() the sessions.json doesn't exist, Backup() returns
+// ("", nil) — used by DeleteAll on a fresh install to skip backing up.
+func TestStoreBackup_NoFile(t *testing.T) {
+ st := newStore(t)
+ path, err := st.Backup()
+ if err != nil {
+ t.Fatalf("Backup on empty store: %v", err)
+ }
+ if path != "" {
+ t.Errorf("Backup empty store = %q, want \"\"", path)
+ }
+}
+
+// TestStoreDeleteAll_NoBackupOnEmpty covers DeleteAll when there's no
+// existing sessions file: backupLocked returns ("", nil) and DeleteAll
+// proceeds to write the empty state. (Hits the success path of the
+// `if backupPath, err := ...; err == nil && backupPath != ""` guard.)
+func TestStoreDeleteAll_NoBackupOnEmpty(t *testing.T) {
+ st := newStore(t)
+ if err := st.DeleteAll(); err != nil {
+ t.Fatalf("DeleteAll on empty store: %v", err)
+ }
+ // Result should still be a usable empty store.
+ list, err := st.List()
+ if err != nil {
+ t.Fatalf("List after DeleteAll: %v", err)
+ }
+ if len(list) != 0 {
+ t.Errorf("expected 0 sessions after DeleteAll on empty store, got %d", len(list))
+ }
+}
+
+// TestStoreDeleteAll_BacksUpExisting confirms DeleteAll creates a
+// .bak. file when sessions.json existed prior. This makes
+// the backupLocked-success branch in DeleteAll observable.
+func TestStoreDeleteAll_BacksUpExisting(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "sessions.json")
+ st := session.NewStore(path)
+
+ if err := st.Save(session.New("alpha", "/work", "safe")); err != nil {
+ t.Fatalf("Save: %v", err)
+ }
+ if err := st.DeleteAll(); err != nil {
+ t.Fatalf("DeleteAll: %v", err)
+ }
+
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ found := false
+ for _, e := range entries {
+ if filepath.Ext(e.Name()) == "" {
+ continue
+ }
+ // Look for a sibling with .bak. in its name.
+ if name := e.Name(); len(name) > len("sessions.json.bak.") &&
+ name[:len("sessions.json.bak.")] == "sessions.json.bak." {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("expected DeleteAll to leave a sessions.json.bak. file")
+ }
+}
diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx
new file mode 100644
index 0000000..6645cae
--- /dev/null
+++ b/ui/src/App.test.tsx
@@ -0,0 +1,348 @@
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ vi,
+} from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { ReactNode } from "react";
+import { TOKEN_KEY } from "@/lib/api";
+
+/*
+ * App.test.tsx — integration test of the top-level shell.
+ *
+ * App.tsx wires the global providers (Theme -> design-system bridge ->
+ * QueryClient -> Auth -> Sse -> AuthGate) and a `createBrowserRouter`.
+ * For tests we:
+ *
+ * 1. drive route resolution through `window.history.pushState` rather
+ * than a MemoryRouter — RouterProvider takes a router instance, so
+ * we have to use the real (browser) history API the createBrowserRouter
+ * reads from. jsdom implements it.
+ * 2. mock the heavy children (Dashboard, DoctorPanel, FeedFullscreen,
+ * SessionDetail) — those have their own dedicated suites and pull in
+ * SSE/network on mount, which would explode in this shell-level test.
+ * 3. mock SseProvider so we don't actually open EventSource / use
+ * fetch-event-source. We expose a fake `useSseStatus` so
+ * ConnectionBanner can read the connected flag the same way it
+ * does in production.
+ * 4. stub `globalThis.fetch` for /api/auth/status so AuthGate resolves
+ * either to (unauthenticated) or to
+ * (authenticated). All other endpoints return 404 — the route
+ * stubs never call them.
+ */
+
+vi.mock("@/components/SseProvider", () => {
+ let connected = true;
+ return {
+ SseProvider: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ useSseStatus: () => ({ connected }),
+ /** Test-only escape hatch — flip the SSE banner state. */
+ __setSseConnected: (v: boolean) => {
+ connected = v;
+ },
+ };
+});
+
+vi.mock("@/routes/Dashboard", () => ({
+ Dashboard: () => dashboard
,
+}));
+
+vi.mock("@/routes/DoctorPanel", () => ({
+ DoctorPanel: () => doctor
,
+}));
+
+vi.mock("@/routes/FeedFullscreen", () => ({
+ FeedFullscreen: () => feed-fs
,
+}));
+
+// LoginForm renders a real