package agent import ( "testing" "github.com/Autumn-27/artex/db" ) // helper: intent/fact node func intent(id int64, state string) cgNode { return cgNode{ID: id, Kind: db.KindIntent, State: state} } func fact(id int64) cgNode { return cgNode{ID: id, Kind: db.KindFact, State: "confirmed"} } func yields(from, to int64) cgEdge { return cgEdge{From: from, Rel: db.RelYields, To: to} } func derived(from, to int64) cgEdge { return cgEdge{From: from, Rel: db.RelDerivedFrom, To: to} } func ptr(v int64) *int64 { return &v } // §附 快照1: a→b→c, b→d, with d live (running) and c settled/inactive. // Expected: d/b/a hot (b kept hot by the live b→d branch); c is a cold candidate // but an isolated singleton → not folded. func TestHotCold_AnyLiveBranchKeepsChainHot(t *testing.T) { // a(intent) → b(intent) → c(fact); b → d(intent, running) nodes := []cgNode{intent(1, "done"), intent(2, "done"), fact(3), intent(4, "running")} edges := []cgEdge{derived(1, 2), yields(2, 3), derived(2, 4)} g := newColdGraph(nodes, edges) hot := g.hotSet() for _, id := range []int64{1, 2, 4} { if !hot[id] { t.Fatalf("node %d should be hot (ancestor of / is live intent 4)", id) } } if hot[3] { t.Fatalf("node 3 (dead leaf c) should be cold") } structCold := g.structuralCold(hot) if !structCold[3] || len(structCold) != 1 { t.Fatalf("only node 3 should be structurally cold, got %v", structCold) } } // §附 快照2: once d also finishes, a/b/c/d are all cold and connected → one block. func TestHotCold_WholeChainFoldsWhenAllSettled(t *testing.T) { nodes := []cgNode{intent(1, "done"), intent(2, "done"), fact(3), intent(4, "done")} edges := []cgEdge{derived(1, 2), yields(2, 3), derived(2, 4)} g := newColdGraph(nodes, edges) hot := g.hotSet() if len(hot) != 0 { t.Fatalf("nothing should be hot once all settled, got %v", hot) } structCold := g.structuralCold(hot) // all stamped R+ rounds ago stamps := map[int64]*int64{1: ptr(1), 2: ptr(1), 3: ptr(1), 4: ptr(1)} elig := g.eligibleCold(structCold, stamps, 100, defaultColdParams()) if len(elig) != 4 { t.Fatalf("all 4 nodes should be eligible cold, got %d", len(elig)) } blocks := g.group(elig, defaultColdParams()) if len(blocks) != 1 || len(blocks[0].Members) != 4 { t.Fatalf("expected one 4-member block, got %+v", blocks) } } // §2.3 debounce: a freshly-cooled node (stamp too recent) is not yet eligible. func TestDebounce_RecentlyCooledNotEligible(t *testing.T) { nodes := []cgNode{intent(1, "done"), fact(2)} edges := []cgEdge{yields(1, 2)} g := newColdGraph(nodes, edges) structCold := g.structuralCold(g.hotSet()) stamps := map[int64]*int64{1: ptr(98), 2: ptr(98)} // cooled at round 98 elig := g.eligibleCold(structCold, stamps, 100, defaultColdParams()) if len(elig) != 0 { t.Fatalf("nodes cooled only 2 rounds ago (