Files
Assistant/modules/work/application/task_evaluation_callback_test.go
m 7afe3ff48f
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m4s
feat(work): accept task evaluation evidence callbacks
2026-08-23 12:28:42 +03:00

81 lines
3.2 KiB
Go

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)
}
}