diff --git a/internal/docgov/materialize_test.go b/internal/docgov/materialize_test.go index 56cf21b2..c501b13b 100644 --- a/internal/docgov/materialize_test.go +++ b/internal/docgov/materialize_test.go @@ -65,3 +65,29 @@ func TestMaterializeTemplateDocsIdempotentAndSkipsRealContent(t *testing.T) { t.Fatalf("real document was overwritten: %+v", got) } } + +func TestMaterializeTemplateDocsSkipsCanonicalSibling(t *testing.T) { + ctx := context.Background() + engine := docgov.NewGovernanceEngine(nil) + if _, err := engine.CreateOrGetDocument( + ctx, "doc_proj70_intake", 70, "intake", "用户初始需求收集表", "V0.1", "", + 1, "编制人", 0, "", 0, "", + ); err != nil { + t.Fatal(err) + } + if err := engine.MaterializeTemplateDocs(ctx, 70, []docgov.TemplateDocSpec{{ + TaskKey: "bd_intake", + Title: "用户初始需求收集表", + StageKey: "V0.1_triage", + Enabled: true, + }}); err != nil { + t.Fatal(err) + } + docs, err := engine.ListWorkflowDocumentsByProject(ctx, 70) + if err != nil { + t.Fatal(err) + } + if len(docs) != 1 || docs[0].DocID != "doc_proj70_intake" || docs[0].TaskKey != "intake" { + t.Fatalf("sibling bd_intake was inserted: %+v", docs) + } +} diff --git a/internal/docgov/project_docs_test.go b/internal/docgov/project_docs_test.go index 560559d7..64f9568b 100644 --- a/internal/docgov/project_docs_test.go +++ b/internal/docgov/project_docs_test.go @@ -79,3 +79,41 @@ func TestListWorkflowDocumentsIncludesAuthoritativeDuties(t *testing.T) { t.Fatalf("stranger must not become author: %+v", got) } } + +func TestListWorkflowDocumentsCollapsesLegacyAlias(t *testing.T) { + engine := docgov.NewGovernanceEngine(nil) + ctx := context.Background() + const projectID = int64(6579) + seeds := []struct { + docID string + taskKey string + title string + }{ + {"doc_proj6579_intake", "intake", "用户初始需求收集表"}, + {"doc_proj6579_bd_intake", "bd_intake", "用户初始需求收集表"}, + {"doc_proj6579_bd_research", "bd_research", "调研报告"}, + } + for _, seed := range seeds { + if _, err := engine.CreateOrGetDocument( + ctx, seed.docID, projectID, seed.taskKey, seed.title, "V0.1", "", + 398, "陈威", 0, "", 0, "", + ); err != nil { + t.Fatal(err) + } + } + docs, err := engine.ListWorkflowDocumentsByProject(ctx, projectID) + if err != nil { + t.Fatal(err) + } + if len(docs) != 2 { + t.Fatalf("documents=%d, want bd_intake and bd_research only", len(docs)) + } + for _, doc := range docs { + if doc.TaskKey == "intake" { + t.Fatalf("legacy intake row leaked into the list: %+v", doc) + } + } + if docs[0].TaskKey != "bd_intake" || docs[0].DocID != "doc_proj6579_bd_intake" { + t.Fatalf("first doc=%+v", docs[0]) + } +} diff --git a/web/src/components/chat/deliverableSync.test.ts b/web/src/components/chat/deliverableSync.test.ts index 095f84f7..d305f541 100644 --- a/web/src/components/chat/deliverableSync.test.ts +++ b/web/src/components/chat/deliverableSync.test.ts @@ -277,4 +277,71 @@ describe('mergeWorkflowDocumentsFromServer', () => { expect(merged).toHaveLength(1); expect(merged[0].docId).toBe('doc_proj6001_intake'); }); + + it('collapses seven legacy local clones and a coexisting intake row into seven bd_ docs', () => { + const legacy = [ + ['intake', '用户初始需求收集表'], + ['research', '调研报告(竞品/技术/专利/市场/对比/法规)'], + ['feasibility_reg', '注册路径可行性评估报告(法规细则)'], + ['feasibility_dev', '设计开发可行性评估报告(研发细则)'], + ['user_requirements', '用户需求规格说明书(URS 长标题)'], + ['prelim_design', '初步设计说明书(长标题)'], + ['cdo_quote', 'CDO报价核算表(长标题)'], + ] as const; + const prev: DeliverableItem[] = legacy.map(([taskKey, title]) => ({ + docId: `doc-p6579-${taskKey}`, + projectId: 6579, + taskKey, + title, + stage: 'V0.5', + category: 'workflow', + creatorId: 398, + creatorName: '陈威', + isFixedWorkflow: true, + isTemplateOnly: true, + lifecycleState: 'drafting', + isClaimed: false, + author: '', + reviewer: '', + approver: '', + updatedAt: '2026-09-21', + governanceMode: 'controlled', + })); + prev.push({ + ...prev[0], + docId: 'doc-p99-intake', + projectId: 99, + title: '未激活项目的唯一模板', + }); + + const server = legacy.map(([taskKey, title]) => ({ + doc_id: `doc_proj6579_bd_${taskKey}`, + project_id: 6579, + task_key: `bd_${taskKey}`, + title: title.split('(')[0], + lifecycle_state: 'drafting' as const, + is_template_only: true, + })); + server.push({ + doc_id: 'doc_proj6579_intake', + project_id: 6579, + task_key: 'intake', + title: '用户初始需求收集表', + lifecycle_state: 'drafting', + is_template_only: false, + }); + + const merged = mergeWorkflowDocumentsFromServer(prev, server, 6579, { id: 398, name: '陈威' }); + const projectDocs = merged.filter((item) => item.projectId === 6579); + expect(projectDocs).toHaveLength(7); + expect(projectDocs.map((item) => item.taskKey).sort()).toEqual( + legacy.map(([taskKey]) => `bd_${taskKey}`).sort(), + ); + expect(new Set(projectDocs.map((item) => item.docId)).size).toBe(7); + expect(projectDocs.some((item) => item.taskKey === 'intake' || item.taskKey === 'research')).toBe(false); + expect(projectDocs.some((item) => item.title.includes('竞品'))).toBe(false); + const intake = projectDocs.find((item) => item.taskKey === 'bd_intake'); + expect(intake?.docId).toBe('doc_proj6579_bd_intake'); + expect(merged.find((item) => item.projectId === 99)?.docId).toBe('doc-p99-intake'); + }); });