package docgov_test import ( "context" "fmt" "testing" "time" "chat-agent-system/internal/db" "chat-agent-system/internal/docgov" "github.com/jackc/pgx/v5/pgxpool" ) // openIsolatedPool 仅连接显式提供的隔离测试库(TEST_DATABASE_URL), // 禁止回退到共享环境库(agent_system_dev/test/main)。 func openIsolatedPool(t *testing.T) *pgxpool.Pool { t.Helper() dsn := db.SkipIfNoTestDatabaseURL(t) db.RefuseSharedEnvDSN(t, dsn) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Skipf("cannot connect to isolated test db: %v", err) } if err := pool.Ping(ctx); err != nil { pool.Close() t.Skipf("isolated test db ping failed: %v", err) } if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { pool.Close() t.Skipf("migration failed: %v", err) } return pool } // seedTestProject 创建本测试专属项目行,满足 document_records.project_id 外键。 func seedTestProject(t *testing.T, pool *pgxpool.Pool) int64 { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() stamp := time.Now().Format("20060102150405.000000") var projectID int64 err := pool.QueryRow(ctx, ` INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status) VALUES ('docgov-' || $1, 'DocGov Engine Test ' || $1, 'active', 2, 'active') RETURNING id `, stamp).Scan(&projectID) if err != nil { t.Fatalf("seed project: %v", err) } t.Cleanup(func() { cleanupCtx := context.Background() _, _ = pool.Exec(cleanupCtx, `DELETE FROM document_comments WHERE doc_id IN (SELECT doc_id FROM document_records WHERE project_id = $1)`, projectID) _, _ = pool.Exec(cleanupCtx, `DELETE FROM document_change_proposals WHERE doc_id IN (SELECT doc_id FROM document_records WHERE project_id = $1)`, projectID) _, _ = pool.Exec(cleanupCtx, `DELETE FROM document_records WHERE project_id = $1`, projectID) _, _ = pool.Exec(cleanupCtx, `DELETE FROM projects WHERE id = $1`, projectID) }) return projectID } func TestDocGov_DatabaseIntegration(t *testing.T) { pool := openIsolatedPool(t) defer pool.Close() projectID := seedTestProject(t, pool) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() engine := docgov.NewGovernanceEngine(pool) testDocID := fmt.Sprintf("doc_test_db_%d", time.Now().UnixNano()) // 1. 创建受控文档 doc, err := engine.CreateOrGetDocument( ctx, testDocID, projectID, "feasibility_reg", "DB集成测试文档", "V0.5_feasibility", "data/dev/workspaces/test_db", 2, "张工", 3, "李主管", 1, "Seed Admin", ) if err != nil { t.Fatalf("CreateOrGetDocument in DB failed: %v", err) } if doc.DocID != testDocID { t.Fatalf("unexpected docID: %s", testDocID) } // 2. 标记正文非模板并认领 _ = engine.MarkDocumentContentUpdated(ctx, testDocID, "db_test_sha256", false) doc, err = engine.RecordDocumentViewed(ctx, testDocID, 2, docgov.RoleAuthor, "张工") if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } if !doc.AuthorClaimed { t.Fatalf("expected author_claimed in DB") } // 3. 生成变更提案 prop, err := engine.ProposeModification( ctx, testDocID, "agent_squad", "测试修改提要", "测试影响分析", "测试潜在风险", ) if err != nil { t.Fatalf("ProposeModification in DB failed: %v", err) } // 4. 拍板同意提案 prop, err = engine.DecideModificationProposal(ctx, prop.ProposalID, 2, true, "DB测试同意") if err != nil { t.Fatalf("DecideModificationProposal in DB failed: %v", err) } if prop.Status != docgov.ProposalApprovedByAuthor { t.Fatalf("expected proposal approved, got %s", prop.Status) } // 5. 批注与送审流程 _, err = engine.AddComment(ctx, testDocID, 3, "李主管", docgov.RoleReviewer, "## 结论", docgov.CommentSuggestion, "请增加法规出处") if err != nil { t.Fatalf("AddComment in DB failed: %v", err) } doc, err = engine.SubmitForReview(ctx, testDocID, 2, docgov.RoleAuthor, "张工") if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } if !doc.IsLocked { t.Fatalf("expected doc locked in reviewing") } } // TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK ensures reviewer/approver=0 // persist as SQL NULL (optional DutyMode) instead of FK-violating 0. func TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK(t *testing.T) { pool := openIsolatedPool(t) defer pool.Close() projectID := seedTestProject(t, pool) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() engine := docgov.NewGovernanceEngine(pool) testDocID := fmt.Sprintf("doc_opt_duty_%d", time.Now().UnixNano()) doc, err := engine.CreateOrGetDocument( ctx, testDocID, projectID, "intake", "可选职责空审核人", "V0.5_feasibility", "", 2, "张工", 0, "", 0, "", ) if err != nil { t.Fatalf("CreateOrGetDocument with reviewer/approver=0 failed: %v", err) } if doc.DocID != testDocID { t.Fatalf("unexpected docID: %s", testDocID) } var reviewerNull, approverNull bool var gotAuthor int64 err = pool.QueryRow(ctx, ` SELECT author_id, reviewer_id IS NULL, approver_id IS NULL FROM document_records WHERE doc_id = $1 `, testDocID).Scan(&gotAuthor, &reviewerNull, &approverNull) if err != nil { t.Fatalf("query document_records: %v", err) } if gotAuthor != 2 { t.Fatalf("author_id=%d want 2", gotAuthor) } if !reviewerNull || !approverNull { t.Fatalf("expected NULL reviewer/approver, got reviewerNull=%v approverNull=%v", reviewerNull, approverNull) } }