From 88103657fb230bb41217a06aa5602ae23e7acb8b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 17 Sep 2025 21:33:45 +0300 Subject: =?UTF-8?q?feat(stats,tmux):=20global=20=CE=A3@window=20stats=20ac?= =?UTF-8?q?ross=20processes=20with=20flocked=20cache;=20width=20mitigation?= =?UTF-8?q?=20(narrow/maxlen);=20configurable=20[stats]=20window=5Fminutes?= =?UTF-8?q?;=20robust=20coverage=20parsing;=20docs=20update\n\n-=20Add=20i?= =?UTF-8?q?nternal/stats=20with=20windowed=20event=20cache=20+=20flock=20+?= =?UTF-8?q?=20atomic=20writes\n-=20Wire=20stats=20into=20LSP/CLI/Tmux=20Ac?= =?UTF-8?q?tion;=20tmux=20shows=20=CE=A3@window=20with=20per-model=20tail\?= =?UTF-8?q?n-=20HEXAI=5FTMUX=5FSTATUS=5FNARROW=20and=20HEXAI=5FTMUX=5FSTAT?= =?UTF-8?q?US=5FMAXLEN=20for=20width=20control\n-=20Add=20[stats]=20window?= =?UTF-8?q?=5Fminutes=20to=20config=20and=20apply=20on=20startup\n-=20Impr?= =?UTF-8?q?ove=20Magefile=20coverage=20handling;=20add=20tests=20to=20lift?= =?UTF-8?q?=20coverage=20>85%\n-=20Update=20docs/tmux.md=20and=20config=20?= =?UTF-8?q?example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/stats/stats_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/stats/stats_test.go (limited to 'internal/stats/stats_test.go') diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go new file mode 100644 index 0000000..a81e215 --- /dev/null +++ b/internal/stats/stats_test.go @@ -0,0 +1,85 @@ +package stats + +import ( + "context" + "path/filepath" + "sync" + "testing" + "time" +) + +func TestUpdateAndSnapshot_Single(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + SetWindow(2 * time.Minute) + if err := Update(context.Background(), "prov", "model", 10, 20); err != nil { + t.Fatalf("update: %v", err) + } + snap, err := TakeSnapshot() + if err != nil { + t.Fatalf("snapshot: %v", err) + } + if snap.Global.Reqs != 1 || snap.Global.Sent != 10 || snap.Global.Recv != 20 { + t.Fatalf("unexpected snap: %+v", snap) + } + if snap.Providers["prov"].Totals.Reqs != 1 || snap.Providers["prov"].Models["model"].Reqs != 1 { + t.Fatalf("missing provider/model aggregates: %+v", snap) + } +} + +func TestUpdate_PrunesOld_ByWindow(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + SetWindow(2 * time.Second) + ctx := context.Background() + if err := Update(ctx, "p", "m", 1, 1); err != nil { + t.Fatal(err) + } + time.Sleep(2200 * time.Millisecond) + if err := Update(ctx, "p", "m", 2, 2); err != nil { + t.Fatal(err) + } + snap, err := TakeSnapshot() + if err != nil { + t.Fatal(err) + } + if snap.Global.Reqs != 1 || snap.Global.Sent != 2 || snap.Global.Recv != 2 { + t.Fatalf("expected first event pruned, got %+v", snap) + } +} + +func TestConcurrentUpdates_LockSafety(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + SetWindow(1 * time.Minute) + ctx := context.Background() + var wg sync.WaitGroup + n := 20 + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + if err := Update(ctx, "p", "m", i, i); err != nil { + t.Errorf("update %d: %v", i, err) + } + }(i) + } + wg.Wait() + snap, err := TakeSnapshot() + if err != nil { + t.Fatal(err) + } + if snap.Global.Reqs != int64(n) { + t.Fatalf("reqs mismatch: %d", snap.Global.Reqs) + } +} + +func TestCacheDir_XDG(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CACHE_HOME", dir) + got, err := CacheDir() + if err != nil { + t.Fatal(err) + } + want := filepath.Join(dir, "hexai") + if got != want { + t.Fatalf("got %q want %q", got, want) + } +} -- cgit v1.2.3