First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s

This commit is contained in:
dela
2026-10-09 08:38:16 +08:00
commit 0335d572de
756 changed files with 201663 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
package agent
import (
"context"
"encoding/json"
"testing"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/guard"
"github.com/Autumn-27/artex/intercept"
"github.com/Autumn-27/norma/agentcore"
"github.com/Autumn-27/norma/llm"
"github.com/Autumn-27/norma/permission"
"github.com/Autumn-27/norma/tool"
)
// Exercise the actual SDK event -> hook -> execution -> result path. No real
// model or command is used; the probe tool only returns a fixed string.
func TestCaptureApprovalLifecycle(t *testing.T) {
dsn, _, err := db.DSN()
if err != nil {
t.Skip("no test database configured")
}
d, err := db.Open(dsn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.Close() })
ic := intercept.New(d)
priorTools, err := ic.GetEnabledTools()
if err != nil {
t.Fatal(err)
}
priorConfig := ic.GetJudgeConfig()
t.Cleanup(func() { _ = ic.SetEnabledTools(priorTools); _ = ic.SetJudgeConfig(priorConfig) })
if err := ic.SetEnabledTools([]string{"ApprovalAuditProbe"}); err != nil {
t.Fatal(err)
}
if err := ic.SetJudgeConfig(intercept.JudgeConfig{Enabled: true, AskTimeoutSeconds: 1, AskTimeoutAction: "allow"}); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, action, status, execution string
manual, approve, toolError bool
}{
{"model_fallback", "invalid", "allowed", "succeeded", false, false, false},
{"model_allow", "allow", "allowed", "succeeded", false, false, false},
{"model_deny", "deny", "denied", "not_executed", false, false, false},
{"human_allow_tool_error", "ask", "allowed", "failed", true, true, true},
{"human_deny", "ask", "denied", "not_executed", true, false, false},
{"timeout_allow", "ask", "timeout", "succeeded", false, false, false},
} {
t.Run(tc.name, func(t *testing.T) {
ic.SetReviewer(func(context.Context, int64, string, intercept.ReviewInput) (intercept.Decision, error) {
return intercept.Decision{Action: tc.action, Message: "probe review", ProfileID: 7}, nil
})
g := guard.NewWithInterceptor(ic)
taskID := "approval-lifecycle-" + tc.name
t.Cleanup(func() { _, _ = d.Exec(`DELETE FROM intercept_pending WHERE task_id=$1`, taskID) })
ctx := intercept.WithTaskContext(t.Context(), taskID, "test-agent", func(a db.Activity) {
if a.Kind != "intercept_request" || !tc.manual {
return
}
var detail struct {
ID int64 `json:"pending_id"`
}
if err := json.Unmarshal([]byte(a.Detail), &detail); err != nil {
t.Error(err)
return
}
if err := ic.Decide(detail.ID, tc.approve); err != nil {
t.Error(err)
}
})
turn, executions := 0, 0
provider := captureUsageProvider{stream: func(_ context.Context, yield func(llm.StreamEvent, error) bool) {
turn++
events := []llm.StreamEvent{{Type: llm.SETextDelta, Text: "done"}, {Type: llm.SEMessageDelta, StopReason: "end_turn"}}
if turn == 1 {
events = []llm.StreamEvent{
{Type: llm.SEToolUseStart, ToolID: "probe-call", ToolName: "ApprovalAuditProbe"},
{Type: llm.SEToolInputJSON, Text: `{}`}, {Type: llm.SEMessageDelta, StopReason: "tool_use"},
}
}
for _, event := range events {
if !yield(event, nil) {
return
}
}
}}
probe := tool.Build(tool.Spec{Name: "ApprovalAuditProbe", Schema: map[string]any{"type": "object"},
Run: func(context.Context, json.RawMessage, *tool.ToolContext) (tool.Result, error) {
executions++
if tc.toolError {
return tool.Errorf("probe failed"), nil
}
return tool.Text("probe succeeded"), nil
},
})
_, _, err := captureRun(ctx, agentcore.Options{Provider: provider, Tools: []tool.CoreTool{probe}, Hooks: g.Hooks(),
PermissionMode: permission.ModeBypass, WorkingDir: t.TempDir(), MaxTurns: 2}, "record this review", nil)
if err != nil {
t.Fatal(err)
}
rows, err := d.ListTaskIntercepts(taskID)
if err != nil || len(rows) != 1 {
t.Fatalf("rows=%d err=%v", len(rows), err)
}
detail, err := d.GetInterceptDetail(rows[0].ID)
if err != nil {
t.Fatal(err)
}
a := detail.Audit
initialAction := tc.action
if tc.action == "invalid" {
initialAction = "allow"
}
wantUserMessage := "record this review"
if initialAction == "allow" {
// Routine automatic allows keep decision/execution metadata but
// intentionally omit the bulky replay context from the audit row.
wantUserMessage = ""
}
if detail.Status != tc.status || detail.DecisionSource != "model" || a == nil || a.ExecutionStatus != tc.execution || a.ToolUseID != "probe-call" || a.UserMessage != wantUserMessage || a.InitialAction != initialAction || a.ModelFallback != (tc.action == "invalid") || a.ProfileID != 7 {
t.Fatalf("unexpected review: row=%+v audit=%+v", detail.InterceptApprovalRow, a)
}
if initialAction == "allow" && (len(a.Context) != 0 || a.UserTruncated || a.ContextTruncated) {
t.Fatalf("routine allow retained replay context: %+v", a)
}
wantCalls := 1
if tc.execution == "not_executed" {
wantCalls = 0
}
if executions != wantCalls {
t.Fatalf("tool ran %d times, wanted %d", executions, wantCalls)
}
})
}
}