a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
205 lines
6.1 KiB
Go
205 lines
6.1 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestIndexAndSessionRecordSitesEmit proves the F9 record-site helpers emit the
|
|
// right allow-listed, bucketed counters: an index pass records a file-count
|
|
// BUCKET (never the exact count) plus deduped per-language counters, and the
|
|
// daemon-session / install / uninstall sites emit their bounded dimensions.
|
|
func TestIndexAndSessionRecordSitesEmit(t *testing.T) {
|
|
store := NewStore(t.TempDir())
|
|
r := NewRecorder(Consent{Enabled: true}, store)
|
|
r.now = fixedNow("2026-06-18")
|
|
|
|
RecordIndex(r, 4200, []string{"go", "python", "go", "", " "})
|
|
RecordDaemonSession(r, "sqlite")
|
|
RecordInstall(r, "install", "claude")
|
|
RecordInstall(r, "uninstall", "cursor")
|
|
r.Flush()
|
|
|
|
got, err := store.Load("2026-06-18")
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
|
|
if got.Counts["index:1k-10k"] != 1 {
|
|
t.Errorf("index bucket = %d, want 1 (counts=%v)", got.Counts["index:1k-10k"], got.Counts)
|
|
}
|
|
if got.Counts["index:4200"] != 0 {
|
|
t.Error("the exact file count must never be recorded")
|
|
}
|
|
// Per-language counters: deduped (go appears twice), empties dropped.
|
|
if got.Counts["index_lang:go"] != 1 {
|
|
t.Errorf("index_lang:go = %d, want 1 (deduped)", got.Counts["index_lang:go"])
|
|
}
|
|
if got.Counts["index_lang:python"] != 1 {
|
|
t.Errorf("index_lang:python = %d, want 1", got.Counts["index_lang:python"])
|
|
}
|
|
if got.Counts["index_lang"] != 0 {
|
|
t.Error("a blank language must not record a bare index_lang counter")
|
|
}
|
|
if got.Counts["daemon_session:sqlite"] != 1 {
|
|
t.Errorf("daemon_session:sqlite = %d, want 1", got.Counts["daemon_session:sqlite"])
|
|
}
|
|
if got.Counts["install:claude"] != 1 || got.Counts["uninstall:cursor"] != 1 {
|
|
t.Errorf("install/uninstall counters wrong: %v", got.Counts)
|
|
}
|
|
}
|
|
|
|
// TestClientNameFolding proves an MCP client handshake folds to a bounded,
|
|
// version-free token, lowercased, and that empty names record nothing.
|
|
func TestClientNameFolding(t *testing.T) {
|
|
store := NewStore(t.TempDir())
|
|
r := NewRecorder(Consent{Enabled: true}, store)
|
|
r.now = fixedNow("2026-06-18")
|
|
|
|
RecordClient(r, "claude-code 1.0.42") // version dropped
|
|
RecordClient(r, "Cursor") // lowercased
|
|
RecordClient(r, " ") // whitespace-only → dropped
|
|
RecordClient(r, "") // empty → dropped
|
|
r.Flush()
|
|
|
|
got, err := store.Load("2026-06-18")
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got.Counts["client:claude-code"] != 1 {
|
|
t.Errorf("client fold = %v, want client:claude-code", got.Counts)
|
|
}
|
|
if got.Counts["client:cursor"] != 1 {
|
|
t.Errorf("client name must be lowercased: %v", got.Counts)
|
|
}
|
|
for k := range got.Counts {
|
|
if strings.Contains(k, "1.0.42") {
|
|
t.Errorf("the client version leaked into telemetry: %q", k)
|
|
}
|
|
}
|
|
if got.Counts["client"] != 0 {
|
|
t.Error("an empty client name must not record a bare client counter")
|
|
}
|
|
if got := NormalizeClientName("VS Code 1.2"); got != "vs" {
|
|
t.Errorf("NormalizeClientName first-field-lowercase = %q, want vs", got)
|
|
}
|
|
}
|
|
|
|
func TestBucketFileCount(t *testing.T) {
|
|
cases := []struct {
|
|
n int
|
|
want string
|
|
}{
|
|
{0, "<100"}, {99, "<100"},
|
|
{100, "100-1k"}, {999, "100-1k"},
|
|
{1000, "1k-10k"}, {9999, "1k-10k"},
|
|
{10000, "10k+"}, {5_000_000, "10k+"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := BucketFileCount(c.n); got != c.want {
|
|
t.Errorf("BucketFileCount(%d) = %q, want %q", c.n, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBucketDuration(t *testing.T) {
|
|
cases := []struct {
|
|
d time.Duration
|
|
want string
|
|
}{
|
|
{5 * time.Second, "<10s"},
|
|
{10 * time.Second, "10-60s"},
|
|
{59 * time.Second, "10-60s"},
|
|
{time.Minute, "1-5m"},
|
|
{4 * time.Minute, "1-5m"},
|
|
{5 * time.Minute, "5m+"},
|
|
{time.Hour, "5m+"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := BucketDuration(c.d); got != c.want {
|
|
t.Errorf("BucketDuration(%s) = %q, want %q", c.d, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRollupAddAllowList(t *testing.T) {
|
|
r := NewRollup(time.Date(2026, 6, 18, 9, 0, 0, 0, time.UTC))
|
|
if r.Day != "2026-06-18" {
|
|
t.Fatalf("Day = %q, want 2026-06-18", r.Day)
|
|
}
|
|
|
|
// Allow-listed key counts.
|
|
if !r.Add("mcp_tool_call", "search_symbols") {
|
|
t.Error("allow-listed metric was dropped")
|
|
}
|
|
r.Add("mcp_tool_call", "search_symbols")
|
|
if got := r.Counts["mcp_tool_call:search_symbols"]; got != 2 {
|
|
t.Errorf("dim counter = %d, want 2", got)
|
|
}
|
|
|
|
// A key not on the allow-list is dropped entirely.
|
|
if r.Add("secret_path", "/Users/x/private.go") {
|
|
t.Error("non-allow-listed metric must be dropped")
|
|
}
|
|
if len(r.Counts) != 1 {
|
|
t.Errorf("dropped metric leaked into counts: %v", r.Counts)
|
|
}
|
|
|
|
// A path-like dimension on an allowed key is discarded; the bare key
|
|
// still counts (no leak of the path).
|
|
r.Add("index", "/abs/path/with/slashes")
|
|
if _, leaked := r.Counts["index:/abs/path/with/slashes"]; leaked {
|
|
t.Error("path dimension leaked into a counter name")
|
|
}
|
|
if got := r.Counts["index"]; got != 1 {
|
|
t.Errorf("bare key counter = %d, want 1", got)
|
|
}
|
|
|
|
// A bucket-label dimension passes.
|
|
r.Add("index", BucketFileCount(5000))
|
|
if got := r.Counts["index:1k-10k"]; got != 1 {
|
|
t.Errorf("bucket dim counter = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestRollupMerge(t *testing.T) {
|
|
day := time.Date(2026, 6, 18, 0, 0, 0, 0, time.UTC)
|
|
a := NewRollup(day)
|
|
a.Add("cli_command", "index")
|
|
b := NewRollup(day)
|
|
b.Add("cli_command", "index")
|
|
b.Add("daemon_session", "sqlite")
|
|
a.Merge(b)
|
|
if got := a.Counts["cli_command:index"]; got != 2 {
|
|
t.Errorf("merged counter = %d, want 2", got)
|
|
}
|
|
if got := a.Counts["daemon_session:sqlite"]; got != 1 {
|
|
t.Errorf("merged new counter = %d, want 1", got)
|
|
}
|
|
if a.Total() != 3 {
|
|
t.Errorf("Total = %d, want 3", a.Total())
|
|
}
|
|
|
|
// A mismatched-day merge is ignored (no corruption).
|
|
other := NewRollup(day.AddDate(0, 0, 1))
|
|
other.Add("cli_command", "x")
|
|
a.Merge(other)
|
|
if a.Total() != 3 {
|
|
t.Errorf("mismatched-day merge mutated rollup: Total = %d, want 3", a.Total())
|
|
}
|
|
}
|
|
|
|
func TestIsAllowedMetric(t *testing.T) {
|
|
for _, k := range []string{"mcp_tool_call", "cli_command", "index", "daemon_session"} {
|
|
if !IsAllowedMetric(k) {
|
|
t.Errorf("%q should be allow-listed", k)
|
|
}
|
|
}
|
|
for _, k := range []string{"", "file_path", "user_query", "anything_else"} {
|
|
if IsAllowedMetric(k) {
|
|
t.Errorf("%q must not be allow-listed", k)
|
|
}
|
|
}
|
|
}
|