Files
multica/server/internal/handler/client_usage_test.go
Multica Eve 216aee5629 [MUL-5125] Add daily Desktop/Web usage and runtime reporting (#5763)
* feat(analytics): add daily client usage reporting (MUL-5125)

Co-authored-by: multica-agent <github@multica.ai>

* fix(analytics): clarify daily usage semantics (MUL-5125)

Co-authored-by: multica-agent <github@multica.ai>

* fix(analytics): resolve usage review blockers (MUL-5125)

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-22 16:52:34 +08:00

92 lines
4.0 KiB
Go

package handler
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/multica-ai/multica/server/internal/middleware"
)
func ptrInt32(value int32) *int32 { return &value }
func TestValidateClientUsageRuntime(t *testing.T) {
tests := []struct {
name string
probe clientUsageRuntimeProbe
wantErr bool
}{
{name: "probe error", probe: clientUsageRuntimeProbe{ProbeResult: "error"}},
{name: "successful empty config", probe: clientUsageRuntimeProbe{ProbeResult: "success", RuntimeCount: ptrInt32(0), ProviderSummary: map[string]int{}, OnlineCount: ptrInt32(0), OfflineCount: ptrInt32(0)}},
{name: "successful mixed config", probe: clientUsageRuntimeProbe{ProbeResult: "success", RuntimeCount: ptrInt32(3), ProviderSummary: map[string]int{"claude": 2, "codex": 1}, OnlineCount: ptrInt32(1), OfflineCount: ptrInt32(2)}},
{name: "error with counts", probe: clientUsageRuntimeProbe{ProbeResult: "error", RuntimeCount: ptrInt32(0)}, wantErr: true},
{name: "missing success fields", probe: clientUsageRuntimeProbe{ProbeResult: "success"}, wantErr: true},
{name: "state total mismatch", probe: clientUsageRuntimeProbe{ProbeResult: "success", RuntimeCount: ptrInt32(2), ProviderSummary: map[string]int{"codex": 2}, OnlineCount: ptrInt32(1), OfflineCount: ptrInt32(0)}, wantErr: true},
{name: "provider total mismatch", probe: clientUsageRuntimeProbe{ProbeResult: "success", RuntimeCount: ptrInt32(2), ProviderSummary: map[string]int{"codex": 1}, OnlineCount: ptrInt32(1), OfflineCount: ptrInt32(1)}, wantErr: true},
{name: "unsafe provider", probe: clientUsageRuntimeProbe{ProbeResult: "success", RuntimeCount: ptrInt32(1), ProviderSummary: map[string]int{"Codex Pro": 1}, OnlineCount: ptrInt32(1), OfflineCount: ptrInt32(0)}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := validateClientUsageRuntime(tt.probe)
if (err != nil) != tt.wantErr {
t.Fatalf("validateClientUsageRuntime() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestNormalizeClientUsageOS(t *testing.T) {
if got := normalizeClientUsageOS(" MacOS "); got != "macos" {
t.Fatalf("normalizeClientUsageOS() = %q, want macos", got)
}
if got := normalizeClientUsageOS("Darwin 24.4"); got != "unknown" {
t.Fatalf("normalizeClientUsageOS() = %q, want unknown", got)
}
}
func TestUpsertClientUsageKeepsRuntimeSnapshotOnActivityRefresh(t *testing.T) {
const installID = "8d98d7db-4d40-4505-bc49-16b76db32721"
t.Cleanup(func() {
_, _ = testPool.Exec(context.Background(), `DELETE FROM client_usage_daily WHERE user_id = $1 AND install_id = $2`, testUserID, installID)
})
report := func(body any) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
req := newRequest(http.MethodPost, "/api/client-usage", body)
req.Header.Set("X-Client-Platform", "desktop")
req.Header.Set("X-Client-Version", "0.1.0")
req.Header.Set("X-Client-OS", "macos")
req = req.WithContext(middleware.SetClientMetadata(req.Context(), "desktop", "0.1.0", "macos"))
testHandler.UpsertClientUsage(w, req)
return w
}
w := report(map[string]any{
"install_id": installID,
"runtime": map[string]any{
"probe_result": "success", "runtime_count": 1,
"provider_summary": map[string]int{"codex": 1},
"online_count": 1, "offline_count": 0,
},
})
if w.Code != http.StatusNoContent {
t.Fatalf("runtime report status = %d: %s", w.Code, w.Body.String())
}
if w = report(map[string]any{"install_id": installID}); w.Code != http.StatusNoContent {
t.Fatalf("activity refresh status = %d: %s", w.Code, w.Body.String())
}
var rowCount, runtimeCount, onlineCount int
if err := testPool.QueryRow(context.Background(), `
SELECT count(*), max(runtime_count), max(online_count)
FROM client_usage_daily
WHERE user_id = $1 AND client_type = 'desktop' AND install_id = $2
`, testUserID, installID).Scan(&rowCount, &runtimeCount, &onlineCount); err != nil {
t.Fatal(err)
}
if rowCount != 1 || runtimeCount != 1 || onlineCount != 1 {
t.Fatalf("daily row = count %d runtime %d online %d", rowCount, runtimeCount, onlineCount)
}
}