Files
CyberStrikeAI/internal/agentfinalizer/decision_test.go
T
2026-07-28 18:20:17 +08:00

133 lines
4.7 KiB
Go

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