136 lines
6.3 KiB
Go
136 lines
6.3 KiB
Go
package app
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"yingce/backend/internal/model"
|
||
)
|
||
|
||
// waitingAgentMediaApproval 把 Agent 推进到“等待生成审批”,返回运行和审批 ID。
|
||
func waitingAgentMediaApproval(t *testing.T, s *Service, a cloudAgentMediaArgs) (*model.CloudAgentExecution, string) {
|
||
t.Helper()
|
||
run, _ := agentMediaRun(t, s, a, "request_approval")
|
||
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
run, _ = s.repo.CloudAgent("user", run.ID)
|
||
state, err := cloudAgentDecode(run)
|
||
if err != nil || run.Status != "waiting_approval" || state.Approval == nil {
|
||
t.Fatalf("expected waiting approval: status=%s err=%v", run.Status, err)
|
||
}
|
||
return run, state.Approval.ID
|
||
}
|
||
|
||
// userNodeTaskRequest 复用 Agent 同一份生成请求,但以普通画布节点生成的身份提交。
|
||
func userNodeTaskRequest(t *testing.T, s *Service, run *model.CloudAgentExecution, metadata map[string]any) CreateTaskRequest {
|
||
t.Helper()
|
||
state, _ := cloudAgentDecode(run)
|
||
req, _, err := s.prepareCloudAgentMedia(run, &state, state.Calls[state.CallIndex])
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
req.Input["metadata"] = metadata
|
||
return req
|
||
}
|
||
|
||
func TestNodeGenerationSupersedesMatchingAgentApproval(t *testing.T) {
|
||
s, db, a := agentMediaFixture(t)
|
||
run, approvalID := waitingAgentMediaApproval(t, s, a)
|
||
req := userNodeTaskRequest(t, s, run, map[string]any{"nodeId": a.NodeID, "source": "canvas"})
|
||
task, err := s.CreateTask("user", req)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
run, _ = s.repo.CloudAgent("user", run.ID)
|
||
state, err := cloudAgentDecode(run)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if run.Status != "running" || state.Approval != nil || state.Decisions[approvalID] != cloudAgentApprovalSuperseded || state.CallIndex != 1 {
|
||
t.Fatalf("approval not superseded: status=%s approval=%+v decision=%q index=%d", run.Status, state.Approval, state.Decisions[approvalID], state.CallIndex)
|
||
}
|
||
decided := false
|
||
for _, event := range state.Events {
|
||
if event.Type == "approval_decided" && event.Payload["decision"] == cloudAgentApprovalSuperseded {
|
||
decided = event.Payload["approvalId"] == approvalID && event.Payload["taskId"] == task.ID && event.Payload["nodeId"] == a.NodeID
|
||
}
|
||
}
|
||
if !decided {
|
||
t.Fatal("missing superseded approval_decided event")
|
||
}
|
||
content, ok := cloudAgentToolMessage(&state, "media-call")
|
||
if !ok || !strings.Contains(content, "superseded_by_user") || !strings.Contains(content, task.ID) || cloudAgentToolContentIsError(content) {
|
||
t.Fatalf("tool result does not tell the Agent the user submitted: %q", content)
|
||
}
|
||
if state.MediaTaskID != "" || state.Generations != 0 {
|
||
t.Fatalf("Agent took over the user task: media=%s generations=%d", state.MediaTaskID, state.Generations)
|
||
}
|
||
var count, leases int64
|
||
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
|
||
db.Model(&model.CloudAgentResourceLease{}).Where("owner_id = ?", approvalID).Count(&leases)
|
||
if count != 1 || leases != 0 {
|
||
t.Fatalf("expected only the user task and released leases: tasks=%d leases=%d", count, leases)
|
||
}
|
||
|
||
// 卡片上的“同意/拒绝”晚到时不能再提交任务,并给出可读原因。
|
||
for _, decision := range []string{"approve", "reject"} {
|
||
err = s.DecideCloudAgentApproval("user", run.ID, approvalID, decision, "")
|
||
if err == nil || !strings.Contains(err.Error(), "直接生成") {
|
||
t.Fatalf("late %s must be rejected with a superseded reason: %v", decision, err)
|
||
}
|
||
}
|
||
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
|
||
if count != 1 {
|
||
t.Fatalf("late approval submitted a duplicate task: %d", count)
|
||
}
|
||
}
|
||
|
||
func TestNodeGenerationLeavesUnrelatedAgentApprovals(t *testing.T) {
|
||
for name, metadata := range map[string]map[string]any{
|
||
"other node": {"nodeId": "hero", "source": "canvas"},
|
||
"no node": {"source": "canvas"},
|
||
"agent origin": {"nodeId": "video-shot-1", "source": "cloud_agent"},
|
||
} {
|
||
t.Run(name, func(t *testing.T) {
|
||
s, _, a := agentMediaFixture(t)
|
||
run, approvalID := waitingAgentMediaApproval(t, s, a)
|
||
state, _ := cloudAgentDecode(run)
|
||
s.supersedeCloudAgentApprovalsForNodeTask("user", &model.Task{ID: "user-task", ProjectID: "agent-canvas"}, map[string]any{"metadata": metadata})
|
||
if name == "agent origin" {
|
||
// 显式带 AgentRunID 的任务同样跳过。
|
||
s.supersedeCloudAgentApprovalsForNodeTask("user", &model.Task{ID: "agent-task", ProjectID: "agent-canvas", AgentRunID: run.ID}, map[string]any{"metadata": map[string]any{"nodeId": a.NodeID}})
|
||
}
|
||
// 其他画布上的同名节点也不能关闭本画布的审批。
|
||
s.supersedeCloudAgentApprovalsForNodeTask("user", &model.Task{ID: "other-canvas-task", ProjectID: "other-canvas"}, map[string]any{"metadata": map[string]any{"nodeId": a.NodeID}})
|
||
after, _ := s.repo.CloudAgent("user", run.ID)
|
||
afterState, _ := cloudAgentDecode(after)
|
||
if after.Status != "waiting_approval" || afterState.Approval == nil || afterState.Approval.ID != approvalID || afterState.CallIndex != state.CallIndex {
|
||
t.Fatalf("unrelated task changed the approval: status=%s approval=%+v", after.Status, afterState.Approval)
|
||
}
|
||
if err := s.DecideCloudAgentApproval("user", run.ID, approvalID, "reject", ""); err != nil {
|
||
t.Fatalf("approval should still be decidable: %v", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestNodeGenerationSupersedeViaSourceNode(t *testing.T) {
|
||
s, _, a := agentMediaFixture(t)
|
||
run, approvalID := waitingAgentMediaApproval(t, s, a)
|
||
// 批量图片的结果落在新子节点,sourceNodeId 才是被审批的那个节点。
|
||
s.supersedeCloudAgentApprovalsForNodeTask("user", &model.Task{ID: "batch-child-task", ProjectID: "agent-canvas"}, map[string]any{"metadata": map[string]any{"nodeId": "batch-child", "sourceNodeId": a.NodeID}})
|
||
after, _ := s.repo.CloudAgent("user", run.ID)
|
||
state, _ := cloudAgentDecode(after)
|
||
if state.Decisions[approvalID] != cloudAgentApprovalSuperseded || state.Approval != nil {
|
||
t.Fatalf("sourceNodeId did not supersede the approval: %+v", state.Approval)
|
||
}
|
||
// 重复通知是幂等的:不会再写第二条工具结果。
|
||
s.supersedeCloudAgentApprovalsForNodeTask("user", &model.Task{ID: "batch-child-task-2", ProjectID: "agent-canvas"}, map[string]any{"metadata": map[string]any{"nodeId": a.NodeID}})
|
||
again, _ := s.repo.CloudAgent("user", run.ID)
|
||
if again.Revision != after.Revision {
|
||
t.Fatalf("repeated supersede mutated the run: %d -> %d", after.Revision, again.Revision)
|
||
}
|
||
}
|