feat: M4 gossip ops and UX
Refs #1 - /v1/diff endpoint returns each node's fingerprint set + tombstoned auto-thread status - knox gossip diff <peer-url>: shows peer-only/local-only fingerprints (pull/keep preview) and tombstone divergence (resolved-on-peer vs would-resurrect) - knox gossip sync: one-shot anti-entropy sweep + reconcile - gossip server starts before initial seed so peers can reach a booting node; KNOX_PEER_ADDR alone now serves without KNOX_PEERS - integration tests for diff + tombstone reporting - e2e verified: two live daemons, diff previewed 1655 peer-only fps, sync converged second node to 1655 observations
This commit is contained in:
@@ -84,6 +84,66 @@ func TestGossipConvergence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGossipDiff ensures /v1/diff reports per-node observation fingerprints and
|
||||
// tombstoned thread divergence.
|
||||
func TestGossipDiff(t *testing.T) {
|
||||
a := tmpKnoxDB(t)
|
||||
b := tmpKnoxDB(t)
|
||||
|
||||
seedObs(a, "AAA")
|
||||
seedObs(b, "BBB")
|
||||
|
||||
nodeA := &Node{Kdb: a, Name: "A"}
|
||||
sa := httptest.NewServer(nodeA.Handler())
|
||||
defer sa.Close()
|
||||
|
||||
c := &Client{Addr: sa.URL}
|
||||
d, err := c.Diff()
|
||||
if err != nil {
|
||||
t.Fatalf("diff: %v", err)
|
||||
}
|
||||
if d.NodeID != a.NodeID() {
|
||||
t.Errorf("diff node mismatch: %s != %s", d.NodeID, a.NodeID())
|
||||
}
|
||||
if len(d.Fingerprints) != 3 {
|
||||
t.Errorf("expected 3 fingerprints in diff, got %d", len(d.Fingerprints))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGossipDiffTombstones confirms resolved auto-threads surface in diff.
|
||||
func TestGossipDiffTombstones(t *testing.T) {
|
||||
a := tmpKnoxDB(t)
|
||||
|
||||
id, _, err := a.CreateThreadCluster("Test thread", "motivation", "medium", "[]", `{"trigger":"auto_thread"}`, "cluster:testkey")
|
||||
if err != nil {
|
||||
t.Fatalf("create thread: %v", err)
|
||||
}
|
||||
if err := a.CloseThread(id); err != nil {
|
||||
t.Fatalf("close thread: %v", err)
|
||||
}
|
||||
|
||||
nodeA := &Node{Kdb: a, Name: "A"}
|
||||
sa := httptest.NewServer(nodeA.Handler())
|
||||
defer sa.Close()
|
||||
|
||||
c := &Client{Addr: sa.URL}
|
||||
d, err := c.Diff()
|
||||
if err != nil {
|
||||
t.Fatalf("diff: %v", err)
|
||||
}
|
||||
if len(d.ThreadStatus) != 1 {
|
||||
t.Errorf("expected 1 thread in diff, got %d", len(d.ThreadStatus))
|
||||
}
|
||||
for k, st := range d.ThreadStatus {
|
||||
if k != "cluster:testkey" {
|
||||
t.Errorf("unexpected thread key %s", k)
|
||||
}
|
||||
if st != "resolved" {
|
||||
t.Errorf("thread %s should be resolved, got %q", k, st)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGossipIdempotent ensures a repeated sweep does not duplicate observations.
|
||||
func TestGossipIdempotent(t *testing.T) {
|
||||
a := tmpKnoxDB(t)
|
||||
|
||||
Reference in New Issue
Block a user