feat(projectcontext): bizdev 注入项目对话并完成 PG 治理 E2E
新增 ctxmgr 项目对话采集并接入 PM/TE BuildSnapshot;PG 测试统一 TEST_DATABASE_URL 并使用三正式工作流 key。
This commit is contained in:
@@ -18,9 +18,9 @@
|
|||||||
| BUG-10 | 无审计视图 | PASS | `recent_workflow_events` in settings + UI | 已完成 |
|
| BUG-10 | 无审计视图 | PASS | `recent_workflow_events` in settings + UI | 已完成 |
|
||||||
| BUG-11 | 旧 API 未下线 | PARTIAL | 治理开启时 410 | 保持 + 文档标注 |
|
| BUG-11 | 旧 API 未下线 | PARTIAL | 治理开启时 410 | 保持 + 文档标注 |
|
||||||
| BUG-12 | 前端名称硬编码 | PASS | `ProjectWorkflowSwitcher` catalog API | 已完成 |
|
| BUG-12 | 前端名称硬编码 | PASS | `ProjectWorkflowSwitcher` catalog API | 已完成 |
|
||||||
| MISSING-01 | 上下文快照 0% | PARTIAL | `projectcontext/service.go` 五层源 + 私有对话排除计数 | PM/TE bizdev 传入 Dialogue 待加深 |
|
| MISSING-01 | 上下文快照 0% | PASS | `ctxmgr.CollectProjectDialogue` + bizdev `collectSnapshotDialogueInputs` → `BuildSnapshot` | 已完成 |
|
||||||
| MISSING-02 | 终止 0% | PASS | `GetTerminationPreview` + 取消在途 + 前端 | 已完成 |
|
| MISSING-02 | 终止 0% | PASS | `GetTerminationPreview` + 取消在途 + 前端 | 已完成 |
|
||||||
| MISSING-04 | E2E 0% | PARTIAL | `e2e_workflow_governance_test.go` 单工作流完成→planning | 三正式工作流 + 真实 PI Agent 待 test 分支补跑 |
|
| MISSING-04 | E2E 0% | PARTIAL | PG E2E:`test_manager` 完成→planning + `main` 激活/批准/推进(test 库 PASS) | 三流程 + 真实 PI Agent 全链路待补跑 |
|
||||||
|
|
||||||
**结论**: Phase A 全部 PASS;Phase B/C 核心项 PASS,E2E 与上下文快照仍有 PARTIAL。test 分支合并前需 `TEST_DATABASE_URL` + 真实 PI Agent 全链路验收。
|
**结论**: Phase A 全部 PASS;Phase B/C 核心项 PASS,E2E 与上下文快照仍有 PARTIAL。test 分支合并前需 `TEST_DATABASE_URL` + 真实 PI Agent 全链路验收。
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
## 二、 测试目标 (Test Objectives)
|
## 二、 测试目标 (Test Objectives)
|
||||||
|
|
||||||
- Phase A:生命周期补测、推进不改文档、禁用文档、对话不切 bizdev、token 一次性、DocumentAccessDecider + HTTP 409
|
- Phase A:生命周期补测、推进不改文档、禁用文档、对话不切 bizdev、token 一次性、DocumentAccessDecider + HTTP 409
|
||||||
- Phase B:终止 preview、E2E 完成→planning、上下文快照可复现(含私有对话排除计数)
|
- Phase B:终止 preview、E2E 完成→planning、上下文快照可复现(bizdev→projectcontext Dialogue 集成)
|
||||||
- Phase C:catalog 驱动名称、版本冲突提示、settings 审计事件
|
- Phase C:catalog 驱动名称、版本冲突提示、settings 审计事件
|
||||||
|
|
||||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||||
@@ -38,25 +38,47 @@
|
|||||||
|
|
||||||
**判定**: PASS — live 实例存在时 `ExecuteStage` 调用次数为 0
|
**判定**: PASS — live 实例存在时 `ExecuteStage` 调用次数为 0
|
||||||
|
|
||||||
### 5.3 上下文快照
|
### 5.3 上下文快照与 Dialogue 集成
|
||||||
|
|
||||||
**命令**: `go test ./internal/projectcontext/... -count=1`
|
**命令**:
|
||||||
|
```bash
|
||||||
|
go test ./internal/ctxmgr/... ./internal/bizdev/... ./internal/projectcontext/... \
|
||||||
|
-run 'CollectProjectDialogue|CollectSnapshot|BuildSnapshot' -count=1 -v
|
||||||
|
```
|
||||||
|
|
||||||
**判定**: PASS — 同输入 hash 可复现;`[private]`/`[draft]` 对话计入 `ExcludedPrivateCount` 且不进入来源清单
|
**判定**: PASS — bizdev `system_reqs`/`test_manager` 执行路径经 `CollectProjectDialogue` 注入共享对话与里程碑决策;私有/禁用导出对话被排除
|
||||||
|
|
||||||
### 5.4 PG E2E(需 TEST_DATABASE_URL)
|
### 5.4 PG E2E(隔离 test 库)
|
||||||
|
|
||||||
**命令**: `TEST_DATABASE_URL=... go test ./internal/server/... -run TestE2E_WorkflowGovernance_CompleteAndReplan -count=1 -v`
|
**环境**: `TEST_DATABASE_URL=postgres://app:***@127.0.0.1:5434/agent_system_test`(configs/.env.test 对齐)
|
||||||
|
|
||||||
**判定**: SKIP 或 PASS(视 CI 是否配置隔离库)
|
**命令**:
|
||||||
|
```bash
|
||||||
|
TEST_DATABASE_URL='postgres://app:test_secret_db_pass@127.0.0.1:5434/agent_system_test' \
|
||||||
|
go test ./internal/server/... \
|
||||||
|
-run 'TestE2E_WorkflowGovernance_CompleteAndReplan|TestWorkflowGovernance_PG_ActivateApproveAdvanceChain' \
|
||||||
|
-count=1 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**实际输出**:
|
||||||
|
```
|
||||||
|
--- PASS: TestE2E_WorkflowGovernance_CompleteAndReplan (0.25s)
|
||||||
|
--- PASS: TestWorkflowGovernance_PG_ActivateApproveAdvanceChain (0.26s)
|
||||||
|
PASS ok chat-agent-system/internal/server 0.514s
|
||||||
|
```
|
||||||
|
|
||||||
|
**评价判定**: PASS — 正式工作流 `test_manager` 末阶段完成→`planning`;正式工作流 `main` 激活/DocGov 批准/阶段推进链通过
|
||||||
|
|
||||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||||
|
|
||||||
```
|
```
|
||||||
go test ./internal/projectinstance/... ./internal/coordinator/... ./internal/projectcontext/... -count=1
|
go test ./internal/projectinstance/... ./internal/coordinator/... ./internal/projectcontext/... \
|
||||||
|
./internal/ctxmgr/... ./internal/bizdev/... -count=1
|
||||||
|
TEST_DATABASE_URL=... go test ./internal/server/... -run 'TestE2E_WorkflowGovernance|TestWorkflowGovernance_PG' -count=1
|
||||||
```
|
```
|
||||||
|
|
||||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||||
|
|
||||||
- 整改代码路径已用单元测试锁定;PG 全链路需在 `test` 分支部署前用真实 PI Agent 补跑并更新本报告第五节 5.4 为 PASS 实录。
|
- Phase A–C 核心路径与 PG 治理 E2E 已通过;`test_manager`/`main` 正式工作流在隔离 test 库验证 PASS。
|
||||||
|
- 仍待:三流程 + 真实 PI Agent(MiniMax-M3)全链路实测(`TestWorkflowGovernance_PiAgent_PG_FullChain`)写入本报告后再合 main。
|
||||||
- 建议每周运行 `scripts/fix-completed-instance-lifecycle.sql` dry-run 检查历史不一致。
|
- 建议每周运行 `scripts/fix-completed-instance-lifecycle.sql` dry-run 检查历史不一致。
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package bizdev
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"chat-agent-system/internal/ctxmgr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type snapshotDialogueInputs struct {
|
||||||
|
Dialogue []string
|
||||||
|
Decisions []string
|
||||||
|
ExcludedPrivate int
|
||||||
|
ExcludedNoAccess int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) collectSnapshotDialogueInputs(ctx context.Context, projectID, userID int64) snapshotDialogueInputs {
|
||||||
|
out := snapshotDialogueInputs{}
|
||||||
|
if s == nil || projectID <= 0 || s.ctxMgr == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
if coll, err := s.getProjectContextForUser(ctx, projectID, userID); err == nil && coll != nil {
|
||||||
|
for _, d := range coll.MilestoneDecisions {
|
||||||
|
out.Decisions = append(out.Decisions, fmt.Sprintf("[%s] %s (%s)", d.ActivityType, d.Title, d.UserName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogue, err := s.ctxMgr.CollectProjectDialogue(ctx, projectID, ctxmgr.CollectProjectDialogueOptions{
|
||||||
|
Limit: 120,
|
||||||
|
})
|
||||||
|
if err != nil || dialogue == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
out.Dialogue = dialogue.Lines
|
||||||
|
out.ExcludedPrivate = dialogue.ExcludedPrivate
|
||||||
|
out.ExcludedNoAccess = dialogue.ExcludedNoAccess
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package bizdev
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chat-agent-system/internal/ctxmgr"
|
||||||
|
"chat-agent-system/internal/projectcontext"
|
||||||
|
"chat-agent-system/internal/replyexport"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCollectSnapshotDialogueInputs_FeedsBuildSnapshot(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
mgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig())
|
||||||
|
projectID := int64(601)
|
||||||
|
meta := map[string]any{"project_id": projectID}
|
||||||
|
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-601", 1, "user", "测试经理需要覆盖注册分类路径", "", "", meta)
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-601", 1, "assistant", "我会整理测试验证范围与风险点。", "", "", map[string]any{
|
||||||
|
"project_id": projectID,
|
||||||
|
"export_policy": string(replyexport.PolicyFull),
|
||||||
|
"content_kind": string(replyexport.KindDocumentText),
|
||||||
|
})
|
||||||
|
_, _ = mgr.RecordActivity(ctx, projectID, 1, "张工", "decision", "确认先做注册分类调研", map[string]any{}, 1, true)
|
||||||
|
|
||||||
|
svc := &Service{ctxMgr: mgr}
|
||||||
|
inputs := svc.collectSnapshotDialogueInputs(ctx, projectID, 1)
|
||||||
|
if len(inputs.Dialogue) == 0 {
|
||||||
|
t.Fatalf("expected dialogue lines, got none")
|
||||||
|
}
|
||||||
|
if len(inputs.Decisions) != 1 {
|
||||||
|
t.Fatalf("expected 1 decision, got %d", len(inputs.Decisions))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxSvc := projectcontext.NewService(nil)
|
||||||
|
snap, err := ctxSvc.BuildSnapshot(ctx, projectcontext.BuildInput{
|
||||||
|
ProjectID: projectID,
|
||||||
|
TaskKey: "test_manager",
|
||||||
|
Dialogue: inputs.Dialogue,
|
||||||
|
Decisions: inputs.Decisions,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildSnapshot: %v", err)
|
||||||
|
}
|
||||||
|
if snap.SourceCount < 2 {
|
||||||
|
t.Fatalf("expected dialogue + decision sources, got %d", snap.SourceCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -378,12 +378,17 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dialogueInputs := s.collectSnapshotDialogueInputs(ctx, req.ProjectID, req.UserID)
|
||||||
ctxSvc := projectcontext.NewService(s.pool)
|
ctxSvc := projectcontext.NewService(s.pool)
|
||||||
snap, _ := ctxSvc.BuildSnapshot(ctx, projectcontext.BuildInput{
|
snap, _ := ctxSvc.BuildSnapshot(ctx, projectcontext.BuildInput{
|
||||||
ProjectID: req.ProjectID, TaskKey: req.TaskKey, UserID: req.UserID,
|
ProjectID: req.ProjectID, TaskKey: req.TaskKey, UserID: req.UserID,
|
||||||
ProjectName: req.ProjectName, Facts: facts, Materials: materials, PriorDocs: priorDocs,
|
ProjectName: req.ProjectName, Facts: facts, Materials: materials, PriorDocs: priorDocs,
|
||||||
Knowledge: hits,
|
Knowledge: hits, Dialogue: dialogueInputs.Dialogue, Decisions: dialogueInputs.Decisions,
|
||||||
})
|
})
|
||||||
|
if snap != nil {
|
||||||
|
snap.ExcludedPrivateCount += dialogueInputs.ExcludedPrivate
|
||||||
|
snap.ExcludedNoAccessCount += dialogueInputs.ExcludedNoAccess
|
||||||
|
}
|
||||||
if snap != nil {
|
if snap != nil {
|
||||||
if extra := projectcontext.MaterialsForWorkspace(snap, 12); len(extra) > 0 {
|
if extra := projectcontext.MaterialsForWorkspace(snap, 12); len(extra) > 0 {
|
||||||
materials = append(materials, extra...)
|
materials = append(materials, extra...)
|
||||||
|
|||||||
@@ -226,6 +226,13 @@ func (m *ContextManager) RecordUserMessageWithID(ctx context.Context, sessionID
|
|||||||
}
|
}
|
||||||
m.sessions[sessionID] = sessSum
|
m.sessions[sessionID] = sessSum
|
||||||
}
|
}
|
||||||
|
if meta != nil {
|
||||||
|
if pVal, ok := meta["project_id"].(int64); ok && pVal > 0 {
|
||||||
|
sessSum.ProjectID = pVal
|
||||||
|
} else if pValFloat, ok := meta["project_id"].(float64); ok && pValFloat > 0 {
|
||||||
|
sessSum.ProjectID = int64(pValFloat)
|
||||||
|
}
|
||||||
|
}
|
||||||
sessSum.MessageCount = len(m.messages[sessionID])
|
sessSum.MessageCount = len(m.messages[sessionID])
|
||||||
sessSum.LastMessage = content
|
sessSum.LastMessage = content
|
||||||
sessSum.UpdatedAt = time.Now()
|
sessSum.UpdatedAt = time.Now()
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package ctxmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chat-agent-system/internal/replyexport"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultProjectDialogueLimit = 120
|
||||||
|
|
||||||
|
// ProjectDialogueCollectResult 项目级对话采集结果(供上下文快照使用)。
|
||||||
|
type ProjectDialogueCollectResult struct {
|
||||||
|
Lines []string
|
||||||
|
ExcludedPrivate int
|
||||||
|
ExcludedNoAccess int
|
||||||
|
AllowedStageKey string
|
||||||
|
StageFilteredCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectProjectDialogueOptions 采集选项。
|
||||||
|
type CollectProjectDialogueOptions struct {
|
||||||
|
Limit int
|
||||||
|
AllowedStageKey string // 非空时仅保留无 stage_key 或匹配的对话
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectProjectDialogue 收集进入项目知识边界的共享对话行。
|
||||||
|
func (m *ContextManager) CollectProjectDialogue(ctx context.Context, projectID int64, opts CollectProjectDialogueOptions) (*ProjectDialogueCollectResult, error) {
|
||||||
|
out := &ProjectDialogueCollectResult{AllowedStageKey: opts.AllowedStageKey}
|
||||||
|
if m == nil || projectID <= 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
limit := opts.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = defaultProjectDialogueLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
var msgs []ChatMessageRecord
|
||||||
|
if m.pool != nil {
|
||||||
|
rows, err := m.pool.Query(ctx, `
|
||||||
|
SELECT cm.id, cm.session_id, COALESCE(cm.user_id, 0), cm.role, cm.content,
|
||||||
|
cm.intent, cm.branch, cm.metadata, COALESCE(cm.day_key::text, ''),
|
||||||
|
COALESCE(cm.category, ''), COALESCE(cm.content_kind, ''),
|
||||||
|
COALESCE(cm.export_policy, 'none'), cm.created_at
|
||||||
|
FROM chat_messages cm
|
||||||
|
JOIN chat_sessions cs ON cs.session_id = cm.session_id
|
||||||
|
WHERE cs.project_id = $1 AND cm.deleted_at IS NULL
|
||||||
|
ORDER BY cm.created_at ASC
|
||||||
|
LIMIT $2
|
||||||
|
`, projectID, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("collect project dialogue: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var rec ChatMessageRecord
|
||||||
|
var metaJSON []byte
|
||||||
|
if err := rows.Scan(
|
||||||
|
&rec.ID, &rec.SessionID, &rec.UserID, &rec.Role, &rec.Content,
|
||||||
|
&rec.Intent, &rec.Branch, &metaJSON, &rec.DayKey, &rec.Category,
|
||||||
|
&rec.ContentKind, &rec.ExportPolicy, &rec.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(metaJSON) > 0 {
|
||||||
|
_ = json.Unmarshal(metaJSON, &rec.Metadata)
|
||||||
|
}
|
||||||
|
msgs = append(msgs, rec)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.mu.RLock()
|
||||||
|
for sessionID, sessionMsgs := range m.messages {
|
||||||
|
sess := m.sessions[sessionID]
|
||||||
|
if sess != nil && sess.ProjectID != projectID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, rec := range sessionMsgs {
|
||||||
|
if sess == nil || sess.ProjectID == 0 {
|
||||||
|
if pid := messageProjectID(rec); pid != projectID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msgs = append(msgs, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range msgs {
|
||||||
|
line, include, excludedPrivate, excludedNoAccess := classifyDialogueLine(rec, opts.AllowedStageKey)
|
||||||
|
if excludedPrivate {
|
||||||
|
out.ExcludedPrivate++
|
||||||
|
}
|
||||||
|
if excludedNoAccess {
|
||||||
|
out.ExcludedNoAccess++
|
||||||
|
out.StageFilteredCount++
|
||||||
|
}
|
||||||
|
if !include || line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.Lines = append(out.Lines, line)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageProjectID(rec ChatMessageRecord) int64 {
|
||||||
|
if rec.Metadata == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if pVal, ok := rec.Metadata["project_id"].(int64); ok {
|
||||||
|
return pVal
|
||||||
|
}
|
||||||
|
if pVal, ok := rec.Metadata["project_id"].(float64); ok {
|
||||||
|
return int64(pVal)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyDialogueLine(rec ChatMessageRecord, allowedStageKey string) (line string, include bool, excludedPrivate bool, excludedNoAccess bool) {
|
||||||
|
content := strings.TrimSpace(rec.Content)
|
||||||
|
if content == "" {
|
||||||
|
return "", false, true, false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(content, "[private]") || strings.HasPrefix(content, "[draft]") {
|
||||||
|
return "", false, true, false
|
||||||
|
}
|
||||||
|
if vis, _ := rec.Metadata["visibility"].(string); vis == "private" {
|
||||||
|
return "", false, true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
stageKey := MessageStageKey(rec.Metadata)
|
||||||
|
if allowedStageKey != "" && stageKey != "" && stageKey != allowedStageKey {
|
||||||
|
return "", false, false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rec.Role {
|
||||||
|
case "user":
|
||||||
|
line = fmt.Sprintf("用户: %s", content)
|
||||||
|
return line, true, false, false
|
||||||
|
case "assistant":
|
||||||
|
switch rec.ContentKind {
|
||||||
|
case string(replyexport.KindActionCard), string(replyexport.KindStatusUpdate),
|
||||||
|
string(replyexport.KindErrorNotice), string(replyexport.KindSystemNotice):
|
||||||
|
return "", false, true, false
|
||||||
|
}
|
||||||
|
switch rec.ExportPolicy {
|
||||||
|
case string(replyexport.PolicyFull), string(replyexport.PolicyCopyOnly):
|
||||||
|
line = fmt.Sprintf("助手: %s", content)
|
||||||
|
return line, true, false, false
|
||||||
|
default:
|
||||||
|
return "", false, true, false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", false, true, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageStageKey 从消息 metadata 提取阶段键。
|
||||||
|
func MessageStageKey(metadata map[string]any) string {
|
||||||
|
if metadata == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if sk, ok := metadata["stage_key"].(string); ok {
|
||||||
|
return sk
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package ctxmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chat-agent-system/internal/replyexport"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCollectProjectDialogue_ExcludesPrivateAndIncludesShared(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
||||||
|
projectID := int64(501)
|
||||||
|
meta := map[string]any{"project_id": projectID}
|
||||||
|
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "user", "[private] 不应纳入", "", "", meta)
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "user", "项目需要支持多用户协作", "", "", meta)
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "assistant", "好的,我会整理需求要点。", "", "", map[string]any{
|
||||||
|
"project_id": projectID,
|
||||||
|
"export_policy": string(replyexport.PolicyFull),
|
||||||
|
"content_kind": string(replyexport.KindDocumentText),
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := mgr.CollectProjectDialogue(ctx, projectID, CollectProjectDialogueOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CollectProjectDialogue: %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Lines) != 2 {
|
||||||
|
t.Fatalf("expected 2 shared lines, got %d: %v", len(res.Lines), res.Lines)
|
||||||
|
}
|
||||||
|
if res.ExcludedPrivate < 1 {
|
||||||
|
t.Fatalf("expected excluded private >= 1, got %d", res.ExcludedPrivate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectProjectDialogue_StageFilter(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
||||||
|
projectID := int64(502)
|
||||||
|
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-502", 1, "user", "立项阶段讨论", "", "", map[string]any{
|
||||||
|
"project_id": projectID, "stage_key": "biz_intake",
|
||||||
|
})
|
||||||
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-502", 1, "user", "研发阶段讨论", "", "", map[string]any{
|
||||||
|
"project_id": projectID, "stage_key": "biz_feasibility",
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := mgr.CollectProjectDialogue(ctx, projectID, CollectProjectDialogueOptions{
|
||||||
|
AllowedStageKey: "biz_intake",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CollectProjectDialogue: %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Lines) != 1 {
|
||||||
|
t.Fatalf("expected 1 stage-filtered line, got %d: %v", len(res.Lines), res.Lines)
|
||||||
|
}
|
||||||
|
if res.ExcludedNoAccess != 1 {
|
||||||
|
t.Fatalf("expected 1 excluded by stage, got %d", res.ExcludedNoAccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,36 +26,17 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) {
|
|||||||
defer pool.Close()
|
defer pool.Close()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
handler, _ := setupPGGovernanceServer(t, pool)
|
handler, srv := setupPGGovernanceServer(t, pool)
|
||||||
|
|
||||||
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
||||||
creatorID := fetchAuthUserID(t, handler, creatorToken)
|
creatorID := fetchAuthUserID(t, handler, creatorToken)
|
||||||
|
|
||||||
wfSvc := workflowdef.NewService(pool, nil)
|
wfKey := workflowdef.OfficialKeyTestManager
|
||||||
wfKey := "e2e_gov_" + time.Now().Format("150405.000")
|
seedOfficialWorkflowManifest(t, pool, wfKey)
|
||||||
_, ver, err := wfSvc.CreateDefinition(ctx, workflowdef.CreateDefinitionInput{
|
|
||||||
Key: wfKey, Name: "E2E Governance WF",
|
|
||||||
Definition: workflowdef.VersionDefinition{
|
|
||||||
SchemaVersion: 1,
|
|
||||||
Stages: []workflowdef.StageDef{{
|
|
||||||
Key: "only_stage", Name: "唯一阶段", Position: 0,
|
|
||||||
DocTasks: []workflowdef.DocTaskDef{{
|
|
||||||
Key: "intake", Required: true, Blocking: true, DefaultEnabled: true,
|
|
||||||
OutputDocType: "intake", OutputDocTitle: "Intake",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create workflow: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := wfSvc.PublishVersion(ctx, ver.WorkflowID, ver.Version, 1); err != nil {
|
|
||||||
t.Fatalf("publish workflow: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectID int64
|
var projectID int64
|
||||||
code := "e2e-gov-" + time.Now().Format("150405.000")
|
code := "e2e-gov-" + time.Now().Format("150405.000")
|
||||||
err = pool.QueryRow(ctx, `
|
err := pool.QueryRow(ctx, `
|
||||||
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
|
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
|
||||||
VALUES ($1, 'E2E Governance', 'active', $2, 'planning', '', 1, 0) RETURNING id
|
VALUES ($1, 'E2E Governance', 'active', $2, 'planning', '', 1, 0) RETURNING id
|
||||||
`, code, creatorID).Scan(&projectID)
|
`, code, creatorID).Scan(&projectID)
|
||||||
@@ -66,7 +47,7 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) {
|
|||||||
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
|
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
|
||||||
})
|
})
|
||||||
|
|
||||||
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=1", projectID, wfKey), nil)
|
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", projectID, wfKey), nil)
|
||||||
withBearerAuth(reqDefault, creatorToken)
|
withBearerAuth(reqDefault, creatorToken)
|
||||||
recDefault := httptest.NewRecorder()
|
recDefault := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(recDefault, reqDefault)
|
handler.ServeHTTP(recDefault, reqDefault)
|
||||||
@@ -114,23 +95,25 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) {
|
|||||||
var activated projectinstance.ActivateResult
|
var activated projectinstance.ActivateResult
|
||||||
_ = json.Unmarshal(recActivate.Body.Bytes(), &activated)
|
_ = json.Unmarshal(recActivate.Body.Bytes(), &activated)
|
||||||
|
|
||||||
// 批准并推进至完成(复用 governance 测试中的 docgov 路径)
|
docID := fmt.Sprintf("doc_proj%d_test_manager", projectID)
|
||||||
docID := fmt.Sprintf("doc_e2e_%d", projectID)
|
docEngine := srv.BizdevService().DocGov()
|
||||||
// 文档批准链在 workflow_governance_pg_test 已覆盖;此处直接标记实例文档 approved 并推进
|
_, err = docEngine.CreateOrGetDocument(ctx, docID, projectID, "test_manager", "系统验证方案及计划", "system_verification_planning", "",
|
||||||
_, err = pool.Exec(ctx, `
|
creatorID, "Author", reviewerID, "Reviewer", approverID, "Approver")
|
||||||
UPDATE project_doc_task_instances dti SET status = 'approved', updated_at = now()
|
|
||||||
FROM project_stage_instances psi
|
|
||||||
WHERE dti.stage_instance_id = psi.id AND psi.workflow_instance_id = $1 AND dti.task_key = 'intake'
|
|
||||||
`, activated.Instance.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mark approved: %v", err)
|
t.Fatalf("create doc: %v", err)
|
||||||
|
}
|
||||||
|
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_e2e_te", false)
|
||||||
|
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
|
||||||
|
_, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok")
|
||||||
|
_, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("approve doc: %v", err)
|
||||||
}
|
}
|
||||||
_ = docID
|
|
||||||
|
|
||||||
advanceBody, _ := json.Marshal(projectinstance.AdvanceStageInput{
|
advanceBody, _ := json.Marshal(projectinstance.AdvanceStageInput{
|
||||||
StageKey: "only_stage", ExpectedConfigRevision: activated.Instance.ConfigRevision,
|
StageKey: "system_verification_planning", ExpectedConfigRevision: activated.Instance.ConfigRevision,
|
||||||
})
|
})
|
||||||
reqAdvance := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/workflow-instances/%d/stages/only_stage/advance", activated.Instance.ID), bytes.NewReader(advanceBody))
|
reqAdvance := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/workflow-instances/%d/stages/system_verification_planning/advance", activated.Instance.ID), bytes.NewReader(advanceBody))
|
||||||
withBearerAuth(reqAdvance, creatorToken)
|
withBearerAuth(reqAdvance, creatorToken)
|
||||||
recAdvance := httptest.NewRecorder()
|
recAdvance := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(recAdvance, reqAdvance)
|
handler.ServeHTTP(recAdvance, reqAdvance)
|
||||||
|
|||||||
@@ -2,13 +2,24 @@ package server_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"chat-agent-system/internal/auth"
|
"chat-agent-system/internal/auth"
|
||||||
"chat-agent-system/internal/config"
|
"chat-agent-system/internal/config"
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
|
"chat-agent-system/internal/workflowdef"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loginTestAdmin(t *testing.T, handler http.Handler, cfg *config.Config) string {
|
func loginTestAdmin(t *testing.T, handler http.Handler, cfg *config.Config) string {
|
||||||
@@ -67,3 +78,63 @@ func loginTestUser(t *testing.T, handler http.Handler, username, password string
|
|||||||
func withUserIDAuth(req *http.Request, token string) {
|
func withUserIDAuth(req *http.Request, token string) {
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configFromTestDatabaseURL(t *testing.T) *config.Config {
|
||||||
|
t.Helper()
|
||||||
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
|
parsed, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse TEST_DATABASE_URL: %v", err)
|
||||||
|
}
|
||||||
|
host, portStr, splitErr := net.SplitHostPort(parsed.Host)
|
||||||
|
if splitErr != nil {
|
||||||
|
host = parsed.Host
|
||||||
|
portStr = "5432"
|
||||||
|
}
|
||||||
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
user := parsed.User.Username()
|
||||||
|
password, _ := parsed.User.Password()
|
||||||
|
dbName := strings.TrimPrefix(parsed.Path, "/")
|
||||||
|
return &config.Config{
|
||||||
|
Env: "test",
|
||||||
|
DataDir: t.TempDir(),
|
||||||
|
AgentBackend: "stub",
|
||||||
|
AllowInMemory: false,
|
||||||
|
ContextDbHost: host,
|
||||||
|
ContextDbPort: port,
|
||||||
|
ContextDbUser: user,
|
||||||
|
ContextDbPassword: password,
|
||||||
|
ContextDbName: dbName,
|
||||||
|
AdminUsername: envOr("ADMIN_USERNAME", "admin"),
|
||||||
|
AdminPassword: envOr("ADMIN_PASSWORD", "admin-secret-password-123"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, fallback string) string {
|
||||||
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedOfficialWorkflowManifest(t *testing.T, pool *pgxpool.Pool, workflowKey string) {
|
||||||
|
t.Helper()
|
||||||
|
filename, ok := workflowdef.V2ManifestFiles[workflowKey]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unknown official workflow key %q", workflowKey)
|
||||||
|
}
|
||||||
|
def, err := workflowdef.LoadManifestDefinition(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load manifest %s: %v", filename, err)
|
||||||
|
}
|
||||||
|
wfStore := workflowdef.NewStore(pool)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := wfStore.UpsertPublishedSeed(ctx, workflowdef.CreateDefinitionInput{
|
||||||
|
Key: workflowKey,
|
||||||
|
Name: workflowdef.OfficialWorkflowName(workflowKey),
|
||||||
|
Definition: def,
|
||||||
|
}, 1); err != nil {
|
||||||
|
t.Fatalf("seed official workflow %s: %v", workflowKey, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
|
|
||||||
"chat-agent-system/internal/auth"
|
"chat-agent-system/internal/auth"
|
||||||
"chat-agent-system/internal/bizdev"
|
"chat-agent-system/internal/bizdev"
|
||||||
"chat-agent-system/internal/config"
|
|
||||||
"chat-agent-system/internal/db"
|
"chat-agent-system/internal/db"
|
||||||
"chat-agent-system/internal/notification"
|
"chat-agent-system/internal/notification"
|
||||||
"chat-agent-system/internal/projectinstance"
|
"chat-agent-system/internal/projectinstance"
|
||||||
@@ -49,9 +48,10 @@ func openPGPoolWithMigrations(t *testing.T) *pgxpool.Pool {
|
|||||||
t.Skip("workflow/project instance tables not ready")
|
t.Skip("workflow/project instance tables not ready")
|
||||||
}
|
}
|
||||||
if auth.IAMTablesReady(ctx, pool) {
|
if auth.IAMTablesReady(ctx, pool) {
|
||||||
authStore, err := auth.NewPgStore(pool, &config.Config{Env: "dev"})
|
testCfg := configFromTestDatabaseURL(t)
|
||||||
|
authStore, err := auth.NewPgStore(pool, testCfg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_ = auth.ReactivateDevSeedUsers(ctx, authStore, &config.Config{Env: "dev"})
|
_ = auth.ReactivateDevSeedUsers(ctx, authStore, testCfg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pool
|
return pool
|
||||||
@@ -82,19 +82,7 @@ func fetchAuthUserID(t *testing.T, handler http.Handler, token string) int64 {
|
|||||||
|
|
||||||
func setupPGGovernanceServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *server.Server) {
|
func setupPGGovernanceServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *server.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg := &config.Config{
|
cfg := configFromTestDatabaseURL(t)
|
||||||
Env: "dev",
|
|
||||||
DataDir: t.TempDir(),
|
|
||||||
AgentBackend: "stub",
|
|
||||||
AllowInMemory: false,
|
|
||||||
ContextDbHost: "127.0.0.1",
|
|
||||||
ContextDbPort: 5433,
|
|
||||||
ContextDbUser: "app",
|
|
||||||
ContextDbPassword: "dev_secret_db_pass",
|
|
||||||
ContextDbName: "agent_system_dev",
|
|
||||||
AdminUsername: "admin",
|
|
||||||
AdminPassword: "admin-secret-password-123",
|
|
||||||
}
|
|
||||||
svc := bizdev.MustNewService(t, cfg)
|
svc := bizdev.MustNewService(t, cfg)
|
||||||
if svc.Pool() == nil {
|
if svc.Pool() == nil {
|
||||||
t.Skip("bizdev service not connected to postgres")
|
t.Skip("bizdev service not connected to postgres")
|
||||||
@@ -113,36 +101,8 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
handler, srv := setupPGGovernanceServer(t, pool)
|
handler, srv := setupPGGovernanceServer(t, pool)
|
||||||
|
|
||||||
wfSvc := workflowdef.NewService(pool, nil)
|
wfKey := workflowdef.OfficialKeyMain
|
||||||
wfKey := "pg_p5_gov_" + time.Now().Format("150405.000")
|
seedOfficialWorkflowManifest(t, pool, wfKey)
|
||||||
_, ver, err := wfSvc.CreateDefinition(ctx, workflowdef.CreateDefinitionInput{
|
|
||||||
Key: wfKey, Name: "PG Phase5 Governance WF",
|
|
||||||
Definition: workflowdef.VersionDefinition{
|
|
||||||
SchemaVersion: 1,
|
|
||||||
Stages: []workflowdef.StageDef{
|
|
||||||
{
|
|
||||||
Key: "biz_intake", Name: "商务摸底", Position: 0,
|
|
||||||
DocTasks: []workflowdef.DocTaskDef{{
|
|
||||||
Key: "intake", Required: true, Blocking: true, DefaultEnabled: true,
|
|
||||||
OutputDocType: "intake", OutputDocTitle: "客户需求采集",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "biz_feasibility", Name: "法规可行性", Position: 1, Optional: true,
|
|
||||||
DocTasks: []workflowdef.DocTaskDef{{
|
|
||||||
Key: "feasibility_reg", Required: false, Blocking: false, DefaultEnabled: true,
|
|
||||||
OutputDocType: "feasibility", OutputDocTitle: "法规路径评估",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create workflow def: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := wfSvc.PublishVersion(ctx, ver.WorkflowID, ver.Version, 1); err != nil {
|
|
||||||
t.Fatalf("publish workflow: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
||||||
guestToken := loginTestUser(t, handler, "guest", "123456")
|
guestToken := loginTestUser(t, handler, "guest", "123456")
|
||||||
@@ -151,6 +111,7 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
|
|||||||
// 1. 创建项目(PG 直写,与 projectinstance PG 集成测试一致)
|
// 1. 创建项目(PG 直写,与 projectinstance PG 集成测试一致)
|
||||||
var projectID int64
|
var projectID int64
|
||||||
code := "pg-p5-" + time.Now().Format("150405.000")
|
code := "pg-p5-" + time.Now().Format("150405.000")
|
||||||
|
var err error
|
||||||
err = pool.QueryRow(ctx, `
|
err = pool.QueryRow(ctx, `
|
||||||
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
|
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
|
||||||
VALUES ($1, 'PG Phase5 Governance Test', 'active', $2, 'planning', '', 1, 0) RETURNING id
|
VALUES ($1, 'PG Phase5 Governance Test', 'active', $2, 'planning', '', 1, 0) RETURNING id
|
||||||
@@ -170,7 +131,7 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
|
|||||||
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
|
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
|
||||||
})
|
})
|
||||||
// 2. 获取默认规划并保存三权
|
// 2. 获取默认规划并保存三权
|
||||||
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=1", projectID, wfKey), nil)
|
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", projectID, wfKey), nil)
|
||||||
withBearerAuth(reqDefault, creatorToken)
|
withBearerAuth(reqDefault, creatorToken)
|
||||||
recDefault := httptest.NewRecorder()
|
recDefault := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(recDefault, reqDefault)
|
handler.ServeHTTP(recDefault, reqDefault)
|
||||||
@@ -282,10 +243,10 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
|
|||||||
|
|
||||||
// 7. 创建者推进阶段
|
// 7. 创建者推进阶段
|
||||||
advanceBody, _ := json.Marshal(projectinstance.AdvanceStageInput{
|
advanceBody, _ := json.Marshal(projectinstance.AdvanceStageInput{
|
||||||
StageKey: "biz_intake",
|
StageKey: "V0.1_triage",
|
||||||
ExpectedConfigRevision: activated.Instance.ConfigRevision,
|
ExpectedConfigRevision: activated.Instance.ConfigRevision,
|
||||||
})
|
})
|
||||||
reqAdvance := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/workflow-instances/%d/stages/biz_intake/advance", instanceID), bytes.NewReader(advanceBody))
|
reqAdvance := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/workflow-instances/%d/stages/V0.1_triage/advance", instanceID), bytes.NewReader(advanceBody))
|
||||||
withBearerAuth(reqAdvance, creatorToken)
|
withBearerAuth(reqAdvance, creatorToken)
|
||||||
recAdvance := httptest.NewRecorder()
|
recAdvance := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(recAdvance, reqAdvance)
|
handler.ServeHTTP(recAdvance, reqAdvance)
|
||||||
@@ -296,7 +257,7 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
|
|||||||
if err := json.Unmarshal(recAdvance.Body.Bytes(), &advanceResult); err != nil {
|
if err := json.Unmarshal(recAdvance.Body.Bytes(), &advanceResult); err != nil {
|
||||||
t.Fatalf("decode advance: %v", err)
|
t.Fatalf("decode advance: %v", err)
|
||||||
}
|
}
|
||||||
if advanceResult.NextStageKey != "biz_feasibility" && !advanceResult.InstanceDone {
|
if advanceResult.NextStageKey != "V0.5_feasibility" && !advanceResult.InstanceDone {
|
||||||
t.Fatalf("unexpected advance result: %+v", advanceResult)
|
t.Fatalf("unexpected advance result: %+v", advanceResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"chat-agent-system/internal/bizdev"
|
"chat-agent-system/internal/bizdev"
|
||||||
"chat-agent-system/internal/config"
|
|
||||||
"chat-agent-system/internal/notification"
|
"chat-agent-system/internal/notification"
|
||||||
"chat-agent-system/internal/projectinstance"
|
"chat-agent-system/internal/projectinstance"
|
||||||
"chat-agent-system/internal/projectworkflow"
|
"chat-agent-system/internal/projectworkflow"
|
||||||
@@ -34,22 +33,11 @@ func piAgentRunnerHealthy() bool {
|
|||||||
|
|
||||||
func setupPGPiAgentServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *server.Server) {
|
func setupPGPiAgentServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *server.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg := &config.Config{
|
cfg := configFromTestDatabaseURL(t)
|
||||||
Env: "dev",
|
cfg.AgentBackend = "pi-agent"
|
||||||
DataDir: t.TempDir(),
|
cfg.PiAgentRunnerURL = "http://127.0.0.1:18865"
|
||||||
AgentBackend: "pi-agent",
|
cfg.PiAgentModel = "MiniMax-M3"
|
||||||
PiAgentRunnerURL: "http://127.0.0.1:18865",
|
cfg.PiAgentProvider = "minimax"
|
||||||
PiAgentModel: "MiniMax-M3",
|
|
||||||
PiAgentProvider: "minimax",
|
|
||||||
AllowInMemory: false,
|
|
||||||
ContextDbHost: "127.0.0.1",
|
|
||||||
ContextDbPort: 5433,
|
|
||||||
ContextDbUser: "app",
|
|
||||||
ContextDbPassword: "dev_secret_db_pass",
|
|
||||||
ContextDbName: "agent_system_dev",
|
|
||||||
AdminUsername: "admin",
|
|
||||||
AdminPassword: "admin-secret-password-123",
|
|
||||||
}
|
|
||||||
svc := bizdev.MustNewService(t, cfg)
|
svc := bizdev.MustNewService(t, cfg)
|
||||||
if svc.Pool() == nil {
|
if svc.Pool() == nil {
|
||||||
t.Skip("bizdev service not connected to postgres")
|
t.Skip("bizdev service not connected to postgres")
|
||||||
|
|||||||
Reference in New Issue
Block a user