package flow import ( "context" "os" "testing" "ai-agent/workflow/consts/public" "gitea.redpowerfuture.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/db/gfdb" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" ) // resultJSONB 是 JSONB 读回的规范化形式:PG 对 {"taskId":2026} 做 JSONB 归一化后输出 // {"taskId": 2026}(冒号后带空格),直接比较字面量会失配,以实际读回为准。 const resultJSONB = `{"taskId": 2026}` func TestFlowAsyncTaskDaoRoundTrip(t *testing.T) { if os.Getenv("AI_AGENT_TEST_DB") != "1" { t.Skip("skip: AI_AGENT_TEST_DB not set") } // gfdb 的 Hook 依赖 ctx 中注入用户信息(租户/创建人),否则 Insert/Select 报 "token 数据为空" ctx := context.WithValue(context.Background(), "user", &beans.User{ UserName: "test-async", TenantId: 1, }) const execId, nodeId, seg = 900001, "node-x", 3 // 清理可能残留 if err := FlowAsyncTaskDao.DeleteByKey(ctx, execId, nodeId, seg); err != nil { t.Fatalf("cleanup: %v", err) } if err := FlowAsyncTaskDao.Upsert(ctx, execId, nodeId, seg, 1001, 2026, "model-call-test"); err != nil { t.Fatalf("upsert: %v", err) } rec, err := FlowAsyncTaskDao.Get(ctx, execId, nodeId, seg) if err != nil || rec == nil { t.Fatalf("get: %v", err) } if rec.TaskId != 2026 || rec.MsgTopic != "model-call-test" || rec.State != FlowAsyncStateInflight { t.Fatalf("upsert state wrong: %+v", rec) } if err := FlowAsyncTaskDao.UpdateByKey(ctx, execId, nodeId, seg, FlowAsyncStateDone, `{"taskId":2026}`); err != nil { t.Fatalf("update result: %v", err) } rec, err = FlowAsyncTaskDao.Get(ctx, execId, nodeId, seg) if err != nil || rec == nil { t.Fatalf("get: %v", err) } if rec.State != FlowAsyncStateDone || rec.Result != resultJSONB { t.Fatalf("result not persisted: %+v", rec) } // 唯一键 upsert:同 (exec,node,seg) 再次写入应更新而非新建 if err := FlowAsyncTaskDao.Upsert(ctx, execId, nodeId, seg, 1001, 9999, "model-call-test2"); err != nil { t.Fatalf("re-upsert: %v", err) } rec, err = FlowAsyncTaskDao.Get(ctx, execId, nodeId, seg) if err != nil || rec == nil { t.Fatalf("get: %v", err) } if rec.TaskId != 9999 { t.Fatalf("re-upsert should update task_id, got %d", rec.TaskId) } // OnDuplicate 限定了冲突更新列,已缓存结果不应被覆盖 if rec.Result != resultJSONB { t.Fatalf("re-upsert should preserve cached result, got %q", rec.Result) } // 物理删除验证:DeleteByKey 后 Get 为 nil(软删会留下 deleted_at 行且 Get 仍返回) if err := FlowAsyncTaskDao.DeleteByKey(ctx, execId, nodeId, seg); err != nil { t.Fatalf("delete: %v", err) } rec, err = FlowAsyncTaskDao.Get(ctx, execId, nodeId, seg) if err != nil { t.Fatalf("get after delete: %v", err) } if rec != nil { t.Fatalf("物理删除后不应查到残留: %+v", rec) } // DeleteByExecution if err := FlowAsyncTaskDao.Upsert(ctx, execId, nodeId, seg, 1001, 1, "t"); err != nil { t.Fatalf("upsert: %v", err) } if err := FlowAsyncTaskDao.DeleteByExecution(ctx, execId); err != nil { t.Fatalf("delete by exec: %v", err) } if rec, _ := FlowAsyncTaskDao.Get(ctx, execId, nodeId, seg); rec != nil { t.Fatalf("DeleteByExecution 后不应查到残留") } // 清理表残留(跨测试) gfdb.DB(ctx, public.DbNameBlackDeacon).Exec(ctx, "DELETE FROM black_deacon_flow_async_task WHERE execution_id = ?", execId) }