package application import ( "context" "errors" "testing" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/work" ) type callbackGraphStore struct { graph work.WorkGraph saves int } func (s *callbackGraphStore) SaveWorkGraph(_ context.Context, graph work.WorkGraph) error { s.graph = graph s.saves++ return nil } func (s *callbackGraphStore) LoadWorkGraph(context.Context, string, int64) (work.WorkGraph, error) { return s.graph, nil } func (s *callbackGraphStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error { return nil } func (s *callbackGraphStore) EnqueueDispatch(context.Context, work.AgentDispatchQueueItem) error { return nil } type callbackSink struct { evaluation capability.TaskEvaluation err error } func (s *callbackSink) SaveTaskEvaluation(_ context.Context, value capability.TaskEvaluation) error { s.evaluation = value return s.err } func callbackRequest() (work.WorkGraph, work.TaskEvaluationEvidenceCallback) { graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphRunning, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, Nodes: []work.GraphNode{{ID: "node-1", DispatchID: "dispatch-1", IdempotencyKey: "idem-1", State: work.GraphNodeRunning, Purpose: work.GraphNodePurposeTaskEvaluation, TaskProfileID: "profile-1", BindingID: "binding-1"}}} evaluation := capability.TaskEvaluation{ID: "evaluation-1", ProfileID: "profile-1", BindingID: "binding-1", BindingRevision: 2, CapabilityID: "capability-1", Result: capability.EvaluationSupported} return graph, work.TaskEvaluationEvidenceCallback{GraphID: "graph-1", Revision: 1, NodeID: "node-1", Evaluation: evaluation, Succeeded: true} } func TestCompleteTaskEvaluationPersistsEvidenceBeforeGraphCompletion(t *testing.T) { graph, callback := callbackRequest() store := &callbackGraphStore{graph: graph} sink := &callbackSink{} completed, err := CompleteTaskEvaluationNode(context.Background(), store, sink, callback) if err != nil { t.Fatal(err) } if sink.evaluation.ID != "evaluation-1" || store.saves != 1 || completed.State != work.GraphCompleted || completed.Nodes[0].State != work.GraphNodeSuccess { t.Fatalf("callback did not complete in order: evaluation=%+v saves=%d graph=%+v", sink.evaluation, store.saves, completed) } } func TestCompleteTaskEvaluationLeavesNodeOpenWhenCapabilityRejectsEvidence(t *testing.T) { graph, callback := callbackRequest() store := &callbackGraphStore{graph: graph} sink := &callbackSink{err: errors.New("capability unavailable")} if _, err := CompleteTaskEvaluationNode(context.Background(), store, sink, callback); err == nil { t.Fatal("expected capability failure") } if store.saves != 0 { t.Fatalf("graph was advanced after capability failure: saves=%d", store.saves) } } func TestCompleteTaskEvaluationRejectsLineageMismatch(t *testing.T) { graph, callback := callbackRequest() callback.Evaluation.ProfileID = "profile-other" store := &callbackGraphStore{graph: graph} if _, err := CompleteTaskEvaluationNode(context.Background(), store, &callbackSink{}, callback); !errors.Is(err, ErrTaskEvaluationCallbackMismatch) { t.Fatalf("expected lineage rejection, got %v", err) } }