Files
Assistant/tests/agent_execution/graph_test.go
T
m 38f8f816d5
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m33s
feat: gate graph synthesis on independent verification
2026-08-23 01:43:35 +03:00

74 lines
4.5 KiB
Go

package agent_execution_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
)
func TestWorkGraphRejectsCycleAndOrdersReadyNodes(t *testing.T) {
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphAdmitted, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Nodes: []work.GraphNode{
{ID: "node-b", IdempotencyKey: "key-b", State: work.GraphNodeReady},
{ID: "node-a", IdempotencyKey: "key-a", State: work.GraphNodeReady},
{ID: "node-c", IdempotencyKey: "key-c", State: work.GraphNodeBlocked},
}, Edges: []work.GraphEdge{{From: "node-a", To: "node-c"}}}
ready, err := workdomain.ReadyGraphNodes(graph)
if err != nil || len(ready) != 2 || ready[0] != "node-a" || ready[1] != "node-b" {
t.Fatalf("unexpected ready nodes: %#v %v", ready, err)
}
graph.Edges = append(graph.Edges, work.GraphEdge{From: "node-c", To: "node-a"})
if err := workdomain.ValidateWorkGraph(graph); err != workdomain.ErrGraphCycle {
t.Fatalf("expected cycle rejection, got %v", err)
}
}
func TestWorkGraphKeepsDependentNodeBlocked(t *testing.T) {
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphRunning, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, Nodes: []work.GraphNode{
{ID: "source", IdempotencyKey: "source-key", State: work.GraphNodeRunning},
{ID: "dependent", IdempotencyKey: "dependent-key", State: work.GraphNodeBlocked},
}, Edges: []work.GraphEdge{{From: "source", To: "dependent"}}}
ready, err := workdomain.ReadyGraphNodes(graph)
if err != nil || len(ready) != 0 {
t.Fatalf("dependent node became ready early: %#v %v", ready, err)
}
}
func TestGraphSchedulerEnforcesParallelBudgetAndExplicitConflict(t *testing.T) {
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphDraft, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Nodes: []work.GraphNode{
{ID: "b", IdempotencyKey: "b-key", State: work.GraphNodeReady},
{ID: "a", IdempotencyKey: "a-key", State: work.GraphNodeReady},
}}
admitted, err := workdomain.AdmitGraph(graph)
if err != nil || admitted.State != work.GraphAdmitted {
t.Fatalf("graph was not admitted: %#v %v", admitted, err)
}
ready, err := workdomain.ScheduleReadyNodes(admitted, 1)
if err != nil || len(ready) != 1 || ready[0] != "a" {
t.Fatalf("scheduler ignored parallel budget/order: %#v %v", ready, err)
}
conflict := work.ConflictSet{ID: "conflict-1", GraphID: "graph-1", GraphRevision: 1, NodeID: "a", CandidateIDs: []string{"candidate-b", "candidate-a"}}
resolved, err := workdomain.ResolveGraphConflict(conflict, "candidate-a")
if err != nil || !resolved.Resolved || resolved.CandidateIDs[0] != "candidate-a" {
t.Fatalf("conflict was not explicitly resolved: %#v %v", resolved, err)
}
if _, err := workdomain.ResolveGraphConflict(conflict, "unknown"); err != workdomain.ErrConflictUnresolved {
t.Fatalf("unknown candidate was accepted: %v", err)
}
}
func TestGraphSynthesisRequiresIndependentVerifierAndRetainsDissent(t *testing.T) {
conflict := work.ConflictSet{ID: "conflict-1", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", CandidateIDs: []string{"candidate-a", "candidate-b"}}
candidates := []work.ResultCandidate{{ID: "candidate-a", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", AuthorRunID: "producer-a", EvidenceSummary: "a"}, {ID: "candidate-b", GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", AuthorRunID: "producer-b", EvidenceSummary: "b"}}
resolved, synthesis, err := workdomain.BuildVerifiedGraphSynthesis(conflict, candidates, "candidate-a", "verifier-1", "independent-verifier", []work.EvidenceRef{"tests:1"}, "candidate a is supported", true)
if err != nil || !resolved.Resolved || synthesis.SelectedCandidateID != "candidate-a" || len(synthesis.DissentCandidateIDs) != 1 || synthesis.VerifierRunID == candidates[0].AuthorRunID {
t.Fatalf("unexpected synthesis: conflict=%+v synthesis=%+v err=%v", resolved, synthesis, err)
}
if _, _, err := workdomain.BuildVerifiedGraphSynthesis(conflict, candidates, "candidate-a", "producer-a", "producer", []work.EvidenceRef{"tests:1"}, "forged", true); err != workdomain.ErrGraphSynthesisRejected {
t.Fatalf("producer must not verify its own result, got %v", err)
}
if _, _, err := workdomain.BuildVerifiedGraphSynthesis(conflict, candidates, "candidate-a", "verifier-1", "independent-verifier", []work.EvidenceRef{"tests:1"}, "failed", false); err != workdomain.ErrGraphSynthesisRejected {
t.Fatalf("failed verification must not resolve conflict, got %v", err)
}
}