52c4d1f1e6
- 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
115 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|