Files
knox/internal/metrics/metrics_test.go
T
david 52c4d1f1e6 fix: derived-state determinism, error visibility, metrics naming, HTTP hardening
- rebuild: fold observations per fingerprint in a total order
  (hcl DESC, node_id DESC, id DESC) so two nodes with identical logs
  rebuild identical entries (was: arbitrary bare-column row, merge-order
  dependent)
- threads: CreateThreadCluster uses INSERT OR IGNORE + existing-id
  fallback — concurrent auto-threaders converge instead of hitting UNIQUE
- errors surfaced instead of swallowed: scanEntries returns rows.Err(),
  Stats() fails fast on query errors, AutoLinkThreadObservations /
  linkTemporalNeighbors / golden-thread linking propagate failures,
  AddThreadNote + LinkObservationToThread write under one tx,
  watch records session upsert failures
- gossip client: push checks HTTP status and reports errors (a broken
  push direction no longer looks like a silent success); gossip diff
  gets a 10s timeout so a dead peer cannot hang the CLI
- ingest: failed source sweeps (obsidian/browser/gitea) are logged,
  and -d's help text now states its file-only scope
- watch --quiet: fatal errors go to stderr instead of io.Discard
- main: cobra SilenceErrors/SilenceUsage (errors print once, usage is
  not dumped on runtime failures); knox mcp exits 0 on SIGINT/SIGTERM
- metrics: drop _total suffix from gauges (knox_observations,
  knox_entries, knox_projects, knox_sessions, knox_peers, knox_threads);
  _total stays on counters per Prometheus convention
- http: ReadHeaderTimeout + IdleTimeout on gossip, metrics, and web servers

tests: concurrent cluster-create idempotency, HCL-order rebuild fold
(both merge orders), push HTTP-error surfacing; full suite + -race pass,
gofmt clean
2026-09-17 02:28:08 -07:00

115 lines
2.6 KiB
Go

package metrics
import (
"io"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/david/knox/internal/db"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func tmpKdb(t *testing.T) *db.KnoxDB {
t.Helper()
k, err := db.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { k.Close() })
return k
}
func seed(t *testing.T, k *db.KnoxDB, src string, n int) {
t.Helper()
for i := 0; i < n; i++ {
_, _, err := k.RecordObservation(db.ObservationRecord{
Fingerprint: "fp-" + src + "-" + string(rune('a'+i)),
SourceID: src,
SourcePath: src,
Project: "test",
ContentType: "test",
Title: src,
Summary: "s",
CreatedAt: "2026-08-29T00:00:00Z",
Confidence: 0.9,
IngesterVersion: "itest/v1",
})
if err != nil {
t.Fatalf("seed: %v", err)
}
}
}
func TestMetricsSnapshot(t *testing.T) {
k := tmpKdb(t)
seed(t, k, "git", 2)
seed(t, k, "browser-history", 3)
s, err := k.MetricsSnapshot()
if err != nil {
t.Fatalf("snapshot: %v", err)
}
if s.Observations != 5 {
t.Errorf("observations = %d, want 5", s.Observations)
}
if s.BySource["git"] != 2 || s.BySource["browser-history"] != 3 {
t.Errorf("by source = %v", s.BySource)
}
if s.KnowledgeVector[k.NodeID()] == 0 {
t.Errorf("knowledge vector missing own node")
}
if s.ByOriginNode[k.NodeID()] != 5 {
t.Errorf("by origin node = %v", s.ByOriginNode)
}
}
func TestMetricsScrape(t *testing.T) {
k := tmpKdb(t)
seed(t, k, "git", 2)
m := New(k, "testnode")
h := m.Handler()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/metrics", nil))
body, _ := io.ReadAll(rec.Body)
out := string(body)
for _, want := range []string{
`knox_node_info{name="testnode"`,
`knox_observations{source_id="git"} 2`,
`knox_gossip_pulls_total 0`,
"go_goroutines",
"process_cpu_seconds_total",
} {
if !strings.Contains(out, want) {
t.Errorf("scrape output missing %q", want)
}
}
}
func TestMetricsCounters(t *testing.T) {
k := tmpKdb(t)
m := New(k, "t")
// Manually drive counters through the Metrics API.
m.IncrementPull(3)
m.IncrementPush(7)
m.IncrementErrors()
if got := testutil.ToFloat64(m.pullsTotal); got != 1 {
t.Errorf("pullsTotal = %v, want 1", got)
}
if got := testutil.ToFloat64(m.obsPulledTotal); got != 3 {
t.Errorf("obsPulled = %v, want 3", got)
}
if got := testutil.ToFloat64(m.obsPushedTotal); got != 7 {
t.Errorf("obsPushed = %v, want 7", got)
}
if got := testutil.ToFloat64(m.errorsTotal); got != 1 {
t.Errorf("errorsTotal = %v, want 1", got)
}
}