package agentfinalizer import ( "path/filepath" "testing" "time" "cyberstrike-ai/internal/database" "cyberstrike-ai/internal/mcp" "go.uber.org/zap" ) func newDecisionTestDB(t *testing.T) *database.DB { t.Helper() db, err := database.NewDB(filepath.Join(t.TempDir(), "finalizer.db"), zap.NewNop()) if err != nil { t.Fatalf("NewDB: %v", err) } t.Cleanup(func() { _ = db.Close() }) return db } func saveDecisionTestExecution(t *testing.T, db *database.DB, id, status string) { t.Helper() if err := db.SaveToolExecution(&mcp.ToolExecution{ ID: id, ToolName: "test::tool", Arguments: map[string]interface{}{"input": id}, Status: status, StartTime: time.Now(), }); err != nil { t.Fatalf("SaveToolExecution(%s): %v", id, err) } } func TestDecideBlocksPendingToolExecutions(t *testing.T) { db := newDecisionTestDB(t) saveDecisionTestExecution(t, db, "run-queued", mcp.ToolExecutionStatusQueued) saveDecisionTestExecution(t, db, "run-running", mcp.ToolExecutionStatusRunning) saveDecisionTestExecution(t, db, "run-completed", mcp.ToolExecutionStatusCompleted) d := Decide(db, Input{ Response: "工具还没全部结束时,这只是一段候选输出。", MCPExecutionIDs: []string{"run-queued", "run-running", "run-completed"}, }) if d.Finalizable || d.Finalized { t.Fatalf("pending tools should not be finalizable: %+v", d) } if d.Status != StatusInProgress || d.CompletionReason != ReasonPendingTools { t.Fatalf("status/reason = %s/%s, want %s/%s", d.Status, d.CompletionReason, StatusInProgress, ReasonPendingTools) } if got, want := len(d.PendingExecutionIDs), 2; got != want { t.Fatalf("pending execution count = %d, want %d (%v)", got, want, d.PendingExecutionIDs) } } func TestDecideBlocksAwaitingHITLAndEmptyCandidate(t *testing.T) { hitl := Decide(nil, Input{Response: "等待人工审批", AwaitingHITL: true}) if hitl.Finalizable || hitl.Status != StatusAwaitingHITL || hitl.CompletionReason != ReasonAwaitingHITL { t.Fatalf("HITL decision mismatch: %+v", hitl) } empty := Decide(nil, Input{Response: "⚠️ Eino 执行完成,但未捕获到助手文本输出。"}) if empty.Finalizable || empty.Status != StatusBlocked || empty.CompletionReason != ReasonEmptyResponse { t.Fatalf("empty candidate decision mismatch: %+v", empty) } } func TestDecideBlocksWhenExecutionEvidenceIsRequiredButMissing(t *testing.T) { d := Decide(nil, Input{ Response: "任务已处理完成。", RequireExecutionEvidence: true, }) if d.Finalizable || d.Finalized { t.Fatalf("missing required execution evidence should not finalize: %+v", d) } if d.Status != StatusBlocked || d.CompletionReason != ReasonMissingEvidence { t.Fatalf("status/reason = %s/%s, want %s/%s", d.Status, d.CompletionReason, StatusBlocked, ReasonMissingEvidence) } if d.EvidenceVerified { t.Fatalf("missing required execution evidence should be marked unverified: %+v", d) } if len(d.MissingChecks) == 0 { t.Fatalf("missing checks should explain the evidence gap: %+v", d) } } func TestDecideBlocksWhenOnlyFailedEvidenceIsRecorded(t *testing.T) { db := newDecisionTestDB(t) saveDecisionTestExecution(t, db, "run-failed", mcp.ToolExecutionStatusFailed) saveDecisionTestExecution(t, db, "run-cancelled", mcp.ToolExecutionStatusCancelled) d := Decide(db, Input{ Response: "任务已处理完成。", MCPExecutionIDs: []string{"run-failed", "run-cancelled"}, RequireExecutionEvidence: true, }) if d.Finalizable || d.Finalized { t.Fatalf("failed evidence should not satisfy required execution evidence: %+v", d) } if d.Status != StatusBlocked || d.CompletionReason != ReasonMissingEvidence { t.Fatalf("status/reason = %s/%s, want %s/%s", d.Status, d.CompletionReason, StatusBlocked, ReasonMissingEvidence) } } func TestDecideFinalizesCompletedEvidence(t *testing.T) { db := newDecisionTestDB(t) saveDecisionTestExecution(t, db, "run-ok", mcp.ToolExecutionStatusCompleted) d := Decide(db, Input{ Response: "任务已处理完成,见工具执行记录。", MCPExecutionIDs: []string{"run-ok"}, RequireExecutionEvidence: true, }) if !d.Finalizable || !d.Finalized || d.Status != StatusCompleted { t.Fatalf("completed execution should finalize: %+v", d) } if !d.EvidenceVerified || len(d.EvidenceRefs) != 1 { t.Fatalf("evidence refs mismatch: %+v", d) } } func TestDecideAllowsInformationalAnswerWhenExecutionEvidenceIsNotRequired(t *testing.T) { d := Decide(nil, Input{Response: "这是一个概念解释,不需要执行工具。"}) if !d.Finalizable || !d.Finalized || d.Status != StatusCompleted { t.Fatalf("informational response should finalize when execution evidence is not required: %+v", d) } }