| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "path/filepath" |
| 5 | "sync" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/agent" |
| 9 | "reasonix/internal/control" |
| 10 | "reasonix/internal/event" |
| 11 | "reasonix/internal/tool" |
| 12 | ) |
| 13 | |
| 14 | type delayedContextController struct { |
| 15 | control.SessionAPI |
| 16 | once sync.Once |
| 17 | entered, release chan struct{} |
| 18 | } |
| 19 | |
| 20 | func TestContextUsageForTabReadsRotatedSessionWithoutMutatingTelemetry(t *testing.T) { |
| 21 | dir := t.TempDir() |
| 22 | rotated, stale := filepath.Join(dir, "rotated.jsonl"), filepath.Join(dir, "stale.jsonl") |
| 23 | ag := agent.New(usageProvider{}, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard) |
| 24 | tab := &WorkspaceTab{ID: "tab", Ctrl: newFixtureController(t, control.Options{ |
| 25 | Executor: ag, Sink: event.Discard, SessionDir: dir, SessionPath: rotated, |
| 26 | })} |
| 27 | // A legacy /new can rotate before the event writer re-keys telemetry. |
| 28 | tab.syncTelemetryToSession(stale) |
| 29 | tab.recordUsage(costedUsageEvent()) |
| 30 | app := &App{tabs: map[string]*WorkspaceTab{"tab": tab}} |
| 31 | info := app.ContextUsageForTab("tab") |
| 32 | if info.SessionCost != 0 || info.SessionTokens != 0 { |
| 33 | t.Fatalf("rotated session read inherited previous counters: %+v", info) |
| 34 | } |
| 35 | if got := tab.telemetrySnapshot().Usage.RequestCount; got != 1 { |
| 36 | t.Fatalf("read changed live telemetry: %d, want original 1", got) |
| 37 | } |
| 38 | // The event/lifecycle writer owns the re-key even when no overview is open. |
| 39 | tab.syncTelemetryToSession(rotated) |
| 40 | if got := tab.telemetrySnapshot().Usage.RequestCount; got != 0 { |
| 41 | t.Fatalf("writer did not reset rotated session telemetry: %d", got) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func (c *delayedContextController) SessionPath() string { |
| 46 | path := c.SessionAPI.SessionPath() |
| 47 | c.once.Do(func() { close(c.entered); <-c.release }) |
| 48 | return path |
| 49 | } |
| 50 | |
| 51 | func TestContextReadsCannotRestorePreviousSessionTelemetry(t *testing.T) { |
| 52 | for _, panel := range []bool{false, true} { |
| 53 | for _, replace := range []bool{false, true} { |
| 54 | name := "usage" |
| 55 | if panel { |
| 56 | name = "panel" |
| 57 | } |
| 58 | if replace { |
| 59 | name += "/replacement" |
| 60 | } else { |
| 61 | name += "/rotation" |
| 62 | } |
| 63 | t.Run(name, func(t *testing.T) { |
| 64 | dir := t.TempDir() |
| 65 | oldPath, newPath := filepath.Join(dir, "old.jsonl"), filepath.Join(dir, "new.jsonl") |
| 66 | makeController := func(path string) *control.Controller { |
| 67 | ag := agent.New(usageProvider{}, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard) |
| 68 | return newFixtureController(t, control.Options{Executor: ag, Sink: event.Discard, SessionDir: dir, SessionPath: path}) |
| 69 | } |
| 70 | original := makeController(oldPath) |
| 71 | old := &delayedContextController{SessionAPI: original, entered: make(chan struct{}), release: make(chan struct{})} |
| 72 | tab := &WorkspaceTab{ID: "tab", Ctrl: old} |
| 73 | tab.syncTelemetryToSession(oldPath) |
| 74 | tab.recordUsage(costedUsageEvent()) |
| 75 | if err := saveTelemetry(oldPath+".telemetry.json", tab.telemetrySnapshot()); err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | app := &App{tabs: map[string]*WorkspaceTab{"tab": tab}} |
| 79 | done := make(chan int, 1) |
| 80 | go func() { |
| 81 | if panel { |
| 82 | done <- app.ContextPanel("tab").TotalTokens |
| 83 | } else { |
| 84 | done <- app.ContextUsageForTab("tab").SessionTokens |
| 85 | } |
| 86 | }() |
| 87 | <-old.entered |
| 88 | if replace { |
| 89 | next := makeController(newPath) |
| 90 | app.mu.Lock() |
| 91 | tab.Ctrl = next |
| 92 | tab.SessionGeneration++ |
| 93 | app.mu.Unlock() |
| 94 | } else { |
| 95 | original.SetSessionPath(newPath) |
| 96 | } |
| 97 | tab.resetTelemetry(newPath) |
| 98 | close(old.release) |
| 99 | if got := <-done; got != 0 { |
| 100 | t.Fatalf("stale response contains %d previous-session tokens", got) |
| 101 | } |
| 102 | if got := tab.telemetrySnapshot().Usage; got.TotalTokens != 0 || got.SessionCost != 0 || got.RequestCount != 0 { |
| 103 | t.Fatalf("old read overwrote new telemetry: %+v", got) |
| 104 | } |
| 105 | tab.telemMu.Lock() |
| 106 | key := tab.telemetrySessionKey |
| 107 | tab.telemMu.Unlock() |
| 108 | if key != sessionRuntimeKey(newPath) { |
| 109 | t.Fatalf("telemetry owner = %q", key) |
| 110 | } |
| 111 | }) |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 |