test(docgov): cover canonical task key collapse for project documents

锁定「7 份旧键本地克隆加 8 行服务端」收成 7 条 bd_*,以及列表与物化不再留下 intake 兄弟行。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-21 06:37:10 +00:00
co-authored by Cursor
parent 51d4390bd6
commit 50c19aaee9
3 changed files with 131 additions and 0 deletions
+26
View File
@@ -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)
}
}
+38
View File
@@ -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])
}
}
@@ -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');
});
});