package flow import ( "context" "os" "testing" "time" "gitea.redpowerfuture.com/red-future/common/beans" // 空导入:pgsql 驱动。main.go 里已有,但 go test ./workflow/dao/flow/ 单测包不经 main, // 不加则报 "cannot find database driver for specified database type pgsql" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" ) // TestFlowSegmentResultDao 段结果 DAO 集成测试:Save / ListByNode / DeleteByExecution。 // 依赖本地 black_deacon + 用户信息注入;默认跳过,设置 AI_AGENT_TEST_DB=1 时运行。 func TestFlowSegmentResultDao(t *testing.T) { if os.Getenv("AI_AGENT_TEST_DB") == "" { t.Skip("设置 AI_AGENT_TEST_DB=1 且本地 black_deacon 可用时运行") } // gfdb Insert/Select 钩子从 ctx 读用户信息补 tenant_id/creator 并追加租户过滤 ctx := context.WithValue(context.Background(), "user", &beans.User{UserName: "unit_test", TenantId: 999999}) execId := time.Now().UnixMilli() nodeId := "seg_test_node" dao := FlowSegmentResultDao if err := dao.Save(ctx, execId, nodeId, 1, "video_url", "http://host/u1.mp4"); err != nil { t.Fatalf("Save 段1失败: %v", err) } if err := dao.Save(ctx, execId, nodeId, 2, "video_url", "http://host/u2.mp4"); err != nil { t.Fatalf("Save 段2失败: %v", err) } // 唯一键冲突 → 更新不报错 if err := dao.Save(ctx, execId, nodeId, 2, "video_url", "http://host/u2-new.mp4"); err != nil { t.Fatalf("Save 段2(冲突更新)失败: %v", err) } m, err := dao.ListByNode(ctx, execId, nodeId) if err != nil { t.Fatalf("ListByNode失败: %v", err) } if len(m) != 2 { t.Fatalf("期望 2 段,实际 %d", len(m)) } if m[1].URL != "http://host/u1.mp4" || m[2].URL != "http://host/u2-new.mp4" { t.Fatalf("段内容不符: %+v", m) } if m[2].Key != "video_url" { t.Fatalf("段2 key 不符: %s", m[2].Key) } if err := dao.DeleteByExecution(ctx, execId); err != nil { t.Fatalf("DeleteByExecution失败: %v", err) } m, err = dao.ListByNode(ctx, execId, nodeId) if err != nil { t.Fatalf("删除后 ListByNode失败: %v", err) } if len(m) != 0 { t.Fatalf("删除后应无段结果,实际 %d", len(m)) } } // TestFlowSegmentResultDaoResurrect 软删→重存的"复活"路径验证。 // 实体嵌入 beans.SQLBaseDO(含 deleted_at),GoFrame 将其识别为软删除列: // DeleteByExecution 实际执行的是 UPDATE ... SET deleted_at=now(),而非物理 DELETE。 // 本测试验证随后对同一 (execution_id, node_id, segment_index) 的 Save(命中 // ON CONFLICT ... DO UPDATE)是否会清除 deleted_at,使 ListByNode 再次可见该段。 // 若不可见,说明 upsert 的 SET 子句不含 deleted_at,复活路径被破坏 → 续跑复用静默失效。 func TestFlowSegmentResultDaoResurrect(t *testing.T) { if os.Getenv("AI_AGENT_TEST_DB") == "" { t.Skip("设置 AI_AGENT_TEST_DB=1 且本地 black_deacon 可用时运行") } ctx := context.WithValue(context.Background(), "user", &beans.User{UserName: "unit_test", TenantId: 999999}) execId := time.Now().UnixMilli() nodeId := "seg_test_resurrect" dao := FlowSegmentResultDao // 软删即可,物理残留由外部硬清理(docker exec psql DELETE FROM)负责 t.Cleanup(func() { _ = dao.DeleteByExecution(ctx, execId) }) if err := dao.Save(ctx, execId, nodeId, 1, "video_url", "http://host/u1-resurrect.mp4"); err != nil { t.Fatalf("Save 段1失败: %v", err) } if err := dao.Save(ctx, execId, nodeId, 2, "video_url", "http://host/u2-resurrect.mp4"); err != nil { t.Fatalf("Save 段2失败: %v", err) } // 软删除整条执行 if err := dao.DeleteByExecution(ctx, execId); err != nil { t.Fatalf("DeleteByExecution失败: %v", err) } // 重新保存段2(同一 exec/node/idx,不同 URL): // 若 ON CONFLICT DO UPDATE 的 SET 子句不含 deleted_at,段2 在 ListByNode 中仍不可见 if err := dao.Save(ctx, execId, nodeId, 2, "video_url", "http://host/u2-resurrect.mp4"); err != nil { t.Fatalf("Save 段2(软删后重存)失败: %v", err) } m, err := dao.ListByNode(ctx, execId, nodeId) if err != nil { t.Fatalf("ListByNode失败: %v", err) } // 期望:段2 复活(deleted_at 被 upsert 清除),len(m)==2 且 m[2] 为更新后的 URL if len(m) != 2 { t.Fatalf("软删后重存应复活段2(len=2),实际 len=%d: %+v", len(m), m) } if m[2].URL != "http://host/u2-resurrect.mp4" { t.Fatalf("段2 应恢复为更新后的 URL,实际: %+v", m[2]) } }