831 lines
34 KiB
JavaScript
831 lines
34 KiB
JavaScript
/**
|
|
* PR-37 Domain Intelligence & Delivery Planning — Test Suite
|
|
*
|
|
* Part M:全覆盖测试
|
|
*
|
|
* 覆盖:
|
|
* A — Domain Registry
|
|
* B — Domain Classification
|
|
* C — Industry Epic Generation
|
|
* D — Industry Feature Generation
|
|
* E — Domain Validation
|
|
* F — Sprint Planning
|
|
* G — Release Planning
|
|
* H — Batch Planning
|
|
* I — Capacity Planning
|
|
* J — Roadmap Alignment
|
|
* K — Factory Integration
|
|
* Z — Regression (SF-01, SF-02, PR-35, PR-36)
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// ─── Domain Registry (Part A) ──────────────────────
|
|
import {
|
|
getDomainRegistry, getDomain, listDomains,
|
|
getDomainsByCategory, searchDomains, DOMAINS, DOMAIN_CATEGORY,
|
|
} from "../src/pr-37-domain-intelligence/domain-registry.mjs";
|
|
|
|
// ─── Domain Intelligence (Parts B, C, D, E) ────────
|
|
import {
|
|
classifyProduct,
|
|
} from "../src/pr-37-domain-intelligence/domain-classifier.mjs";
|
|
|
|
import {
|
|
generateIndustryEpics, resetEpicCounter, describeDomain, listDomainEpics,
|
|
} from "../src/pr-37-domain-intelligence/industry-epic-generator.mjs";
|
|
|
|
import {
|
|
generateIndustryFeatures, resetFeatureCounter, detectFeatureType,
|
|
} from "../src/pr-37-domain-intelligence/industry-feature-generator.mjs";
|
|
|
|
import {
|
|
validateEpics, validateFeatures,
|
|
} from "../src/pr-37-domain-intelligence/domain-validator.mjs";
|
|
|
|
import { createDomainIntelligenceFactory } from "../src/pr-37-domain-intelligence/index.mjs";
|
|
|
|
// ─── Delivery Planning (Parts F, G, H, I, J) ──────
|
|
import {
|
|
generateSprintPlan, defaultSprintConfig,
|
|
} from "../src/pr-37-delivery-planning/sprint-planner.mjs";
|
|
|
|
import {
|
|
generateReleasePlan,
|
|
} from "../src/pr-37-delivery-planning/release-planner.mjs";
|
|
|
|
import {
|
|
generateBatchPlan,
|
|
} from "../src/pr-37-delivery-planning/batch-planner.mjs";
|
|
|
|
import {
|
|
generateCapacityPlan, defaultCapacityConfig,
|
|
} from "../src/pr-37-delivery-planning/capacity-planner.mjs";
|
|
|
|
import {
|
|
alignRoadmapWithPlans,
|
|
} from "../src/pr-37-delivery-planning/roadmap-aligner.mjs";
|
|
|
|
import {
|
|
createDeliveryPlanningFactory, PR37_ARTIFACTS,
|
|
} from "../src/pr-37-delivery-planning/index.mjs";
|
|
|
|
// ─── Regression Suite ──────────────────────────────
|
|
import { createFactoryRegistry } from "../src/software-factory-core/factory-registry.mjs";
|
|
import { createFactoryInput } from "../src/software-factory-core/factory-contract.mjs";
|
|
import { registerSF01 } from "../src/sf-01-product-strategy/index.mjs";
|
|
import { registerSF02 } from "../src/sf-02-requirement-engineering/index.mjs";
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// Test Fixtures
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
function mockStories(count = 30) {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: `us-${i + 1}`,
|
|
statement: `As a user, I want feature ${i + 1}, so that I can work better.`,
|
|
role: "用户",
|
|
goal: `feature ${i + 1}`,
|
|
value: "better work",
|
|
priority: i < 10 ? "Must Have" : i < 20 ? "Should Have" : "Could Have",
|
|
storyPoints: i < 10 ? 3 : i < 20 ? 2 : 1,
|
|
featureId: `ft-${Math.floor(i / 5) + 1}`,
|
|
epicId: `ep-${Math.floor(i / 8) + 1}`,
|
|
}));
|
|
}
|
|
|
|
function mockEpics(count = 4) {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: `ep-${i + 1}`,
|
|
title: ["财务管理", "采购管理", "库存管理", "销售管理"][i] || `Epic ${i + 1}`,
|
|
description: `Description for epic ${i + 1}`,
|
|
category: "管理",
|
|
objective: `Objective ${i + 1}`,
|
|
}));
|
|
}
|
|
|
|
function mockFeatures(count = 6) {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: `ft-${i + 1}`,
|
|
title: `Feature ${i + 1}`,
|
|
description: `Description for feature ${i + 1}`,
|
|
epicId: `ep-${Math.floor(i / 2) + 1}`,
|
|
featureType: "CRUD",
|
|
}));
|
|
}
|
|
|
|
function mockMVPScope() {
|
|
return {
|
|
mvp: { label: "MVP", epics: 2, features: 2, userStories: 10, totalStoryPoints: 30 },
|
|
v1: { label: "V1", epics: 4, features: 4, userStories: 20, totalStoryPoints: 50 },
|
|
v2: { label: "V2", epics: 4, features: 6, userStories: 30, totalStoryPoints: 60 },
|
|
enterprise: { label: "Enterprise", epics: 4, features: 6, userStories: 30 },
|
|
global: { label: "Global", epics: 4, features: 6, userStories: 30 },
|
|
phases: [
|
|
{ name: "mvp", scope: "最小可行产品", duration: 3, budget: 300000 },
|
|
{ name: "v1", scope: "第一完整版本", duration: 4, budget: 500000 },
|
|
{ name: "v2", scope: "功能增强版本", duration: 3, budget: 300000 },
|
|
],
|
|
};
|
|
}
|
|
|
|
function mockPriors(stories) {
|
|
return stories.map(s => ({
|
|
itemId: s.id, itemType: "story", category: s.priority,
|
|
score: s.priority === "Must Have" ? 100 : s.priority === "Should Have" ? 70 : 40,
|
|
}));
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// A — Domain Registry
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part A — Domain Registry", () => {
|
|
|
|
it("registers exactly 16 domains", () => {
|
|
const domains = listDomains();
|
|
assert.equal(domains.length, 16);
|
|
});
|
|
|
|
it("each domain has all 10 knowledge dimensions", () => {
|
|
for (const name of listDomains()) {
|
|
const d = getDomain(name);
|
|
assert.ok(d.name);
|
|
assert.ok(d.displayName);
|
|
assert.ok(d.description);
|
|
assert.ok(d.coreProcesses.length >= 3, `${name}: coreProcesses`);
|
|
assert.ok(d.coreEntities.length >= 3, `${name}: coreEntities`);
|
|
assert.ok(d.coreEpics.length >= 5, `${name}: coreEpics`);
|
|
assert.ok(Object.keys(d.coreFeatures).length >= 3, `${name}: coreFeatures`);
|
|
assert.ok(d.coreKPIs.length >= 3, `${name}: coreKPIs`);
|
|
assert.ok(d.coreRoles.length >= 3, `${name}: coreRoles`);
|
|
assert.ok(d.terminology.length >= 2, `${name}: terminology`);
|
|
assert.ok(d.constraints.length >= 1, `${name}: constraints`);
|
|
}
|
|
});
|
|
|
|
it("ERP domain has 10 core epics", () => {
|
|
const erp = getDomain("ERP");
|
|
assert.equal(erp.coreEpics.length, 10);
|
|
const titles = erp.coreEpics.map(e => e.title);
|
|
assert.ok(titles.includes("财务管理"));
|
|
assert.ok(titles.includes("采购管理"));
|
|
assert.ok(titles.includes("库存管理"));
|
|
assert.ok(titles.includes("生产管理"));
|
|
});
|
|
|
|
it("MES domain has 10 core epics", () => {
|
|
const mes = getDomain("MES");
|
|
assert.equal(mes.coreEpics.length, 10);
|
|
const titles = mes.coreEpics.map(e => e.title);
|
|
assert.ok(titles.includes("生产执行"));
|
|
assert.ok(titles.includes("质量管理"));
|
|
assert.ok(titles.includes("设备管理"));
|
|
assert.ok(titles.includes("工单管理"));
|
|
});
|
|
|
|
it("CRM domain has 9 core epics", () => {
|
|
const crm = getDomain("CRM");
|
|
assert.ok(crm.coreEpics.length >= 7);
|
|
const titles = crm.coreEpics.map(e => e.title);
|
|
assert.ok(titles.includes("客户管理"));
|
|
assert.ok(titles.includes("商机管理"));
|
|
assert.ok(titles.includes("销售漏斗"));
|
|
});
|
|
|
|
it("AI Platform domain has 8 core epics", () => {
|
|
const ai = getDomain("AI Platform");
|
|
assert.ok(ai.coreEpics.length >= 6);
|
|
const titles = ai.coreEpics.map(e => e.title);
|
|
assert.ok(titles.includes("知识库管理"));
|
|
assert.ok(titles.includes("Agent管理"));
|
|
});
|
|
|
|
it("getDomain returns null for unknown domain", () => {
|
|
assert.equal(getDomain("UnknownDomain"), null);
|
|
});
|
|
|
|
it("searchDomains finds by keyword", () => {
|
|
const results = searchDomains("MES");
|
|
assert.ok(results.length >= 1);
|
|
assert.equal(results[0].name, "MES");
|
|
});
|
|
|
|
it("getDomainsByCategory filters correctly", () => {
|
|
const enterprise = getDomainsByCategory(DOMAIN_CATEGORY.ENTERPRISE_SOFTWARE);
|
|
assert.ok(enterprise.length >= 3);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// B — Domain Classification
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part B — Domain Classification", () => {
|
|
|
|
it("classifies ERP system correctly", () => {
|
|
const result = classifyProduct({ productName: "ERP系统", description: "企业资源计划" });
|
|
assert.equal(result.primaryDomain, "ERP");
|
|
assert.ok(result.confidenceScore >= 0.3);
|
|
assert.ok(result.matchedKeywords.length >= 1);
|
|
});
|
|
|
|
it("classifies MES system correctly", () => {
|
|
const result = classifyProduct({ productName: "工业MES系统", description: "制造执行系统" });
|
|
assert.equal(result.primaryDomain, "MES");
|
|
assert.ok(result.confidenceScore >= 0.5);
|
|
});
|
|
|
|
it("classifies AI customer service platform", () => {
|
|
const result = classifyProduct({ productName: "AI客服平台", description: "智能客服系统" });
|
|
assert.equal(result.primaryDomain, "AI Platform");
|
|
});
|
|
|
|
it("classifies CRM system", () => {
|
|
const result = classifyProduct({ productName: "CRM系统", description: "客户关系管理" });
|
|
assert.equal(result.primaryDomain, "CRM");
|
|
});
|
|
|
|
it("classifies cross-border ecommerce with secondary", () => {
|
|
const result = classifyProduct({ productName: "跨境电商平台", description: "跨境电商" });
|
|
assert.equal(result.primaryDomain, "E-Commerce");
|
|
assert.ok(result.secondaryDomain);
|
|
});
|
|
|
|
it("classifies industrial AI quality inspection", () => {
|
|
const result = classifyProduct({ productName: "工业AI质检", description: "智能制造AI质检" });
|
|
assert.equal(result.primaryDomain, "MES");
|
|
});
|
|
|
|
it("classifies WMS warehouse", () => {
|
|
const result = classifyProduct({ productName: "WMS仓库", description: "仓储管理" });
|
|
assert.equal(result.primaryDomain, "WMS");
|
|
});
|
|
|
|
it("returns null for empty input", () => {
|
|
const result = classifyProduct({ productName: "", description: "" });
|
|
assert.equal(result.primaryDomain, null);
|
|
assert.equal(result.confidenceScore, 0);
|
|
assert.ok(result.classificationReason);
|
|
});
|
|
|
|
it("returns null for null input", () => {
|
|
const result = classifyProduct(null);
|
|
assert.equal(result.primaryDomain, null);
|
|
assert.equal(result.confidenceScore, 0);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// C — Industry Epic Generation
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part C — Industry Epic Generation", () => {
|
|
|
|
it("generates ERP-specific epics with domain vocabulary", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("ERP", { productName: "ERP系统" });
|
|
assert.equal(epics.length, 10);
|
|
const titles = epics.map(e => e.title);
|
|
assert.ok(titles.includes("财务管理"));
|
|
assert.ok(titles.includes("采购管理"));
|
|
assert.ok(titles.includes("生产管理"));
|
|
// Should NOT have generic "内容管理"
|
|
assert.ok(!titles.includes("内容管理"));
|
|
});
|
|
|
|
it("generates MES-specific epics", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("MES", { productName: "工业MES系统" });
|
|
assert.equal(epics.length, 10);
|
|
const titles = epics.map(e => e.title);
|
|
assert.ok(titles.includes("质量管理"));
|
|
assert.ok(titles.includes("设备管理"));
|
|
assert.ok(titles.includes("追溯管理"));
|
|
});
|
|
|
|
it("generates AI Platform epics", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("AI Platform", { productName: "AI客服平台" });
|
|
assert.ok(epics.length >= 6);
|
|
const titles = epics.map(e => e.title);
|
|
assert.ok(titles.includes("知识库管理"));
|
|
assert.ok(titles.includes("Agent管理"));
|
|
});
|
|
|
|
it("each epic has all required fields", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("ERP", { productName: "ERP系统" });
|
|
for (const epic of epics) {
|
|
assert.ok(epic.id.startsWith("ep-"));
|
|
assert.ok(epic.title);
|
|
assert.ok(epic.description);
|
|
assert.ok(epic.category);
|
|
assert.ok(epic.objective);
|
|
assert.ok(epic.successMetric);
|
|
}
|
|
});
|
|
|
|
it("falls back to generic template for unknown domain", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("量子计算", { productName: "量子计算机" });
|
|
assert.ok(epics.length >= 5);
|
|
// Generic epics shouldn't have ERP-specific ones
|
|
const titles = epics.map(e => e.title);
|
|
assert.ok(!titles.includes("财务管理"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// D — Industry Feature Generation
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part D — Industry Feature Generation", () => {
|
|
|
|
it("generates ERP features from domain registry", () => {
|
|
resetEpicCounter(); resetFeatureCounter();
|
|
const epics = generateIndustryEpics("ERP", { productName: "ERP系统" });
|
|
const features = generateIndustryFeatures("ERP", epics);
|
|
assert.ok(features.length >= 50);
|
|
const titles = features.map(f => f.title);
|
|
assert.ok(titles.includes("总账管理"));
|
|
assert.ok(titles.includes("供应商管理"));
|
|
});
|
|
|
|
it("detects feature types correctly", () => {
|
|
assert.equal(detectFeatureType("数据分析"), "analytics");
|
|
assert.equal(detectFeatureType("审批流程"), "workflow");
|
|
assert.equal(detectFeatureType("统计分析"), "analytics");
|
|
assert.equal(detectFeatureType("消息推送"), "notification");
|
|
assert.equal(detectFeatureType("搜索"), "search");
|
|
assert.equal(detectFeatureType("系统设置"), "configuration");
|
|
assert.equal(detectFeatureType("用户管理"), "CRUD");
|
|
});
|
|
|
|
it("each feature references correct epicId", () => {
|
|
resetEpicCounter(); resetFeatureCounter();
|
|
const epics = generateIndustryEpics("ERP", { productName: "ERP系统" });
|
|
const features = generateIndustryFeatures("ERP", epics);
|
|
const epicIds = new Set(epics.map(e => e.id));
|
|
for (const f of features) {
|
|
assert.ok(epicIds.has(f.epicId), `Feature ${f.title} epicId ${f.epicId} not in epics`);
|
|
}
|
|
});
|
|
|
|
it("each feature has id with ft- prefix", () => {
|
|
resetEpicCounter(); resetFeatureCounter();
|
|
const epics = generateIndustryEpics("MES", { productName: "MES系统" });
|
|
const features = generateIndustryFeatures("MES", epics);
|
|
for (const f of features) {
|
|
assert.ok(f.id.startsWith("ft-"));
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// E — Domain Validation
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part E — Domain Validation", () => {
|
|
|
|
it("validates ERP epics with 100% coverage", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("ERP", { productName: "ERP系统" });
|
|
const result = validateEpics(epics, "ERP");
|
|
assert.equal(result.overallVerdict, "pass");
|
|
assert.equal(result.coverageScore, 100);
|
|
assert.equal(result.completenessScore, 100);
|
|
});
|
|
|
|
it("validates MES epics with 100% coverage", () => {
|
|
resetEpicCounter();
|
|
const epics = generateIndustryEpics("MES", { productName: "MES系统" });
|
|
const result = validateEpics(epics, "MES");
|
|
assert.equal(result.overallVerdict, "pass");
|
|
assert.equal(result.coverageScore, 100);
|
|
});
|
|
|
|
it("detects generic epics as low coverage for ERP", () => {
|
|
const genericEpics = [
|
|
{ title: "用户管理" }, { title: "内容管理" }, { title: "业务流程" },
|
|
];
|
|
const result = validateEpics(genericEpics, "ERP");
|
|
assert.ok(result.coverageScore < 100);
|
|
assert.ok(result.missingEpics.length > 0);
|
|
assert.equal(result.overallVerdict, "fail");
|
|
});
|
|
|
|
it("validates features consistency", () => {
|
|
resetEpicCounter(); resetFeatureCounter();
|
|
const epics = generateIndustryEpics("ERP", { productName: "ERP系统" });
|
|
const features = generateIndustryFeatures("ERP", epics);
|
|
const result = validateFeatures(features, "ERP");
|
|
assert.ok(result.consistencyScore >= 70);
|
|
});
|
|
|
|
it("handles unknown domain gracefully", () => {
|
|
const result = validateEpics([{ title: "Test" }], "UnknownDomain");
|
|
assert.equal(result.coverageScore, 0);
|
|
assert.equal(result.overallVerdict, "fail");
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// F — Sprint Planning
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part F — Sprint Planning", () => {
|
|
|
|
it("generates sprints from Must Have stories", () => {
|
|
const stories = mockStories(30);
|
|
const priors = mockPriors(stories);
|
|
const result = generateSprintPlan({
|
|
stories, dependencies: [], priorities: priors,
|
|
config: defaultSprintConfig({ teamVelocity: 20 }),
|
|
});
|
|
assert.ok(result.sprints.length >= 1, `Expected >=1 sprints, got ${result.sprints.length}`);
|
|
assert.ok(result.metrics.totalSprints >= 1);
|
|
});
|
|
|
|
it("respects sprint capacity", () => {
|
|
const stories = mockStories(30);
|
|
const priors = mockPriors(stories);
|
|
const result = generateSprintPlan({
|
|
stories, dependencies: [], priorities: priors,
|
|
config: defaultSprintConfig({ teamVelocity: 15, maxStoryPointsPerSprint: 15 }),
|
|
});
|
|
for (const sprint of result.sprints) {
|
|
assert.ok(sprint.totalStoryPoints <= 15 + 5, // allow small overflow
|
|
`Sprint ${sprint.sprintNumber}: ${sprint.totalStoryPoints} pts > 20`);
|
|
}
|
|
});
|
|
|
|
it("each sprint has required fields", () => {
|
|
const result = generateSprintPlan({
|
|
stories: mockStories(20), dependencies: [], priorities: mockPriors(mockStories(20)),
|
|
});
|
|
for (const s of result.sprints) {
|
|
assert.ok(s.sprintNumber);
|
|
assert.ok(s.sprintGoal);
|
|
assert.ok(s.startDate);
|
|
assert.ok(s.endDate);
|
|
assert.ok(Array.isArray(s.storyIds));
|
|
assert.ok(s.totalStoryPoints >= 0);
|
|
assert.ok(Array.isArray(s.risks));
|
|
}
|
|
});
|
|
|
|
it("handles single story", () => {
|
|
const stories = [{ id: "us-1", statement: "test", priority: "Must Have", storyPoints: 3, featureId: "ft-1", epicId: "ep-1" }];
|
|
const priors = [{ itemId: "us-1", itemType: "story", category: "Must Have", score: 100 }];
|
|
const result = generateSprintPlan({ stories, dependencies: [], priorities: priors });
|
|
assert.equal(result.sprints.length, 1);
|
|
assert.equal(result.sprints[0].storyIds.length, 1);
|
|
});
|
|
|
|
it("handles empty stories", () => {
|
|
const result = generateSprintPlan({ stories: [], dependencies: [], priorities: [] });
|
|
assert.equal(result.sprints.length, 0);
|
|
assert.equal(result.metrics.totalSprints, 0);
|
|
});
|
|
|
|
it("respects dependency order", () => {
|
|
const stories = [
|
|
{ id: "us-1", statement: "base", priority: "Must Have", storyPoints: 5, featureId: "ft-1", epicId: "ep-1" },
|
|
{ id: "us-2", statement: "dependent", priority: "Must Have", storyPoints: 5, featureId: "ft-1", epicId: "ep-1" },
|
|
{ id: "us-3", statement: "filler", priority: "Should Have", storyPoints: 3, featureId: "ft-1", epicId: "ep-1" },
|
|
];
|
|
const priors = [
|
|
{ itemId: "us-1", itemType: "story", category: "Must Have", score: 100 },
|
|
{ itemId: "us-2", itemType: "story", category: "Must Have", score: 95 },
|
|
{ itemId: "us-3", itemType: "story", category: "Should Have", score: 70 },
|
|
];
|
|
// us-2 depends on us-1 (us-2 → us-1 means us-2 requires us-1 first)
|
|
const deps = { edges: [{ from: "us-2", to: "us-1", critical: true }] };
|
|
const result = generateSprintPlan({ stories, dependencies: deps, priorities: priors, config: defaultSprintConfig({ teamVelocity: 8 }) });
|
|
// Both us-1 and us-2 should be in the sprint plan
|
|
const allIds = result.sprints.flatMap(s => s.storyIds);
|
|
assert.ok(allIds.includes("us-1"), "us-1 should be planned");
|
|
assert.ok(allIds.includes("us-2"), "us-2 should be planned");
|
|
// us-2 must not be assigned before us-1 (dependency constraint)
|
|
const s1 = result.sprints.find(s => s.storyIds.includes("us-1"));
|
|
const s2 = result.sprints.find(s => s.storyIds.includes("us-2"));
|
|
assert.ok(s1.sprintNumber <= s2.sprintNumber,
|
|
`us-1 (Sprint ${s1.sprintNumber}) must not be after us-2 (Sprint ${s2.sprintNumber})`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// G — Release Planning
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part G — Release Planning", () => {
|
|
|
|
it("generates 5 releases from mvpScope", () => {
|
|
const result = generateReleasePlan({
|
|
mvpScope: mockMVPScope(),
|
|
priorities: mockPriors(mockStories(30)),
|
|
dependencies: [],
|
|
stories: mockStories(30),
|
|
epics: mockEpics(4),
|
|
features: mockFeatures(6),
|
|
});
|
|
assert.equal(result.releases.length, 5);
|
|
const versions = result.releases.map(r => r.version);
|
|
assert.ok(versions.includes("1.0.0"));
|
|
assert.ok(versions.includes("2.0.0"));
|
|
});
|
|
|
|
it("R1.0 (MVP) contains Must Have stories", () => {
|
|
const stories = mockStories(30);
|
|
const result = generateReleasePlan({
|
|
mvpScope: mockMVPScope(),
|
|
priorities: mockPriors(stories),
|
|
dependencies: [],
|
|
stories,
|
|
epics: mockEpics(4),
|
|
features: mockFeatures(6),
|
|
});
|
|
const r1 = result.releases[0];
|
|
assert.ok(r1.features.storyCount > 0);
|
|
assert.ok(r1.validationGates.length >= 3);
|
|
});
|
|
|
|
it("each release has validation gates and goals", () => {
|
|
const result = generateReleasePlan({
|
|
mvpScope: mockMVPScope(),
|
|
priorities: mockPriors(mockStories(30)),
|
|
dependencies: [],
|
|
stories: mockStories(30),
|
|
epics: mockEpics(4),
|
|
features: mockFeatures(6),
|
|
});
|
|
for (const release of result.releases) {
|
|
assert.ok(release.version);
|
|
assert.ok(release.name);
|
|
assert.ok(Array.isArray(release.validationGates));
|
|
assert.ok(release.releaseGoals);
|
|
}
|
|
});
|
|
|
|
it("handles null mvpScope gracefully", () => {
|
|
const result = generateReleasePlan({
|
|
mvpScope: null,
|
|
priorities: [],
|
|
dependencies: [],
|
|
stories: [],
|
|
epics: [],
|
|
features: [],
|
|
});
|
|
assert.ok(result.releases);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// H — Batch Planning
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part H — Batch Planning", () => {
|
|
|
|
it("generates batches with dependency levels", () => {
|
|
const stories = [
|
|
{ id: "us-1", statement: "no deps", priority: "Must Have", storyPoints: 3, featureId: "ft-1", epicId: "ep-1" },
|
|
{ id: "us-2", statement: "dep on 1", priority: "Must Have", storyPoints: 2, featureId: "ft-1", epicId: "ep-1" },
|
|
{ id: "us-3", statement: "dep on 2", priority: "Should Have", storyPoints: 1, featureId: "ft-1", epicId: "ep-1" },
|
|
];
|
|
const deps = {
|
|
edges: [
|
|
{ from: "us-2", to: "us-1", critical: true },
|
|
{ from: "us-3", to: "us-2", critical: true },
|
|
],
|
|
};
|
|
const result = generateBatchPlan({
|
|
stories,
|
|
dependencies: deps,
|
|
priorities: mockPriors(stories),
|
|
sprints: { sprints: [] },
|
|
releases: { releases: [] },
|
|
});
|
|
assert.ok(result.batches.length >= 1);
|
|
// Should have at least a DevelopmentBatch with dependency levels
|
|
const devBatch = result.batches.find(b => b.batchType === "DevelopmentBatch");
|
|
assert.ok(devBatch);
|
|
});
|
|
|
|
it("generates all 4 batch types when data is sufficient", () => {
|
|
const stories = mockStories(20);
|
|
const sp = generateSprintPlan({ stories, dependencies: [], priorities: mockPriors(stories) });
|
|
const rp = generateReleasePlan({
|
|
mvpScope: mockMVPScope(), priorities: mockPriors(stories),
|
|
dependencies: [], stories, epics: mockEpics(4), features: mockFeatures(6),
|
|
});
|
|
const result = generateBatchPlan({
|
|
stories, dependencies: [], priorities: mockPriors(stories),
|
|
sprints: sp, releases: rp.releases,
|
|
});
|
|
assert.ok(result.batches.length >= 1);
|
|
const types = new Set(result.batches.map(b => b.batchType));
|
|
assert.ok(types.has("DevelopmentBatch"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// I — Capacity Planning
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part I — Capacity Planning", () => {
|
|
|
|
it("calculates capacity from sprint plan", () => {
|
|
const stories = mockStories(20);
|
|
const sp = generateSprintPlan({ stories, dependencies: [], priorities: mockPriors(stories) });
|
|
const cp = generateCapacityPlan({ sprints: sp.sprints, releases: [] });
|
|
assert.ok(cp.totalEffort > 0);
|
|
assert.ok(cp.totalCapacity > 0);
|
|
assert.ok(cp.capacityUtilization.includes("%"));
|
|
});
|
|
|
|
it("delivery forecast provides realistic date", () => {
|
|
const stories = mockStories(20);
|
|
const sp = generateSprintPlan({ stories, dependencies: [], priorities: mockPriors(stories) });
|
|
const cp = generateCapacityPlan({ sprints: sp.sprints, releases: [] });
|
|
assert.ok(cp.deliveryForecast.realistic);
|
|
assert.notEqual(cp.deliveryForecast.realistic, "N/A");
|
|
});
|
|
|
|
it("handles empty sprint list gracefully", () => {
|
|
const cp = generateCapacityPlan({ sprints: [], releases: [] });
|
|
assert.equal(cp.totalEffort, 0);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// J — Roadmap Alignment
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part J — Roadmap Alignment", () => {
|
|
|
|
it("aligns roadmap with sprint and release plans", () => {
|
|
const stories = mockStories(30);
|
|
const sp = generateSprintPlan({ stories, dependencies: [], priorities: mockPriors(stories) });
|
|
const rp = generateReleasePlan({
|
|
mvpScope: mockMVPScope(), priorities: mockPriors(stories),
|
|
dependencies: [], stories, epics: mockEpics(4), features: mockFeatures(6),
|
|
});
|
|
const bp = generateBatchPlan({ stories, dependencies: [], priorities: mockPriors(stories), sprints: sp.sprints, releases: rp.releases });
|
|
|
|
const result = alignRoadmapWithPlans({
|
|
roadmap: mockMVPScope(),
|
|
sprintPlan: sp,
|
|
releasePlan: rp,
|
|
batchPlan: bp,
|
|
mvpScope: mockMVPScope(),
|
|
});
|
|
assert.ok(["aligned", "partial", "misaligned"].includes(result.overallVerdict));
|
|
assert.ok(result.alignmentScore >= 0 && result.alignmentScore <= 100);
|
|
assert.ok(result.summary);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// K — Factory Integration
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part K — Factory Integration", () => {
|
|
|
|
it("createDomainIntelligenceFactory produces epics + features + validation", () => {
|
|
const result = createDomainIntelligenceFactory({ productName: "ERP系统" });
|
|
assert.equal(result.classification.primaryDomain, "ERP");
|
|
assert.ok(result.epics.length >= 8);
|
|
assert.ok(result.features.length >= 50);
|
|
assert.ok(result.validation);
|
|
assert.equal(result.validation.overallVerdict, "pass");
|
|
});
|
|
|
|
it("createDomainIntelligenceFactory accepts domainOverride", () => {
|
|
const result = createDomainIntelligenceFactory(
|
|
{ productName: "神秘系统" },
|
|
{ domainOverride: "MES" }
|
|
);
|
|
assert.equal(result.classification.primaryDomain, "MES");
|
|
assert.equal(result.classification.confidenceScore, 1.0);
|
|
});
|
|
|
|
it("createDeliveryPlanningFactory produces 4 plans", () => {
|
|
const stories = mockStories(30);
|
|
const sf02Data = {
|
|
userStories: stories,
|
|
epics: mockEpics(4),
|
|
features: mockFeatures(6),
|
|
dependencies: { nodes: [], edges: [] },
|
|
priorities: mockPriors(stories),
|
|
mvpScope: mockMVPScope(),
|
|
roadmap: mockMVPScope().phases,
|
|
};
|
|
const result = createDeliveryPlanningFactory(sf02Data);
|
|
assert.equal(result.status, "passed");
|
|
assert.ok(result.sprintPlan.sprints.length >= 1);
|
|
assert.ok(result.releasePlan.releases.length >= 1);
|
|
assert.ok(result.batchPlan.batches.length >= 1);
|
|
assert.ok(result.capacityPlan.totalEffort > 0);
|
|
assert.ok(result.alignmentReport);
|
|
assert.ok(["aligned", "partial", "misaligned"].includes(result.alignmentReport.overallVerdict));
|
|
});
|
|
|
|
it("createDeliveryPlanningFactory fails gracefully with no data", () => {
|
|
const result = createDeliveryPlanningFactory(null);
|
|
assert.equal(result.status, "failed");
|
|
assert.ok(result.error);
|
|
});
|
|
|
|
it("PR37_ARTIFACTS has all artifact types", () => {
|
|
const types = Object.values(PR37_ARTIFACTS);
|
|
assert.ok(types.includes("sprint-plan"));
|
|
assert.ok(types.includes("release-plan"));
|
|
assert.ok(types.includes("batch-plan"));
|
|
assert.ok(types.includes("capacity-plan"));
|
|
assert.ok(types.includes("alignment-report"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// Z — Regression: SF-01, SF-02, PR-35, PR-36
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("PR-37 Part Z — Regression", () => {
|
|
|
|
it("SF-01 factory still works after PR-37", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
const input = createFactoryInput({ factoryId: "SF-01", parameters: { idea: "AI客服平台" } });
|
|
const output = registry.execute("SF-01", input);
|
|
assert.equal(output.status, "passed");
|
|
});
|
|
|
|
it("SF-02 factory still works after PR-37", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
const sf01Out = registry.execute("SF-01", createFactoryInput({ factoryId: "SF-01", parameters: { idea: "AI客服平台" } }));
|
|
const sf02Out = registry.execute("SF-02", createFactoryInput({ factoryId: "SF-02", upstreamArtifacts: sf01Out.artifacts }));
|
|
assert.equal(sf02Out.status, "passed");
|
|
assert.ok(sf02Out.artifacts.length >= 10);
|
|
});
|
|
|
|
it("SF-01 → SF-02 → PR-37 full pipeline", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
const sf01Out = registry.execute("SF-01", createFactoryInput({ factoryId: "SF-01", parameters: { idea: "ERP系统" } }));
|
|
const sf02Out = registry.execute("SF-02", createFactoryInput({ factoryId: "SF-02", upstreamArtifacts: sf01Out.artifacts }));
|
|
|
|
// Extract SF-02 data
|
|
const stories = JSON.parse(sf02Out.artifacts.find(a => a.metadata?.artifactType === "user-story-catalog").content);
|
|
const epics = JSON.parse(sf02Out.artifacts.find(a => a.metadata?.artifactType === "epic-catalog").content);
|
|
const features = JSON.parse(sf02Out.artifacts.find(a => a.metadata?.artifactType === "feature-catalog").content);
|
|
const depGraph = JSON.parse(sf02Out.artifacts.find(a => a.metadata?.artifactType === "dependency-graph").content);
|
|
const priMatrix = JSON.parse(sf02Out.artifacts.find(a => a.metadata?.artifactType === "priority-matrix").content);
|
|
const mvpScope = JSON.parse(sf02Out.artifacts.find(a => a.metadata?.artifactType === "mvp-scope").content);
|
|
|
|
// Build pri entries from priMatrix
|
|
const priEntries = [];
|
|
const catMap = { mustHave: "Must Have", shouldHave: "Should Have", couldHave: "Could Have", wontHave: "Won't Have" };
|
|
if (priMatrix?.story) {
|
|
for (const [cat, ids] of Object.entries(priMatrix.story)) {
|
|
if (cat === "summary") continue;
|
|
for (const id of ids) priEntries.push({ itemId: id, itemType: "story", category: catMap[cat] || cat, score: cat === "mustHave" ? 100 : cat === "shouldHave" ? 70 : 40 });
|
|
}
|
|
}
|
|
|
|
// PR-37 Domain Intelligence
|
|
const diResult = createDomainIntelligenceFactory({ productName: "ERP系统" });
|
|
assert.equal(diResult.classification.primaryDomain, "ERP");
|
|
assert.ok(diResult.epics.length >= 8);
|
|
assert.equal(diResult.validation.overallVerdict, "pass");
|
|
|
|
// PR-37 Delivery Planning
|
|
const dpResult = createDeliveryPlanningFactory({
|
|
userStories: stories, epics, features,
|
|
dependencies: depGraph,
|
|
priorities: priEntries,
|
|
mvpScope,
|
|
roadmap: mvpScope.phases,
|
|
});
|
|
assert.equal(dpResult.status, "passed");
|
|
assert.ok(dpResult.sprintPlan.sprints.length >= 1);
|
|
assert.ok(dpResult.releasePlan.releases.length >= 1);
|
|
assert.ok(dpResult.alignmentReport);
|
|
});
|
|
|
|
it("PR-35/PR-36 factory registry topological order preserved", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
const order = registry.getTopologicalOrder();
|
|
const sf01Idx = order.indexOf("SF-01");
|
|
const sf02Idx = order.indexOf("SF-02");
|
|
assert.ok(sf01Idx < sf02Idx, "SF-01 must come before SF-02 in topological order");
|
|
});
|
|
});
|