Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_diagnostics_test.go

189 lines
8.0 KiB
Go

package app
import (
"context"
"encoding/json"
"strings"
"testing"
"infinite-canvas/backend/internal/model"
)
func TestCloudAgentCanvasArgumentFeedbackIdentifiesFailingOperation(t *testing.T) {
for _, tc := range []struct{ op, field, issue string }{
{`{"type":"add_node","id":"note","nodeType":"text","snapshotHash":"misplaced"}`, "ops[0].snapshotHash", "unexpected_field"},
{`{"type":"add_node","nodeType":"text"}`, "ops[0].id", "required"},
{`{"type":"add_node","id":"note"}`, "ops[0].nodeType", "required"},
{`{"type":"connect_nodes","id":"edge","toNodeId":"target"}`, "ops[0].fromNodeId", "required"},
{`{"type":"update_node","id":"note"}`, "ops[0].patch", "required"},
{`{"type":"add_node","id":"note","nodeType":"text","x":"private-sentinel"}`, "ops[0].x", "type_mismatch"},
{`{"type":"add_node","id":"note","nodeType":"text","private-sentinel":"private-sentinel"}`, "ops[0]", "unexpected_field"},
} {
t.Run(tc.field+tc.issue, func(t *testing.T) {
call := cloudAgentCall{ID: "write"}
call.Function.Name, call.Function.Arguments = "canvas_apply_ops", `{"snapshotHash":"snapshot","ops":[`+tc.op+`]}`
// Malformed calls must fail before reading, approving, or writing.
_, err := prepareCloudAgentCanvasMutation(nil, "user", "canvas", call)
if err == nil {
t.Fatal("invalid operation accepted")
}
req := agentTestRequest()
req.PermissionMode = "request_approval"
state := &cloudAgentRuntime{Canonical: canonicalAgentRequest{Tools: cloudAgentTools(req)}}
cloudAgentToolResult("run", state, call, nil, err)
content := state.Canonical.Messages[0]["content"].(string)
var result map[string]any
if err := json.Unmarshal([]byte(content), &result); err != nil {
t.Fatal(err)
}
if result["field"] != tc.field || result["issue"] != tc.issue || result["reason"] != "invalid_tool_arguments" || result["parameters"] == nil || strings.Contains(content, "private-sentinel") {
t.Fatalf("incorrect repair feedback: %s", content)
}
})
}
// Replay the logged failure with only the misplaced field removed.
s, _, _ := agentMediaFixture(t)
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
call := cloudAgentCall{ID: "corrected"}
call.Function.Name = "canvas_apply_ops"
call.Function.Arguments = `{"snapshotHash":"` + cloudAgentCanvasHash(doc) + `","ops":[{"type":"add_node","id":"note","nodeType":"text"}]}`
plan, err := prepareCloudAgentCanvasMutation(s.repo, "user", "agent-canvas", call)
if err != nil || plan == nil {
t.Fatalf("corrected call failed: %v", err)
}
stored, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
if stored.PayloadJSON != canvas.PayloadJSON {
t.Fatal("preview wrote without approval")
}
}
func TestCloudAgentGenerationDiagnosticsSeparateOutputAndTask(t *testing.T) {
s, db, _ := agentMediaFixture(t)
for _, tc := range []struct {
name, user, canvas, detail string
status model.TaskStatus
visible bool
}{
{"failed", "user", "agent-canvas", "media dimensions must be between 256 and 5760 pixels", model.TaskStatusFailed, true},
{"cancelled", "user", "agent-canvas", "任务已取消", model.TaskStatusCancelled, true},
{"sensitive", "user", "agent-canvas", "https://private-sentinel.invalid?token=private-sentinel", model.TaskStatusFailed, true},
{"other-user", "other", "agent-canvas", "private-sentinel", model.TaskStatusFailed, false},
{"other-canvas", "user", "other-canvas", "private-sentinel", model.TaskStatusFailed, false},
} {
t.Run(tc.name, func(t *testing.T) {
task := &model.Task{ID: tc.name, UserID: tc.user, ProjectID: tc.canvas, Status: tc.status, Error: tc.detail}
if err := db.Create(task).Error; err != nil {
t.Fatal(err)
}
node := map[string]any{"id": "video", "type": "video", "metadata": map[string]any{"status": "loading", "taskId": task.ID}}
doc := map[string]any{"nodes": []map[string]any{node}}
view, err := cloudAgentCanvasState(s.repo, "user", "agent-canvas", doc, 0, nil, 0)
if err != nil {
t.Fatal(err)
}
item := view.(map[string]any)["nodes"].([]any)[0].(map[string]any)
generation := item["generation"].(map[string]any)
output := item["outputReference"].(map[string]any)
if output["ready"] != false || output["issue"] == nil || generation["submitBlockedReason"] != "task_bound" {
t.Fatalf("ambiguous state: %+v", item)
}
if tc.visible {
if generation["taskId"] != task.ID || generation["taskStatus"] != string(tc.status) || generation["error"] == nil {
t.Fatalf("lost task failure: %+v", generation)
}
if tc.name == "failed" && generation["error"] != tc.detail {
t.Fatalf("lost useful diagnostic: %+v", generation)
}
} else if generation["taskStatus"] != "unavailable" || generation["taskId"] != nil || generation["error"] != nil {
t.Fatalf("task ownership leaked: %+v", generation)
}
raw, _ := json.Marshal(view)
if strings.Contains(string(raw), "private-sentinel") {
t.Fatal("unsafe error leaked")
}
call := cloudAgentCall{ID: "query"}
call.Function.Name, call.Function.Arguments = "task_get", `{"taskId":"`+task.ID+`"}`
result, err := cloudAgentReadTool(s.repo, "user", &cloudAgentRuntime{Request: agentTestRequest()}, call)
if tc.visible {
if err != nil || result.(map[string]any)["error"] != generation["error"] {
t.Fatalf("task_get lost diagnostic: %+v %v", result, err)
}
} else if err == nil {
t.Fatal("task_get crossed ownership boundary")
}
})
}
}
func TestCloudAgentMediaAdmissionReportsTaskBinding(t *testing.T) {
s, db, args := agentMediaFixture(t)
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
doc["nodes"] = append(creationMaps(doc["nodes"]), map[string]any{"id": args.NodeID, "type": "video", "metadata": map[string]any{"status": "error", "taskId": "original-task"}})
raw, _ := json.Marshal(doc)
if err := db.Model(canvas).Update("payload_json", string(raw)).Error; err != nil {
t.Fatal(err)
}
args.SnapshotHash, args.DraftRunID = cloudAgentMediaContentHash(doc), "current-run"
_, _, _, err := cloudAgentMediaDocument(s.repo, "user", "agent-canvas", args)
if err == nil {
t.Fatal("existing task overwritten")
}
state := &cloudAgentRuntime{}
cloudAgentToolResult("run", state, agentMediaCall(args), map[string]any{"phase": "admission", "taskSubmitted": false}, err)
var result map[string]any
if err := json.Unmarshal([]byte(state.Canonical.Messages[0]["content"].(string)), &result); err != nil {
t.Fatal(err)
}
if result["reason"] != "task_bound" || result["nodeId"] != args.NodeID || result["taskSubmitted"] != false {
t.Fatalf("missing admission diagnosis: %+v", result)
}
var count int64
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
if count != 0 {
t.Fatal("admission submitted task")
}
}
func TestCloudAgentCancellationRecordsSourceOnce(t *testing.T) {
s, _, args := agentMediaFixture(t)
run, _ := agentMediaRun(t, s, args, "request_approval")
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
approveAgentMediaDraft(t, s, run.ID)
run, _ = s.repo.CloudAgent("user", run.ID)
before, err := cloudAgentDecode(run)
if err != nil || before.MediaTaskID == "" {
t.Fatalf("missing media task: %v", err)
}
for range 2 {
if err := s.CancelCloudAgent(context.Background(), "user", run.ID); err != nil {
t.Fatal(err)
}
}
run, _ = s.repo.CloudAgent("user", run.ID)
after, err := cloudAgentDecode(run)
if err != nil {
t.Fatal(err)
}
count := 0
for _, event := range after.Events {
if event.Type != "run_cancelled" {
continue
}
count++
if event.Payload["source"] != "user_request" || event.Payload["mediaTaskId"] != before.MediaTaskID {
t.Fatalf("missing cancel source: %+v", event)
}
}
if count != 1 {
t.Fatalf("expected one cancellation event, got %d", count)
}
task, _ := s.repo.TaskForUser("user", before.MediaTaskID)
if task.Status != model.TaskStatusCancelled {
t.Fatalf("child not cancelled: %s", task.Status)
}
}