1428 lines
60 KiB
JavaScript
1428 lines
60 KiB
JavaScript
/**
|
|
* SF-02 Requirement Engineering Factory — Test Suite
|
|
*
|
|
* Part S:全覆盖测试
|
|
*
|
|
* 覆盖:
|
|
* A — Requirement Domain Model
|
|
* B — Strategy Conversion Engine
|
|
* C — Epic Generation Test
|
|
* D — Feature Generation Test
|
|
* E — User Story Generation Test
|
|
* F — Acceptance Criteria Generation Test
|
|
* G — Business Rules Generation Test
|
|
* H — Permission Requirement Test
|
|
* I — NFR Generation Test
|
|
* J — Integration Requirement Test
|
|
* K — Data Requirement Test
|
|
* L — Dependency Graph Test
|
|
* M — Priority Matrix Test
|
|
* N — MVP Scope Test
|
|
* O — Traceability Test
|
|
* P — Requirement Package Test
|
|
* Q — Factory Integration Test (PR-36)
|
|
* R — Artifact Registration Test
|
|
* S — Runtime Integration Test (PR-35)
|
|
* T — SF-01 Compatibility Test
|
|
* U — Edge Cases Test
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// ─── SF-02 Domain Model ──────────────────────────
|
|
import * as dm from "../src/sf-02-requirement-engineering/domain-model.mjs";
|
|
|
|
// ─── SF-02 Conversion Engine ─────────────────────
|
|
import {
|
|
convertStrategyToRequirements,
|
|
generateEpicsFromStrategy,
|
|
generateFeaturesFromEpic,
|
|
generateUserStoriesFromFeature,
|
|
generateAcceptanceCriteriaForStory,
|
|
generateIntegrationRequirements,
|
|
generateNFRs,
|
|
generatePermissionRequirements,
|
|
generateBusinessRules,
|
|
generateDataRequirements,
|
|
generateConstraints,
|
|
} from "../src/sf-02-requirement-engineering/strategy-to-requirement.mjs";
|
|
|
|
// ─── SF-02 Dependency & Priority & MVP & Trace ────
|
|
import {
|
|
buildDependencyGraph,
|
|
buildPriorityMatrix,
|
|
generateMVPScope,
|
|
buildTraceabilityMatrix,
|
|
prioritizeByMoSCoW,
|
|
prioritizeByRICE,
|
|
prioritizeByWSJF,
|
|
} from "../src/sf-02-requirement-engineering/dependency-engine.mjs";
|
|
|
|
// ─── SF-02 Factory ───────────────────────────────
|
|
import {
|
|
sf02RequirementEngineeringFactory,
|
|
registerSF02,
|
|
generateRequirementReport,
|
|
SF02_ARTIFACTS,
|
|
} from "../src/sf-02-requirement-engineering/index.mjs";
|
|
|
|
// ─── SF-01 (for compatibility test) ──────────────
|
|
import {
|
|
sf01ProductStrategyFactory, registerSF01,
|
|
} from "../src/sf-01-product-strategy/index.mjs";
|
|
|
|
// ─── PR-36 Factory Core ──────────────────────────
|
|
import { createFactoryRegistry } from "../src/software-factory-core/factory-registry.mjs";
|
|
import { createFactoryInput, createFactoryOutput } from "../src/software-factory-core/factory-contract.mjs";
|
|
import { createArtifactGraph, createArtifactNode } from "../src/software-factory-core/artifact-graph.mjs";
|
|
|
|
// ─── PR-35 Runtime ───────────────────────────────
|
|
import {
|
|
createTask, createExecutionPlan, createRuntimeContext, executePipeline,
|
|
} from "../src/agent-runtime/index.mjs";
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// Test Fixture: Mock Strategy Package
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
function mockStrategyPackage(category = "AI平台") {
|
|
return {
|
|
executiveSummary: {
|
|
title: "Product Strategy: AI客服平台",
|
|
viabilityVerdict: "STRONG_GO",
|
|
viabilityScore: 82,
|
|
priorityLevel: "P0",
|
|
recommendation: "强烈建议立即启动",
|
|
},
|
|
problemStatement: {
|
|
who: "中小企业客服团队",
|
|
what: "客服效率低,知识管理混乱",
|
|
why: "人工客服成本高,响应慢",
|
|
severity: 75,
|
|
},
|
|
targetCustomer: {
|
|
segment: "中小企业",
|
|
willingnessToPay: 70,
|
|
painPoints: ["客服效率低", "知识碎片化", "培训成本高"],
|
|
},
|
|
targetMarket: {
|
|
tam: 50_000_000_000,
|
|
sam: 5_000_000_000,
|
|
som: 500_000_000,
|
|
growthRate: 35,
|
|
},
|
|
businessModel: { type: "SaaS", ltvCacRatio: 5.2, grossMargin: 85 },
|
|
revenueModel: { year1Revenue: 2_000_000 },
|
|
productVision: {
|
|
vision: "让每个企业拥有智能客服",
|
|
mission: "通过AI技术降低客服成本80%",
|
|
},
|
|
riskAssessment: { overallRisk: 40, breakdown: { marketRisk: 30, techRisk: 50 } },
|
|
gtmStrategy: { entryStrategy: "PLG", timeToMarketMonths: 6, initialBudget: 500_000 },
|
|
roadmap: {
|
|
totalDurationMonths: 18,
|
|
totalBudget: 2_000_000,
|
|
phases: [
|
|
{ phase: "mvp", scope: "核心AI客服功能", durationMonths: 6, budget: 500_000 },
|
|
{ phase: "v1", scope: "多语言+多渠道", durationMonths: 6, budget: 700_000 },
|
|
{ phase: "v2", scope: "高级分析+自动化", durationMonths: 4, budget: 500_000 },
|
|
{ phase: "enterprise", scope: "企业版功能", durationMonths: 2, budget: 300_000 },
|
|
],
|
|
},
|
|
metadata: { factoryId: "SF-01", category, version: "1.0" },
|
|
};
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// A — Requirement Domain Model
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part A — Requirement Domain Model", () => {
|
|
|
|
it("creates a valid Epic", () => {
|
|
const epic = dm.createEpic({
|
|
title: "知识库管理",
|
|
description: "知识文档的全生命周期管理",
|
|
category: dm.EPIC_CATEGORY.CONTENT_MANAGEMENT,
|
|
objective: "构建高质量知识体系",
|
|
});
|
|
assert.ok(epic.id.startsWith("ep-"));
|
|
assert.equal(epic.title, "知识库管理");
|
|
assert.equal(epic.category, "内容管理");
|
|
assert.equal(epic.objective, "构建高质量知识体系");
|
|
});
|
|
|
|
it("creates a valid Feature", () => {
|
|
const feature = dm.createFeature({
|
|
title: "知识库创建",
|
|
description: "创建知识库并设置基本属性",
|
|
epicId: "ep-1",
|
|
featureType: "CRUD",
|
|
});
|
|
assert.ok(feature.id.startsWith("ft-"));
|
|
assert.equal(feature.epicId, "ep-1");
|
|
assert.equal(feature.featureType, "CRUD");
|
|
});
|
|
|
|
it("creates a valid UserStory with standard format", () => {
|
|
const story = dm.createUserStory({
|
|
role: "客服人员",
|
|
goal: "快速搜索知识库",
|
|
value: "高效回答用户问题",
|
|
featureId: "ft-1",
|
|
epicId: "ep-1",
|
|
priority: dm.STORY_PRIORITY.MUST_HAVE,
|
|
});
|
|
assert.ok(story.id.startsWith("us-"));
|
|
assert.equal(story.role, "客服人员");
|
|
assert.equal(story.goal, "快速搜索知识库");
|
|
assert.equal(story.priority, "Must Have");
|
|
assert.ok(story.statement.includes("As a 客服人员"));
|
|
assert.ok(story.statement.includes("I want 快速搜索知识库"));
|
|
});
|
|
|
|
it("creates valid AcceptanceCriteria in Given/When/Then format", () => {
|
|
const ac = dm.createAcceptanceCriteria({
|
|
storyId: "us-1",
|
|
scenario: "正常搜索",
|
|
given: "用户已登录系统",
|
|
when: "用户输入搜索关键词",
|
|
then: "系统返回相关结果",
|
|
});
|
|
assert.ok(ac.id.startsWith("ac-"));
|
|
assert.ok(ac.statement.includes("Given 用户已登录系统"));
|
|
assert.ok(ac.statement.includes("When 用户输入搜索关键词"));
|
|
assert.ok(ac.statement.includes("Then 系统返回相关结果"));
|
|
});
|
|
|
|
it("creates valid BusinessRule", () => {
|
|
const br = dm.createBusinessRule({
|
|
name: "密码安全策略",
|
|
description: "密码必须满足安全要求",
|
|
ruleType: dm.RULE_TYPE.CONSTRAINT,
|
|
condition: "用户设置新密码",
|
|
action: "验证密码复杂度",
|
|
});
|
|
assert.ok(br.id.startsWith("br-"));
|
|
assert.equal(br.ruleType, "业务约束");
|
|
});
|
|
|
|
it("creates valid Constraint", () => {
|
|
const ct = dm.createConstraint({
|
|
name: "交付时间",
|
|
description: "MVP 6 个月交付",
|
|
constraintType: dm.CONSTRAINT_TYPE.TIME,
|
|
});
|
|
assert.ok(ct.id.startsWith("ct-"));
|
|
assert.equal(ct.constraintType, "时间约束");
|
|
});
|
|
|
|
it("creates valid NFR", () => {
|
|
const nfr = dm.createNonFunctionalRequirement({
|
|
name: "页面响应时间",
|
|
description: "页面需快速响应",
|
|
category: dm.NFR_CATEGORY.PERFORMANCE,
|
|
metric: "P95 响应时间",
|
|
target: "< 500ms",
|
|
});
|
|
assert.ok(nfr.id.startsWith("nfr-"));
|
|
assert.equal(nfr.category, "性能需求");
|
|
});
|
|
|
|
it("creates valid PermissionRequirement", () => {
|
|
const pr = dm.createPermissionRequirement({
|
|
accessModel: dm.ACCESS_MODEL.HYBRID,
|
|
roles: [
|
|
{ name: dm.ROLE_TYPE.SUPER_ADMIN, description: "超级管理员" },
|
|
{ name: dm.ROLE_TYPE.USER, description: "普通用户" },
|
|
],
|
|
permissions: [
|
|
{ resource: "content", action: "read" },
|
|
{ resource: "content", action: "create" },
|
|
],
|
|
});
|
|
assert.equal(pr.accessModel, "Hybrid");
|
|
assert.equal(pr.roles.length, 2);
|
|
assert.equal(pr.permissions.length, 2);
|
|
});
|
|
|
|
it("creates valid IntegrationRequirement", () => {
|
|
const ir = dm.createIntegrationRequirement({
|
|
name: "支付集成",
|
|
category: dm.INTEGRATION_CATEGORY.PAYMENT,
|
|
description: "集成微信支付和支付宝",
|
|
necessity: "required",
|
|
});
|
|
assert.ok(ir.id.startsWith("ir-"));
|
|
assert.equal(ir.category, "支付系统");
|
|
});
|
|
|
|
it("creates valid DataRequirement (no DB design)", () => {
|
|
const dr = dm.createDataRequirement({
|
|
entityName: "用户 (User)",
|
|
description: "平台用户基本信息",
|
|
requiredFields: ["用户名", "邮箱", "密码哈希"],
|
|
relationships: ["用户 → 角色(多对多)"],
|
|
dataClassification: dm.DATA_CLASSIFICATION.PII,
|
|
lifecycle: [dm.DATA_LIFECYCLE.CREATE, dm.DATA_LIFECYCLE.READ],
|
|
owner: "用户本人 + 管理员",
|
|
complianceReqs: ["GDPR", "个人信息保护法"],
|
|
});
|
|
assert.ok(dr.id.startsWith("dr-"));
|
|
assert.equal(dr.dataClassification, "个人隐私数据");
|
|
assert.equal(dr.owner, "用户本人 + 管理员");
|
|
});
|
|
|
|
it("creates valid RiskRequirement with score calculation", () => {
|
|
const rr = dm.createRiskRequirement({
|
|
name: "第三方API不稳定",
|
|
description: "AI模型API可能不稳定",
|
|
level: dm.RISK_LEVEL.HIGH,
|
|
probability: 0.3,
|
|
impact: 0.8,
|
|
});
|
|
assert.equal(rr.riskScore, 0.24);
|
|
assert.equal(rr.level, "high");
|
|
});
|
|
|
|
it("creates valid TraceabilityLink", () => {
|
|
const tl = dm.createTraceabilityLink({
|
|
sourceId: "ep-1",
|
|
sourceType: "epic",
|
|
targetId: "ft-1",
|
|
targetType: "feature",
|
|
linkType: dm.LINK_TYPE.EPIC_TO_FEATURE,
|
|
});
|
|
assert.ok(tl.id.startsWith("tl-"));
|
|
assert.equal(tl.linkType, "epic→feature");
|
|
});
|
|
|
|
it("creates valid Priority with RICE scoring", () => {
|
|
const pri = dm.createPriority({
|
|
itemId: "ep-1",
|
|
itemType: "epic",
|
|
category: dm.PRIORITY_CATEGORY.MUST_HAVE,
|
|
method: dm.PRIORITY_METHOD.RICE,
|
|
score: 65,
|
|
scores: { reach: 80, impact: 3, confidence: 0.8, effort: 3 },
|
|
});
|
|
assert.equal(pri.category, "Must Have");
|
|
assert.equal(pri.method, "RICE");
|
|
// RICE: 80*3*0.8/3 = 64
|
|
assert.ok(Math.abs(pri.score - 64) <= 2, `Expected ~64, got ${pri.score}`);
|
|
});
|
|
|
|
it("creates valid Dependency", () => {
|
|
const dep = dm.createDependency({
|
|
fromId: "ep-2",
|
|
fromType: "epic",
|
|
toId: "ep-1",
|
|
toType: "epic",
|
|
depType: dm.DEPENDENCY_TYPE.REQUIRED_BY,
|
|
description: "Agent 管理需要用户管理",
|
|
});
|
|
assert.ok(dep.id.startsWith("dep-"));
|
|
assert.equal(dep.depType, "required_by");
|
|
});
|
|
|
|
it("creates valid RequirementVersion", () => {
|
|
const rv = dm.createRequirementVersion({
|
|
requirementId: "us-5",
|
|
requirementType: "story",
|
|
versionNumber: 2,
|
|
changeDescription: "更新了权限验证流程",
|
|
});
|
|
assert.equal(rv.versionNumber, 2);
|
|
});
|
|
|
|
it("creates complete RequirementPackage", () => {
|
|
const pkg = dm.createRequirementPackage({
|
|
productName: "AI客服平台",
|
|
strategyPackage: mockStrategyPackage(),
|
|
executiveSummary: { summary: "test" },
|
|
epics: [dm.createEpic({ title: "t", description: "d", category: dm.EPIC_CATEGORY.SYSTEM })],
|
|
});
|
|
assert.equal(pkg.productName, "AI客服平台");
|
|
assert.equal(pkg.epics.length, 1);
|
|
assert.equal(pkg.metadata.factoryId, "SF-02");
|
|
});
|
|
|
|
it("Throws on required fields missing", () => {
|
|
assert.throws(() => dm.createEpic({}), /title/);
|
|
assert.throws(() => dm.createUserStory({}), /role/);
|
|
assert.throws(() => dm.createAcceptanceCriteria({}), /storyId/);
|
|
assert.throws(() => dm.createBusinessRule({}), /name/);
|
|
assert.throws(() => dm.createConstraint({}), /name/);
|
|
assert.throws(() => dm.createNonFunctionalRequirement({}), /name/);
|
|
assert.throws(() => dm.createIntegrationRequirement({}), /name/);
|
|
assert.throws(() => dm.createDataRequirement({}), /entityName/);
|
|
assert.throws(() => dm.createDependency({}), /fromId/);
|
|
assert.throws(() => dm.createPriority({}), /itemId/);
|
|
assert.throws(() => dm.createTraceabilityLink({}), /sourceId/);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// B — Strategy Conversion Engine
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part B — Strategy → Requirement Conversion", () => {
|
|
|
|
it("converts AI platform strategy to requirements", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const result = convertStrategyToRequirements(strategy);
|
|
|
|
assert.ok(result.epics.length >= 5, `Expected >=5 epics, got ${result.epics.length}`);
|
|
assert.ok(result.features.length >= 15, `Expected >=15 features, got ${result.features.length}`);
|
|
assert.ok(result.userStories.length >= 40, `Expected >=40 stories, got ${result.userStories.length}`);
|
|
assert.ok(result.acceptanceCriteria.length >= 50, `Expected >=50 AC, got ${result.acceptanceCriteria.length}`);
|
|
assert.ok(result.businessRules.length >= 5);
|
|
assert.ok(result.nonFunctionalReqs.length >= 5);
|
|
assert.ok(result.integrationReqs.length >= 3, `Expected >=3 integration reqs, got ${result.integrationReqs.length}`);
|
|
assert.ok(result.dataReqs.length >= 3);
|
|
});
|
|
|
|
it("converts enterprise SaaS strategy", () => {
|
|
const strategy = mockStrategyPackage("企业SaaS");
|
|
const result = convertStrategyToRequirements(strategy);
|
|
assert.ok(result.epics.length >= 5);
|
|
assert.ok(result.features.length >= 15);
|
|
});
|
|
|
|
it("converts ecommerce strategy", () => {
|
|
const strategy = mockStrategyPackage("电商平台");
|
|
const result = convertStrategyToRequirements(strategy);
|
|
const epicTitles = result.epics.map(e => e.title);
|
|
assert.ok(epicTitles.some(t => t.includes("商品")), "Should have product management epic");
|
|
assert.ok(epicTitles.some(t => t.includes("订单")), "Should have order management epic");
|
|
});
|
|
|
|
it("converts education platform strategy", () => {
|
|
const strategy = mockStrategyPackage("教育平台");
|
|
const result = convertStrategyToRequirements(strategy);
|
|
const epicTitles = result.epics.map(e => e.title);
|
|
assert.ok(epicTitles.some(t => t.includes("课程")));
|
|
assert.ok(epicTitles.some(t => t.includes("学习")));
|
|
});
|
|
|
|
it("falls back to generic template for unknown category", () => {
|
|
const strategy = mockStrategyPackage("量子计算平台");
|
|
const result = convertStrategyToRequirements(strategy);
|
|
assert.ok(result.epics.length >= 5, `Expected >=5 epics for unknown category, got ${result.epics.length}`);
|
|
});
|
|
|
|
it("empty strategy returns empty requirements", () => {
|
|
const result = convertStrategyToRequirements({ metadata: { category: "通用" } });
|
|
assert.equal(result.epics.length, 6); // generic template has 6
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// C — Epic Generation
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part C — Epic Generation", () => {
|
|
|
|
it("generates AI platform epics with expected categories", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const epics = generateEpicsFromStrategy(strategy);
|
|
|
|
const titles = epics.map(e => e.title);
|
|
assert.ok(titles.includes("用户管理"), `Expected '用户管理', got: ${titles}`);
|
|
assert.ok(titles.includes("知识库管理"), `Expected '知识库管理', got: ${titles}`);
|
|
assert.ok(titles.includes("Agent管理"), `Expected 'Agent管理', got: ${titles}`);
|
|
assert.ok(titles.includes("对话管理"), `Expected '对话管理', got: ${titles}`);
|
|
assert.ok(titles.includes("权限管理"), `Expected '权限管理', got: ${titles}`);
|
|
assert.ok(titles.includes("运营分析"), `Expected '运营分析', got: ${titles}`);
|
|
assert.ok(titles.includes("系统设置"), `Expected '系统设置', got: ${titles}`);
|
|
});
|
|
|
|
it("each epic has required fields", () => {
|
|
const epics = generateEpicsFromStrategy(mockStrategyPackage());
|
|
for (const epic of epics) {
|
|
assert.ok(epic.id, "Epic must have id");
|
|
assert.ok(epic.title, "Epic must have title");
|
|
assert.ok(epic.description, "Epic must have description");
|
|
assert.ok(epic.category, "Epic must have category");
|
|
assert.ok(epic.objective, "Epic must have objective");
|
|
assert.ok(epic.successMetric, "Epic must have successMetric");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// D — Feature Generation
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part D — Feature Generation", () => {
|
|
|
|
it("generates features from knowledge base epic", () => {
|
|
const epic = dm.createEpic({
|
|
title: "知识库管理", description: "知识管理", category: dm.EPIC_CATEGORY.CONTENT_MANAGEMENT,
|
|
objective: "构建知识体系",
|
|
});
|
|
const features = generateFeaturesFromEpic(epic);
|
|
|
|
assert.ok(features.length >= 3, `Expected >=3 features, got ${features.length}`);
|
|
const titles = features.map(f => f.title);
|
|
assert.ok(titles.includes("知识库创建"));
|
|
assert.ok(titles.includes("文档上传"));
|
|
assert.ok(titles.includes("索引构建"));
|
|
});
|
|
|
|
it("generates features from user management epic", () => {
|
|
const epic = dm.createEpic({
|
|
title: "用户管理", description: "用户管理", category: dm.EPIC_CATEGORY.USER_MANAGEMENT,
|
|
objective: "用户身份管理",
|
|
});
|
|
const features = generateFeaturesFromEpic(epic);
|
|
|
|
const titles = features.map(f => f.title);
|
|
assert.ok(titles.includes("用户注册"));
|
|
assert.ok(titles.includes("用户登录"));
|
|
assert.ok(titles.includes("个人信息管理"));
|
|
});
|
|
|
|
it("all features reference correct epicId", () => {
|
|
const epic = dm.createEpic({
|
|
title: "知识库管理", description: "d", category: dm.EPIC_CATEGORY.CONTENT_MANAGEMENT,
|
|
objective: "o",
|
|
});
|
|
const features = generateFeaturesFromEpic(epic);
|
|
for (const f of features) {
|
|
assert.equal(f.epicId, epic.id);
|
|
}
|
|
});
|
|
|
|
it("generates fallback features for unknown epic titles", () => {
|
|
const epic = dm.createEpic({
|
|
title: "宇宙探索", description: "d", category: dm.EPIC_CATEGORY.SYSTEM,
|
|
objective: "o",
|
|
});
|
|
const features = generateFeaturesFromEpic(epic);
|
|
assert.ok(features.length >= 3);
|
|
assert.ok(features[0].title.includes("宇宙探索"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// E — User Story Generation
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part E — User Story Generation", () => {
|
|
|
|
it("generates CRUD stories", () => {
|
|
const feature = dm.createFeature({
|
|
title: "文档管理", description: "d", epicId: "ep-1", featureType: "CRUD",
|
|
});
|
|
const stories = generateUserStoriesFromFeature(feature, { title: "知识库" }, ["客服人员", "管理员"]);
|
|
|
|
assert.ok(stories.length >= 4);
|
|
const goals = stories.map(s => s.goal);
|
|
assert.ok(goals.some(g => g.includes("浏览")), `Should have browse story, got: ${goals}`);
|
|
assert.ok(goals.some(g => g.includes("创建")), `Should have create story, got: ${goals}`);
|
|
assert.ok(goals.some(g => g.includes("编辑")), `Should have edit story, got: ${goals}`);
|
|
});
|
|
|
|
it("generates workflow stories", () => {
|
|
const feature = dm.createFeature({
|
|
title: "审批流程", description: "d", epicId: "ep-1", featureType: "workflow",
|
|
});
|
|
const stories = generateUserStoriesFromFeature(feature, { title: "业务流程" });
|
|
assert.ok(stories.length >= 3);
|
|
const goals = stories.map(s => s.goal);
|
|
assert.ok(goals.some(g => g.includes("发起")), `Should have initiate story, got: ${goals}`);
|
|
});
|
|
|
|
it("generates analytics stories", () => {
|
|
const feature = dm.createFeature({
|
|
title: "数据看板", description: "d", epicId: "ep-1", featureType: "analytics",
|
|
});
|
|
const stories = generateUserStoriesFromFeature(feature, { title: "运营分析" });
|
|
assert.ok(stories.length >= 3);
|
|
const goals = stories.map(s => s.goal);
|
|
assert.ok(goals.some(g => g.includes("导出")), `Should have export story, got: ${goals}`);
|
|
});
|
|
|
|
it("all stories use As a/I want/So that format", () => {
|
|
const feature = dm.createFeature({
|
|
title: "知识编辑", description: "d", epicId: "ep-1", featureType: "CRUD",
|
|
});
|
|
const stories = generateUserStoriesFromFeature(feature, { title: "知识库" }, ["用户"]);
|
|
for (const story of stories) {
|
|
assert.ok(story.statement.includes("As a "));
|
|
assert.ok(story.statement.includes("I want "));
|
|
assert.ok(story.statement.includes("so that "));
|
|
}
|
|
});
|
|
|
|
it("assigns correct priority distribution", () => {
|
|
const feature = dm.createFeature({
|
|
title: "文档管理", description: "d", epicId: "ep-1", featureType: "CRUD",
|
|
});
|
|
const stories = generateUserStoriesFromFeature(feature, { title: "知识库" }, ["用户", "管理员"]);
|
|
const mustHave = stories.filter(s => s.priority === "Must Have");
|
|
const shouldHave = stories.filter(s => s.priority === "Should Have");
|
|
const couldHave = stories.filter(s => s.priority === "Could Have");
|
|
|
|
assert.ok(mustHave.length >= 1, "Must have at least 1 Must Have");
|
|
assert.ok(shouldHave.length >= 1, "Must have at least 1 Should Have");
|
|
assert.ok(couldHave.length >= 1, "Must have at least 1 Could Have");
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// F — Acceptance Criteria
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part F — Acceptance Criteria", () => {
|
|
|
|
it("generates 3-10 ACs per story", () => {
|
|
const story = dm.createUserStory({
|
|
role: "用户", goal: "创建文档", value: "管理知识",
|
|
featureId: "ft-1", epicId: "ep-1",
|
|
});
|
|
const acs = generateAcceptanceCriteriaForStory(story);
|
|
assert.ok(acs.length >= 3, `Expected >=3 AC, got ${acs.length}`);
|
|
assert.ok(acs.length <= 12);
|
|
});
|
|
|
|
it("all ACs use Given/When/Then format", () => {
|
|
const story = dm.createUserStory({
|
|
role: "用户", goal: "搜索知识", value: "快速查找",
|
|
featureId: "ft-1", epicId: "ep-1",
|
|
});
|
|
const acs = generateAcceptanceCriteriaForStory(story);
|
|
for (const ac of acs) {
|
|
assert.ok(ac.given, `AC missing given: ${ac.scenario}`);
|
|
assert.ok(ac.when, `AC missing when: ${ac.scenario}`);
|
|
assert.ok(ac.then, `AC missing then: ${ac.scenario}`);
|
|
}
|
|
});
|
|
|
|
it("covers normal flow, error flow, edge cases", () => {
|
|
const story = dm.createUserStory({
|
|
role: "用户", goal: "上传文档", value: "添加知识",
|
|
featureId: "ft-1", epicId: "ep-1",
|
|
});
|
|
const acs = generateAcceptanceCriteriaForStory(story);
|
|
const scenarios = acs.map(a => a.scenario);
|
|
assert.ok(scenarios.some(s => s.includes("正常")), "Should have normal flow");
|
|
assert.ok(scenarios.some(s => s.includes("空数据") || s.includes("验证")), "Should have edge case");
|
|
assert.ok(scenarios.some(s => s.includes("权限")), "Should have permission check");
|
|
});
|
|
|
|
it("delete stories have delete-specific ACs", () => {
|
|
const story = dm.createUserStory({
|
|
role: "管理员", goal: "删除知识库", value: "清理无效数据",
|
|
featureId: "ft-1", epicId: "ep-1",
|
|
});
|
|
const acs = generateAcceptanceCriteriaForStory(story);
|
|
const scenarios = acs.map(a => a.scenario);
|
|
assert.ok(scenarios.some(s => s.includes("确认")), `Expected delete confirmation, got: ${scenarios}`);
|
|
assert.ok(scenarios.some(s => s.includes("关联")), `Expected related data check, got: ${scenarios}`);
|
|
});
|
|
|
|
it("search stories have search-specific ACs", () => {
|
|
const story = dm.createUserStory({
|
|
role: "用户", goal: "搜索文档内容", value: "快速定位信息",
|
|
featureId: "ft-1", epicId: "ep-1",
|
|
});
|
|
const acs = generateAcceptanceCriteriaForStory(story);
|
|
const scenarios = acs.map(a => a.scenario);
|
|
assert.ok(scenarios.some(s => s.includes("搜索结果") || s.includes("无搜索")), `Expected search scenarios, got: ${scenarios}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// G — Business Rules
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part G — Business Rules", () => {
|
|
|
|
it("generates standard business rules", () => {
|
|
const rules = generateBusinessRules(mockStrategyPackage());
|
|
assert.ok(rules.length >= 5, `Expected >=5 rules, got ${rules.length}`);
|
|
});
|
|
|
|
it("covers all rule types", () => {
|
|
const rules = generateBusinessRules(mockStrategyPackage());
|
|
const types = rules.map(r => r.ruleType);
|
|
assert.ok(types.includes("验证规则"), `Expected validation rule, got: ${types}`);
|
|
assert.ok(types.includes("业务约束"), `Expected constraint rule, got: ${types}`);
|
|
assert.ok(types.includes("流程规则"), `Expected process rule, got: ${types}`);
|
|
assert.ok(types.includes("审批规则"), `Expected approval rule, got: ${types}`);
|
|
assert.ok(types.includes("状态机规则"), `Expected state machine rule, got: ${types}`);
|
|
});
|
|
|
|
it("each business rule has condition and action", () => {
|
|
const rules = generateBusinessRules(mockStrategyPackage());
|
|
for (const r of rules) {
|
|
assert.ok(r.condition, `Rule ${r.name} missing condition`);
|
|
assert.ok(r.action, `Rule ${r.name} missing action`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// H — Permission Requirements
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part H — Permission Requirements", () => {
|
|
|
|
it("generates permission model with all standard roles", () => {
|
|
const pr = generatePermissionRequirements(mockStrategyPackage());
|
|
assert.equal(pr.accessModel, "Hybrid");
|
|
|
|
const roleNames = pr.roles.map(r => r.name);
|
|
assert.ok(roleNames.includes("超级管理员"));
|
|
assert.ok(roleNames.includes("管理员"));
|
|
assert.ok(roleNames.includes("运营人员"));
|
|
assert.ok(roleNames.includes("普通用户"));
|
|
assert.ok(roleNames.includes("访客"));
|
|
assert.ok(roleNames.includes("审计员"));
|
|
});
|
|
|
|
it("provides role-permission mappings", () => {
|
|
const pr = generatePermissionRequirements(mockStrategyPackage());
|
|
assert.ok(pr.rolePermissions.length >= 4);
|
|
const superAdmin = pr.rolePermissions.find(rp => rp.role === "超级管理员");
|
|
assert.deepEqual(superAdmin.permissions, ["*"]);
|
|
});
|
|
|
|
it("AI platform has agent/conversation resources", () => {
|
|
const pr = generatePermissionRequirements(mockStrategyPackage("AI平台"));
|
|
assert.ok(pr.resourceTypes.includes("agent"));
|
|
assert.ok(pr.resourceTypes.includes("conversation"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// I — Non-Functional Requirements
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part I — NFR Generation", () => {
|
|
|
|
it("generates NFRs covering all categories", () => {
|
|
const nfrs = generateNFRs(mockStrategyPackage());
|
|
assert.ok(nfrs.length >= 8);
|
|
|
|
const categories = nfrs.map(n => n.category);
|
|
assert.ok(categories.includes("性能需求"));
|
|
assert.ok(categories.includes("安全需求"));
|
|
assert.ok(categories.includes("可靠性需求"));
|
|
assert.ok(categories.includes("扩展性需求"));
|
|
assert.ok(categories.includes("可观测性需求"));
|
|
});
|
|
|
|
it("each NFR has quantifiable metric and target", () => {
|
|
const nfrs = generateNFRs(mockStrategyPackage());
|
|
for (const nfr of nfrs) {
|
|
assert.ok(nfr.metric, `NFR ${nfr.name} missing metric`);
|
|
assert.ok(nfr.target, `NFR ${nfr.name} missing target`);
|
|
}
|
|
});
|
|
|
|
it("enterprise scope raises availability target", () => {
|
|
const enterpriseStrategy = mockStrategyPackage();
|
|
enterpriseStrategy.roadmap.phases.push({
|
|
phase: "enterprise", scope: "Enterprise", durationMonths: 4, budget: 500000,
|
|
});
|
|
const nfrs = generateNFRs(enterpriseStrategy);
|
|
const availability = nfrs.find(n => n.name === "系统可用性");
|
|
assert.ok(availability.target.includes("99.99"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// J — Integration Requirements
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part J — Integration Requirements", () => {
|
|
|
|
it("generates AI platform integration requirements", () => {
|
|
const irs = generateIntegrationRequirements("AI平台");
|
|
assert.ok(irs.length >= 4);
|
|
const names = irs.map(ir => ir.name);
|
|
assert.ok(names.some(n => n.includes("AI模型")));
|
|
assert.ok(names.some(n => n.includes("邮件")));
|
|
assert.ok(names.some(n => n.includes("搜索")));
|
|
});
|
|
|
|
it("generates ecommerce integration requirements", () => {
|
|
const irs = generateIntegrationRequirements("电商平台");
|
|
const names = irs.map(ir => ir.name);
|
|
assert.ok(names.some(n => n.includes("支付")));
|
|
});
|
|
|
|
it("each integration requirement has providers listed", () => {
|
|
const irs = generateIntegrationRequirements("AI平台");
|
|
for (const ir of irs) {
|
|
if (ir.necessity === "required") {
|
|
assert.ok(ir.providers.length >= 1, `${ir.name} should have providers`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// K — Data Requirements
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part K — Data Requirements", () => {
|
|
|
|
it("generates data requirements from epics (no DB design)", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const epics = generateEpicsFromStrategy(strategy);
|
|
const dataReqs = generateDataRequirements(epics, strategy);
|
|
|
|
assert.ok(dataReqs.length >= 4);
|
|
|
|
// Verify it's NOT a database design (no column types, no normalization)
|
|
for (const dr of dataReqs) {
|
|
assert.ok(dr.entityName, "Must have entity name");
|
|
assert.ok(dr.description, "Must have business description");
|
|
assert.ok(dr.dataClassification, "Must have data classification");
|
|
assert.ok(dr.owner, "Must have data owner");
|
|
assert.ok(Array.isArray(dr.lifecycle));
|
|
// Must NOT contain SQL or table definition patterns
|
|
const allKeys = JSON.stringify(dr).toLowerCase();
|
|
assert.ok(!allKeys.includes("varchar"), "Must not contain SQL types");
|
|
assert.ok(!allKeys.includes("primary key"), "Must not contain DB design");
|
|
}
|
|
});
|
|
|
|
it("includes user data requirement with PII classification", () => {
|
|
const dataReqs = generateDataRequirements(
|
|
generateEpicsFromStrategy(mockStrategyPackage()),
|
|
mockStrategyPackage()
|
|
);
|
|
const userReq = dataReqs.find(d => d.entityName.includes("用户"));
|
|
assert.ok(userReq);
|
|
assert.equal(userReq.dataClassification, "个人隐私数据");
|
|
assert.ok(userReq.complianceReqs.some(c => c.includes("GDPR")));
|
|
});
|
|
|
|
it("includes audit log requirement", () => {
|
|
const dataReqs = generateDataRequirements(
|
|
generateEpicsFromStrategy(mockStrategyPackage()),
|
|
mockStrategyPackage()
|
|
);
|
|
const logReq = dataReqs.find(d => d.entityName.includes("日志"));
|
|
assert.ok(logReq, "Must include audit log data requirement");
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// L — Dependency Graph
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part L — Dependency Graph", () => {
|
|
|
|
it("builds dependency graph from requirements", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
const { dependencies, graph } = buildDependencyGraph(req);
|
|
|
|
assert.ok(dependencies.length >= 5, `Expected >=5 dependencies, got ${dependencies.length}`);
|
|
assert.ok(graph.nodes.length > 0);
|
|
assert.ok(graph.edges.length > 0);
|
|
});
|
|
|
|
it("establishes epic-level dependencies", () => {
|
|
const req = convertStrategyToRequirements(mockStrategyPackage("AI平台"));
|
|
const { dependencies } = buildDependencyGraph(req);
|
|
|
|
const epicDeps = dependencies.filter(d => d.fromType === "epic" && d.toType === "epic");
|
|
assert.ok(epicDeps.length >= 2, `Expected >=2 epic deps, got ${epicDeps.length}`);
|
|
});
|
|
|
|
it("establishes story-level CRUD dependencies", () => {
|
|
const req = convertStrategyToRequirements(mockStrategyPackage("AI平台"));
|
|
const { dependencies } = buildDependencyGraph(req);
|
|
|
|
const storyDeps = dependencies.filter(d => d.fromType === "story");
|
|
assert.ok(storyDeps.length >= 3, `Expected >=3 story deps, got ${storyDeps.length}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// M — Priority Matrix
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part M — Priority Matrix", () => {
|
|
|
|
it("MoSCoW prioritization produces all 4 categories", () => {
|
|
const items = Array.from({ length: 20 }, (_, i) => ({
|
|
id: `item-${i}`, title: `Item ${i}`,
|
|
}));
|
|
const priorities = prioritizeByMoSCoW(items, "story", mockStrategyPackage());
|
|
|
|
assert.equal(priorities.length, 20);
|
|
const mustHave = priorities.filter(p => p.category === "Must Have");
|
|
const shouldHave = priorities.filter(p => p.category === "Should Have");
|
|
const couldHave = priorities.filter(p => p.category === "Could Have");
|
|
const wontHave = priorities.filter(p => p.category === "Won't Have");
|
|
|
|
assert.ok(mustHave.length > 0, "Must have Must Have items");
|
|
assert.ok(shouldHave.length > 0, "Must have Should Have items");
|
|
assert.ok(couldHave.length > 0, "Must have Could Have items");
|
|
assert.ok(wontHave.length > 0, "Must have Won't Have items");
|
|
});
|
|
|
|
it("system setup epic is always Must Have", () => {
|
|
const items = [
|
|
{ id: "ep-1", title: "系统设置" },
|
|
{ id: "ep-2", title: "用户管理" },
|
|
];
|
|
const priorities = prioritizeByMoSCoW(items, "epic", mockStrategyPackage());
|
|
const sysEpic = priorities.find(p => p.itemId === "ep-1");
|
|
assert.equal(sysEpic.category, "Must Have", "System setup should be Must Have");
|
|
});
|
|
|
|
it("RICE prioritization calculates correct formula", () => {
|
|
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
id: `item-${i}`, title: `Item ${i}`,
|
|
}));
|
|
const priorities = prioritizeByRICE(items, "feature", mockStrategyPackage());
|
|
|
|
assert.equal(priorities.length, 10);
|
|
for (const p of priorities) {
|
|
assert.ok(p.scores.reach > 0);
|
|
assert.ok(p.scores.impact > 0);
|
|
assert.ok(p.scores.confidence > 0);
|
|
assert.ok(p.scores.effort > 0);
|
|
}
|
|
});
|
|
|
|
it("WSJF prioritization calculates CoD/Duration", () => {
|
|
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
id: `item-${i}`, title: `Item ${i}`,
|
|
}));
|
|
const priorities = prioritizeByWSJF(items, "story");
|
|
|
|
assert.equal(priorities.length, 10);
|
|
for (const p of priorities) {
|
|
assert.ok(p.scores.costOfDelay > 0);
|
|
assert.ok(p.scores.jobDuration > 0);
|
|
}
|
|
});
|
|
|
|
it("buildPriorityMatrix returns combined results", () => {
|
|
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
id: `item-${i}`, title: `Item ${i}`,
|
|
}));
|
|
const { priorities, matrix } = buildPriorityMatrix(items, "feature", mockStrategyPackage());
|
|
|
|
assert.equal(priorities.length, 10);
|
|
assert.ok(matrix.mustHave.length > 0);
|
|
assert.ok(matrix.shouldHave.length > 0);
|
|
assert.ok(matrix.couldHave.length > 0);
|
|
assert.ok(matrix.wontHave.length > 0);
|
|
|
|
const total = Object.values(matrix.summary).reduce((a, b) => a + b, 0);
|
|
assert.equal(total, 10);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// N — MVP Scope
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part N — MVP Scope", () => {
|
|
|
|
it("generates MVP scope aligned with roadmap", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
const epicPri = buildPriorityMatrix(req.epics, "epic", strategy);
|
|
const featurePri = buildPriorityMatrix(req.features, "feature", strategy);
|
|
const storyPri = buildPriorityMatrix(req.userStories, "story", strategy);
|
|
|
|
const mvpScope = generateMVPScope({
|
|
strategyPackage: strategy,
|
|
epicPriorities: epicPri.priorities,
|
|
featurePriorities: featurePri.priorities,
|
|
storyPriorities: storyPri.priorities,
|
|
epics: req.epics,
|
|
features: req.features,
|
|
userStories: req.userStories,
|
|
});
|
|
|
|
assert.ok(mvpScope.phases.length >= 4);
|
|
assert.ok(mvpScope.mvp.userStories > 0, `MVP must have stories, got ${mvpScope.mvp.userStories}`);
|
|
assert.ok(mvpScope.mvp.totalStoryPoints > 0);
|
|
|
|
// V1 >= MVP
|
|
assert.ok(mvpScope.v1.userStories >= mvpScope.mvp.userStories,
|
|
`V1 stories (${mvpScope.v1.userStories}) must >= MVP stories (${mvpScope.mvp.userStories})`);
|
|
|
|
// V2 >= V1
|
|
assert.ok(mvpScope.v2.userStories >= mvpScope.v1.userStories);
|
|
});
|
|
|
|
it("MVP only contains Must Have items", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
const storyPri = buildPriorityMatrix(req.userStories, "story", strategy);
|
|
|
|
const mvpScope = generateMVPScope({
|
|
strategyPackage: strategy,
|
|
epicPriorities: [],
|
|
featurePriorities: [],
|
|
storyPriorities: storyPri.priorities,
|
|
epics: req.epics,
|
|
features: req.features,
|
|
userStories: req.userStories,
|
|
});
|
|
|
|
const mustHaveStories = storyPri.priorities.filter(p => p.category === "Must Have");
|
|
assert.equal(mvpScope.mvp.userStories, mustHaveStories.length,
|
|
`MVP should have ${mustHaveStories.length} stories, got ${mvpScope.mvp.userStories}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// O — Traceability Matrix
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part O — Traceability", () => {
|
|
|
|
it("builds complete traceability matrix from requirements", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
const { links, matrix } = buildTraceabilityMatrix(req);
|
|
|
|
assert.ok(links.length >= 20, `Expected >=20 traceability links, got ${links.length}`);
|
|
assert.ok(matrix.strategyToEpic > 0);
|
|
assert.ok(matrix.epicToFeature > 0);
|
|
assert.ok(matrix.featureToStory > 0);
|
|
assert.ok(matrix.storyToAC > 0);
|
|
});
|
|
|
|
it("traces every epic back to strategy", () => {
|
|
const req = convertStrategyToRequirements(mockStrategyPackage("AI平台"));
|
|
const { links } = buildTraceabilityMatrix(req);
|
|
|
|
const epicIds = req.epics.map(e => e.id);
|
|
const tracedEpics = new Set(
|
|
links.filter(l => l.linkType === "strategy→epic").map(l => l.targetId)
|
|
);
|
|
assert.equal(tracedEpics.size, epicIds.length,
|
|
`All ${epicIds.length} epics should be traced, got ${tracedEpics.size}`);
|
|
});
|
|
|
|
it("traces every feature to an epic", () => {
|
|
const req = convertStrategyToRequirements(mockStrategyPackage("AI平台"));
|
|
const { links } = buildTraceabilityMatrix(req);
|
|
|
|
const featureIds = req.features.map(f => f.id);
|
|
const tracedFeatures = new Set(
|
|
links.filter(l => l.linkType === "epic→feature").map(l => l.targetId)
|
|
);
|
|
assert.equal(tracedFeatures.size, featureIds.length,
|
|
`All features should be traced to epics`);
|
|
});
|
|
|
|
it("traces every story to a feature", () => {
|
|
const req = convertStrategyToRequirements(mockStrategyPackage("AI平台"));
|
|
const { links } = buildTraceabilityMatrix(req);
|
|
|
|
const storyIds = req.userStories.map(s => s.id);
|
|
const tracedStories = new Set(
|
|
links.filter(l => l.linkType === "feature→story").map(l => l.targetId)
|
|
);
|
|
assert.equal(tracedStories.size, storyIds.length,
|
|
`All stories should be traced to features`);
|
|
});
|
|
|
|
it("every acceptance criteria is traced to its story", () => {
|
|
const req = convertStrategyToRequirements(mockStrategyPackage("AI平台"));
|
|
const { links } = buildTraceabilityMatrix(req);
|
|
|
|
const acIds = req.acceptanceCriteria.map(a => a.id);
|
|
const tracedACs = new Set(
|
|
links.filter(l => l.linkType === "story→ac").map(l => l.targetId)
|
|
);
|
|
assert.equal(tracedACs.size, acIds.length,
|
|
`All ${acIds.length} ACs should be traced, got ${tracedACs.size}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// P — Requirement Package
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part P — Requirement Package", () => {
|
|
|
|
it("generates complete requirement report markdown", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
const epics = req.epics;
|
|
const report = generateRequirementReport({
|
|
productName: "AI客服平台",
|
|
strategyPackage: strategy,
|
|
executiveSummary: { epicCount: epics.length },
|
|
epics: req.epics,
|
|
features: req.features,
|
|
userStories: req.userStories,
|
|
acceptanceCriteria: req.acceptanceCriteria,
|
|
businessRules: req.businessRules,
|
|
nonFunctionalReqs: req.nonFunctionalReqs,
|
|
integrationReqs: req.integrationReqs,
|
|
dataReqs: req.dataReqs,
|
|
permissionReqs: [req.permissionReqs],
|
|
});
|
|
|
|
assert.ok(report.includes("Requirement Package: AI客服平台"));
|
|
assert.ok(report.includes("Executive Summary"));
|
|
assert.ok(report.includes("Epic Catalog"));
|
|
assert.ok(report.includes("User Story Catalog"));
|
|
assert.ok(report.includes("MVP Scope"));
|
|
assert.ok(report.includes("Priority Distribution"));
|
|
assert.ok(report.includes("Traceability Coverage"));
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// Q — Factory Integration (PR-36)
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part Q — Factory Integration (PR-36)", () => {
|
|
|
|
it("registers in PR-36 Factory Registry", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF02(registry);
|
|
|
|
const sf02 = registry.get("SF-02");
|
|
assert.equal(sf02.id, "SF-02");
|
|
assert.equal(sf02.status, "active");
|
|
assert.equal(sf02.domain, "requirement-engineering");
|
|
});
|
|
|
|
it("executes SF-02 factory with SF-01 output as input", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
// Step 1: Run SF-01
|
|
const sf01Input = createFactoryInput({
|
|
factoryId: "SF-01",
|
|
parameters: { idea: "开发一个AI Agent平台" },
|
|
});
|
|
const sf01Output = registry.execute("SF-01", sf01Input);
|
|
assert.equal(sf01Output.status, "passed", `SF-01 should pass, got: ${sf01Output.status}`);
|
|
|
|
// Step 2: Run SF-02 with SF-01 output
|
|
const sf02Input = createFactoryInput({
|
|
factoryId: "SF-02",
|
|
upstreamArtifacts: sf01Output.artifacts,
|
|
});
|
|
const sf02Output = registry.execute("SF-02", sf02Input);
|
|
|
|
assert.equal(sf02Output.status, "passed", `SF-02 should pass, got: ${sf02Output.status} — ${sf02Output.error || ''}`);
|
|
assert.ok(sf02Output.artifacts.length >= 10, `Expected >=10 artifacts, got ${sf02Output.artifacts.length}`);
|
|
assert.ok(sf02Output.metrics.length >= 8);
|
|
});
|
|
|
|
it("SF-02 fails with no upstream artifacts", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF02(registry);
|
|
|
|
const input = createFactoryInput({
|
|
factoryId: "SF-02",
|
|
upstreamArtifacts: [],
|
|
});
|
|
const output = registry.execute("SF-02", input);
|
|
|
|
assert.equal(output.status, "failed");
|
|
assert.ok(output.error.includes("SF-01"));
|
|
});
|
|
|
|
it("factory output contains all required artifact types", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
const sf01Output = registry.execute("SF-01", createFactoryInput({
|
|
factoryId: "SF-01", parameters: { idea: "AI Agent平台" },
|
|
}));
|
|
const sf02Output = registry.execute("SF-02", createFactoryInput({
|
|
factoryId: "SF-02", upstreamArtifacts: sf01Output.artifacts,
|
|
}));
|
|
|
|
const artifactTypes = sf02Output.artifacts.map(a => a.metadata?.artifactType);
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.REQUIREMENT_PACKAGE));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.EPIC_CATALOG));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.FEATURE_CATALOG));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.USER_STORY_CATALOG));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.ACCEPTANCE_CRITERIA_PACKAGE));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.BUSINESS_RULES_CATALOG));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.PERMISSION_REQUIREMENTS));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.NFR_PACKAGE));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.INTEGRATION_REQUIREMENTS));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.DATA_REQUIREMENTS));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.DEPENDENCY_GRAPH));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.PRIORITY_MATRIX));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.MVP_SCOPE));
|
|
assert.ok(artifactTypes.includes(SF02_ARTIFACTS.TRACEABILITY_MATRIX));
|
|
});
|
|
|
|
it("SF-02 has correct dependency on SF-01 in registry", () => {
|
|
const registry = createFactoryRegistry();
|
|
const deps = registry.getDependencies("SF-02");
|
|
assert.equal(deps.length, 1);
|
|
assert.equal(deps[0].from, "SF-01");
|
|
assert.equal(deps[0].kind, "data");
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// R — Artifact Graph Registration
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part R — Artifact Graph", () => {
|
|
|
|
it("registers SF-02 artifacts in artifact graph", () => {
|
|
const graph = createArtifactGraph();
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
const sf01Out = registry.execute("SF-01", createFactoryInput({
|
|
factoryId: "SF-01", parameters: { idea: "AI Agent平台" },
|
|
}));
|
|
const sf02Out = registry.execute("SF-02", createFactoryInput({
|
|
factoryId: "SF-02", upstreamArtifacts: sf01Out.artifacts,
|
|
}));
|
|
|
|
// Register SF-02 artifacts as ArtifactGraph nodes
|
|
for (const art of sf02Out.artifacts) {
|
|
const node = createArtifactNode({
|
|
kind: art.kind,
|
|
name: art.name,
|
|
factoryId: art.factoryId,
|
|
content: art.content,
|
|
metadata: art.metadata,
|
|
});
|
|
graph.addNode(node);
|
|
}
|
|
|
|
const stats = graph.stats();
|
|
assert.ok(stats.totalNodes >= 10, `Expected >=10 graph nodes, got ${stats.totalNodes}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// S — Runtime Integration (PR-35)
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part S — Runtime Integration (PR-35)", () => {
|
|
|
|
it("executes within PR-35 pipeline context", () => {
|
|
const task = createTask({ id: "T-SF02-001", title: "SF-02 Requirement Engineering" });
|
|
const plan = createExecutionPlan({ taskId: task.id, steps: [] });
|
|
const ctx = createRuntimeContext({ task, plan });
|
|
|
|
// Run SF-01 first
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
const sf01Out = registry.execute("SF-01", createFactoryInput({
|
|
factoryId: "SF-01", parameters: { idea: "智能客服系统" },
|
|
}));
|
|
|
|
// Then SF-02
|
|
const sf02Out = registry.execute("SF-02", createFactoryInput({
|
|
factoryId: "SF-02", upstreamArtifacts: sf01Out.artifacts,
|
|
}));
|
|
|
|
assert.equal(sf02Out.status, "passed");
|
|
assert.ok(sf02Out.artifacts.length >= 10);
|
|
assert.equal(sf02Out.metrics.length, 11, `Expected 11 metrics, got ${sf02Out.metrics.length}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// T — SF-01 Compatibility
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part T — SF-01 Compatibility", () => {
|
|
|
|
it("SF-02 correctly consumes SF-01 output artifacts", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
// Test with multiple product ideas
|
|
const ideas = ["AI Agent平台", "电商SaaS平台", "在线教育平台"];
|
|
for (const idea of ideas) {
|
|
const sf01Out = registry.execute("SF-01", createFactoryInput({
|
|
factoryId: "SF-01", parameters: { idea },
|
|
}));
|
|
assert.equal(sf01Out.status, "passed", `SF-01 should pass for: ${idea}`);
|
|
|
|
const sf02Out = registry.execute("SF-02", createFactoryInput({
|
|
factoryId: "SF-02", upstreamArtifacts: sf01Out.artifacts,
|
|
}));
|
|
assert.equal(sf02Out.status, "passed", `SF-02 should pass for: ${idea} — ${sf02Out.error || ''}`);
|
|
assert.ok(sf02Out.artifacts.length >= 10, `${idea}: expected >=10 artifacts, got ${sf02Out.artifacts.length}`);
|
|
}
|
|
});
|
|
|
|
it("roadmap phase alignment: SF-01 → SF-02 MVP scope", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF01(registry);
|
|
registerSF02(registry);
|
|
|
|
const sf01Out = registry.execute("SF-01", createFactoryInput({
|
|
factoryId: "SF-01", parameters: { idea: "AI Agent平台" },
|
|
}));
|
|
|
|
// Extract roadmap from SF-01
|
|
const roadmapArtifact = sf01Out.artifacts.find(a => a.metadata?.artifactType === "roadmap-report");
|
|
assert.ok(roadmapArtifact, "SF-01 must produce roadmap");
|
|
|
|
const sf02Out = registry.execute("SF-02", createFactoryInput({
|
|
factoryId: "SF-02", upstreamArtifacts: sf01Out.artifacts,
|
|
}));
|
|
|
|
// Check MVP scope artifact
|
|
const mvpArtifact = sf02Out.artifacts.find(a => a.metadata?.artifactType === SF02_ARTIFACTS.MVP_SCOPE);
|
|
assert.ok(mvpArtifact, "SF-02 must produce MVP scope artifact");
|
|
|
|
// Verify the scope metadata shows MVP stories
|
|
assert.ok(mvpArtifact.metadata.mvpStories > 0, "MVP should have stories");
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// U — Edge Cases
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part U — Edge Cases", () => {
|
|
|
|
it("handles empty strategy package gracefully", () => {
|
|
const result = convertStrategyToRequirements({ metadata: { category: "通用" } });
|
|
assert.ok(result.epics.length >= 5); // falls back to generic template
|
|
assert.ok(result.features.length > 0);
|
|
assert.ok(result.userStories.length > 0);
|
|
});
|
|
|
|
it("handles strategy with missing roadmap", () => {
|
|
const strategy = { metadata: { category: "AI平台" } };
|
|
const result = convertStrategyToRequirements(strategy);
|
|
assert.ok(result.epics.length >= 5);
|
|
});
|
|
|
|
it("handles extremely large strategy (stress test)", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
strategy.roadmap.phases.push(
|
|
{ phase: "v3", scope: "V3", durationMonths: 3, budget: 300000 },
|
|
{ phase: "v4", scope: "V4", durationMonths: 3, budget: 300000 },
|
|
{ phase: "v5", scope: "V5", durationMonths: 3, budget: 300000 },
|
|
);
|
|
const result = convertStrategyToRequirements(strategy);
|
|
assert.ok(result.userStories.length >= 40);
|
|
assert.ok(result.acceptanceCriteria.length >= 50);
|
|
});
|
|
|
|
it("no SQL or code artifacts in output", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
const allContent = JSON.stringify(req);
|
|
assert.ok(!allContent.includes("CREATE TABLE"), "Must not contain SQL DDL");
|
|
assert.ok(!allContent.includes("import React"), "Must not contain React code");
|
|
assert.ok(!allContent.includes("function Component"), "Must not contain component code");
|
|
assert.ok(!allContent.includes("PRIMARY KEY"), "Must not contain DB design");
|
|
assert.ok(!allContent.includes("<div"), "Must not contain HTML/JSX");
|
|
});
|
|
|
|
it("generates valid IDs with correct prefixes", () => {
|
|
const strategy = mockStrategyPackage("AI平台");
|
|
const req = convertStrategyToRequirements(strategy);
|
|
|
|
for (const e of req.epics) assert.ok(e.id.startsWith("ep-"));
|
|
for (const f of req.features) assert.ok(f.id.startsWith("ft-"));
|
|
for (const s of req.userStories) assert.ok(s.id.startsWith("us-"));
|
|
for (const ac of req.acceptanceCriteria) assert.ok(ac.id.startsWith("ac-"));
|
|
for (const br of req.businessRules) assert.ok(br.id.startsWith("br-"));
|
|
for (const nfr of req.nonFunctionalReqs) assert.ok(nfr.id.startsWith("nfr-"));
|
|
for (const ir of req.integrationReqs) assert.ok(ir.id.startsWith("ir-"));
|
|
for (const dr of req.dataReqs) assert.ok(dr.id.startsWith("dr-"));
|
|
});
|
|
|
|
it("factory handles corrupted strategy artifact", () => {
|
|
const registry = createFactoryRegistry();
|
|
registerSF02(registry);
|
|
|
|
const input = createFactoryInput({
|
|
factoryId: "SF-02",
|
|
upstreamArtifacts: [{
|
|
id: "bad-artifact",
|
|
kind: "requirement",
|
|
name: "Corrupted Strategy",
|
|
factoryId: "SF-01",
|
|
metadata: { artifactType: "product-strategy-package" },
|
|
content: "{invalid json!!!",
|
|
}],
|
|
});
|
|
const output = registry.execute("SF-02", input);
|
|
|
|
// Should fail gracefully, not crash
|
|
assert.ok(output.status === "failed" || output.status === "passed",
|
|
`Should complete without crash, got: ${output.status}`);
|
|
});
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// V — Cross-Factory Compatibility
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
describe("SF-02 Part V — PR-35 & PR-36 Compatibility", () => {
|
|
|
|
it("PR-35 Runtime tests still pass", () => {
|
|
// Verify no regression — PR-35 task creation works
|
|
const task = createTask({ id: "T-COMPAT-001", title: "Compatibility Test" });
|
|
assert.ok(task.id);
|
|
assert.ok(task.title);
|
|
});
|
|
|
|
it("PR-36 Registry tests still pass", () => {
|
|
const registry = createFactoryRegistry();
|
|
// SF-01 and SF-02 both present; SF-03 removed (merged into SF-05)
|
|
assert.ok(registry.get("SF-01"));
|
|
assert.ok(registry.get("SF-02"));
|
|
assert.equal(registry.get("SF-03"), null);
|
|
|
|
const deps = registry.getDependencies("SF-02");
|
|
assert.equal(deps.length, 1);
|
|
assert.equal(deps[0].from, "SF-01");
|
|
|
|
const dependents = registry.getDependents("SF-02");
|
|
const depIds = dependents.map(d => d.id);
|
|
// SF-03 removed; SF-05, SF-08 still depend on SF-02
|
|
assert.ok(depIds.length >= 2, `Expected >=2 dependents on SF-02, got ${depIds.length}`);
|
|
assert.ok(depIds.includes("SF-05"), "SF-05 should depend on SF-02");
|
|
assert.ok(depIds.includes("SF-08"), "SF-08 should depend on SF-02");
|
|
});
|
|
|
|
it("topological order puts SF-01 before SF-02", () => {
|
|
const registry = createFactoryRegistry();
|
|
const order = registry.getTopologicalOrder();
|
|
const sf01Idx = order.indexOf("SF-01");
|
|
const sf02Idx = order.indexOf("SF-02");
|
|
assert.ok(sf01Idx < sf02Idx, `SF-01 (${sf01Idx}) must come before SF-02 (${sf02Idx})`);
|
|
});
|
|
|
|
it("parallel batches: SF-02 is in batch 2 (after SF-01)", () => {
|
|
const registry = createFactoryRegistry();
|
|
const batches = registry.getParallelBatches();
|
|
// Batch 1: SF-01
|
|
assert.ok(batches[0].includes("SF-01"));
|
|
// Batch 2: SF-02
|
|
assert.ok(batches[1].includes("SF-02"));
|
|
});
|
|
});
|