🎉 init: 小龙的工作空间

This commit is contained in:
大海
2026-06-06 10:40:48 +08:00
commit a188ee1426
3201 changed files with 231817 additions and 0 deletions
+622
View File
@@ -0,0 +1,622 @@
/**
* Project Intake & PRD Agent Test — SF-01
*
* 验证场景:
* 1. 宠物管理 App → 正确识别领域
* 2. 电商小程序 → 平台检测 + 物流/支付
* 3. 教育平台 → 课程相关 PRD
* 4. 企业 OA → 审批流/考勤
* 5. 极简输入 → 笔记应用
* 6. 健身打卡 iOS → 平台偏好
* 7. 空输入 → 错误处理
* 8. 通用/不识别输入 → default domain
* 9. PRD 输出结构完整性
* 10. 任务拆解
*/
import { describe, it, before } from "node:test";
import assert from "node:assert/strict";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync, rmSync, readFileSync } from "node:fs";
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const FIXTURES_DIR = resolve(WORKSPACE, "test/fixtures/project-intake");
let generatePRD, matchDomain, detectPlatforms, detectExtraFeatures;
let generateUserStories, determineTechConstraints, generateDevTasks;
let loadInput, writeOutput;
before(async () => {
const mod = await import("../scripts/project-intake-agent.mjs");
generatePRD = mod.generatePRD;
matchDomain = mod.matchDomain;
detectPlatforms = mod.detectPlatforms;
detectExtraFeatures = mod.detectExtraFeatures;
generateUserStories = mod.generateUserStories;
determineTechConstraints = mod.determineTechConstraints;
generateDevTasks = mod.generateDevTasks;
loadInput = mod.loadInput;
writeOutput = mod.writeOutput;
});
// ═══════════════════════════════════════════════════════
// 1 — 宠物管理 App
// ═══════════════════════════════════════════════════════
describe("1 — 宠物管理 App", () => {
it("识别为 pet 领域", () => {
const { data } = loadInput(resolve(FIXTURES_DIR, "pet-management.json"));
const prd = generatePRD(data.input);
assert.equal(prd.domain, "pet");
assert.equal(prd.matchConfidence, "medium"); // matchCount=1 but no keywords extracted from user input
assert.equal(prd.projectName, "PetCare");
assert.ok(prd.summary.includes("宠物"));
});
it("包含宠物相关特征", () => {
const prd = generatePRD("做一个宠物管理 App");
assert.ok(prd.features.some(f => f.name.includes("宠物")));
assert.ok(prd.features.some(f => f.name.includes("健康")));
assert.ok(prd.pages.some(p => p.name.includes("宠物")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("pet")));
});
it("生成用户故事", () => {
const prd = generatePRD("做一个宠物管理 App");
assert.ok(prd.userStories.length >= 2, `Expected >= 2 stories, got ${prd.userStories.length}`);
for (const story of prd.userStories) {
assert.ok(story.id.startsWith("US-"));
assert.ok(story.as);
assert.ok(story.want);
assert.ok(story.soThat);
assert.ok(story.priority);
}
});
it("生成 MVP 范围", () => {
const prd = generatePRD("做一个宠物管理 App");
assert.ok(prd.mvpScope);
assert.ok(prd.mvpScope.features.length > 0);
assert.ok(prd.mvpScope.estimatedWeeks > 0);
assert.ok(prd.mvpScope.description);
});
});
// ═══════════════════════════════════════════════════════
// 2 — 电商小程序
// ═══════════════════════════════════════════════════════
describe("2 — 电商小程序(支付+物流)", () => {
it("识别为 ecommerce 领域", () => {
const { data } = loadInput(resolve(FIXTURES_DIR, "ecommerce-miniapp.json"));
const prd = generatePRD(data.input);
assert.equal(prd.domain, "ecommerce");
assert.equal(prd.projectName, "ShopApp");
});
it("检测到小程序平台", () => {
const platforms = detectPlatforms("做一个电商小程序,需要支持微信支付和物流追踪");
assert.ok(platforms.includes("miniapp") || platforms.includes("wechat-miniapp"));
});
it("检测到支付和物流特性", () => {
const features = detectExtraFeatures("做一个电商小程序,需要支持微信支付和物流追踪");
assert.ok(features.includes("wechat-pay"));
assert.ok(features.includes("logistics-tracking"));
});
it("技术约束包含微信支付和物流", () => {
const prd = generatePRD("做一个电商小程序,需要支持微信支付和物流追踪");
const considerations = prd.techConstraints.considerations;
assert.ok(considerations.some(c => c.includes("微信支付")));
assert.ok(considerations.some(c => c.includes("物流")));
});
it("包含电商 API 端点(domain template fallback", () => {
const prd = generatePRD("做一个电商小程序,需要支持微信支付和物流追踪");
// Domain template APIs used (no explicit API/接口 mentions in input)
assert.ok(prd.apiRequirements.some(a => a.path.includes("products")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("orders")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("payments")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("logistics")));
// Verify extra features are still detected
assert.ok(prd.techConstraints.considerations.some(c => c.includes("微信支付")));
assert.ok(prd.techConstraints.considerations.some(c => c.includes("物流")));
});
});
// ═══════════════════════════════════════════════════════
// 3 — 在线教育平台
// ═══════════════════════════════════════════════════════
describe("3 — 在线教育平台", () => {
it("识别为 education 领域", () => {
const { data } = loadInput(resolve(FIXTURES_DIR, "edu-platform.json"));
const prd = generatePRD(data.input);
assert.equal(prd.domain, "education");
assert.equal(prd.projectName, "EduPlatform");
});
it("包含课程和题库功能", () => {
const prd = generatePRD("做一个在线教育平台");
assert.ok(prd.features.some(f => f.name.includes("课程")));
assert.ok(prd.features.some(f => f.name.includes("题库")));
// Domain template pages used (no explicit page mentions in input)
assert.ok(prd.pages.some(p => p.route.includes("course")));
assert.ok(prd.pages.some(p => p.route.includes("learn")));
assert.ok(prd.pages.some(p => p.route.includes("exercises")));
});
it("API 包含课程和进度端点", () => {
const prd = generatePRD("做一个在线教育平台");
assert.ok(prd.apiRequirements.some(a => a.path.includes("courses")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("progress")));
});
});
// ═══════════════════════════════════════════════════════
// 4 — 企业 OA
// ═══════════════════════════════════════════════════════
describe("4 — 企业 OA 系统", () => {
it("识别为 enterprise 领域", () => {
const { data } = loadInput(resolve(FIXTURES_DIR, "enterprise-oa.json"));
const prd = generatePRD(data.input);
assert.equal(prd.domain, "enterprise");
assert.equal(prd.projectName, "OAFlow");
});
it("包含审批流和考勤功能", () => {
const prd = generatePRD("做一个企业办公自动化系统,包含审批流、考勤打卡、部门管理");
assert.ok(prd.features.some(f => f.name.includes("审批")));
assert.ok(prd.features.some(f => f.name.includes("考勤")));
assert.ok(prd.features.some(f => f.name.includes("部门")));
});
it("技术约束包含 RBAC", () => {
const prd = generatePRD("做一个企业办公自动化系统,包含审批流、考勤打卡、部门管理");
assert.ok(prd.techConstraints.considerations.some(c => c.includes("RBAC")));
});
it("API 包含审批端点", () => {
const prd = generatePRD("做一个企业办公自动化系统,包含审批流、考勤打卡、部门管理");
assert.ok(prd.apiRequirements.some(a => a.path.includes("approvals")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("attendance")));
});
it("包含三个用户画像", () => {
const prd = generatePRD("做一个企业办公自动化系统");
assert.ok(prd.personas.length >= 3, `Expected >= 3 personas, got ${prd.personas.length}`);
});
});
// ═══════════════════════════════════════════════════════
// 5 — 极简输入
// ═══════════════════════════════════════════════════════
describe("5 — 极简输入", () => {
it("两个词的输入也能生成 PRD", () => {
const { data } = loadInput(resolve(FIXTURES_DIR, "minimal.json"));
const prd = generatePRD(data.input);
assert.ok(prd);
assert.ok(!prd.error);
assert.equal(prd.projectName, "NoteApp");
assert.equal(prd.domain, "note");
});
it("极简输入有合理的结构", () => {
const prd = generatePRD("笔记应用");
assert.ok(prd.features.length >= 3);
assert.ok(prd.pages.length >= 2);
assert.ok(prd.apiRequirements.length >= 3);
assert.ok(prd.userStories.length >= 2);
});
});
// ═══════════════════════════════════════════════════════
// 6 — 健身打卡 iOS
// ═══════════════════════════════════════════════════════
describe("6 — 健身打卡 + 平台偏好", () => {
it("检测到 iOS 平台偏好", () => {
const platforms = detectPlatforms("做一个健身打卡 AppiOS 优先");
assert.ok(platforms.includes("ios"));
});
it("识别为 fitness 领域", () => {
const prd = generatePRD("做一个健身打卡 AppiOS 优先");
assert.equal(prd.domain, "fitness");
assert.equal(prd.projectName, "FitTracker");
});
it("技术栈推荐 iOS 原生", () => {
const prd = generatePRD("做一个健身打卡 AppiOS 优先");
assert.ok(prd.techConstraints.recommendedStack.includes("SwiftUI"));
});
it("包含打卡和统计功能", () => {
const prd = generatePRD("做一个健身打卡 App");
assert.ok(prd.features.some(f => f.name.includes("打卡")));
assert.ok(prd.features.some(f => f.name.includes("统计")));
assert.ok(prd.apiRequirements.some(a => a.path.includes("checkins")));
});
});
// ═══════════════════════════════════════════════════════
// 7 — 空输入 / 错误处理
// ═══════════════════════════════════════════════════════
describe("7 — 空输入与错误处理", () => {
it("空字符串返回错误", () => {
const prd = generatePRD("");
assert.equal(prd.error, "EMPTY_INPUT");
assert.ok(prd.message);
});
it("null 输入返回错误", () => {
const prd = generatePRD(null);
assert.equal(prd.error, "EMPTY_INPUT");
});
it("仅空白字符返回错误", () => {
const prd = generatePRD(" ");
assert.equal(prd.error, "EMPTY_INPUT");
});
});
// ═══════════════════════════════════════════════════════
// 8 — 通用/不匹配输入
// ═══════════════════════════════════════════════════════
describe("8 — 通用领域降级", () => {
it("不匹配任何领域时返回 EXTRACTION_FAILED 但包含基础字段", () => {
const prd = generatePRD("做一个区块链数据分析平台");
assert.equal(prd.error, "EXTRACTION_FAILED");
assert.ok(prd.message.includes("no features/pages/apis detected"));
assert.equal(prd.domain, "generic");
assert.equal(prd.projectName, "MyProject");
assert.equal(prd.features.length, 0);
assert.equal(prd.pages.length, 0);
});
it("无法提取特征时返回错误而非虚空 PRD", () => {
const prd = generatePRD("做一个很酷的东西");
// P0 硬化后:无法提取特征时不会生成虚空 PRD
assert.equal(prd.error, "EXTRACTION_FAILED");
assert.ok(prd.projectName);
assert.equal(prd.domain, "generic");
});
it("有领域匹配时生成完整 PRD(正常分支)", () => {
const prd = generatePRD("做一个宠物管理 App");
assert.equal(prd.error, undefined);
assert.ok(prd.projectName);
assert.ok(prd.summary);
assert.ok(prd.personas.length > 0);
assert.ok(prd.userStories.length > 0);
});
});
// ═══════════════════════════════════════════════════════
// 9 — PRD 结构完整性
// ═══════════════════════════════════════════════════════
describe("9 — PRD 结构完整性", () => {
it("PRD 包含所有必需字段", () => {
const prd = generatePRD("做一个宠物管理 App");
const requiredFields = [
"projectName", "chineseName", "domain", "matchConfidence",
"summary", "personas", "userStories", "mvpScope",
"features", "pages", "apiRequirements",
"techConstraints", "extraFeatures", "devTasks", "meta",
];
for (const field of requiredFields) {
assert.ok(prd[field] !== undefined, `Missing required field: ${field}`);
}
});
it("features 包含 priority 字段", () => {
const prd = generatePRD("做一个宠物管理 App");
for (const f of prd.features) {
assert.ok(f.name);
assert.ok(f.description);
assert.ok(f.priority);
assert.ok(["P0", "P1", "P2"].includes(f.priority));
}
});
it("pages 包含 route 字段", () => {
const prd = generatePRD("做一个宠物管理 App");
for (const p of prd.pages) {
assert.ok(p.name);
assert.ok(p.route);
assert.ok(p.description);
}
});
it("apiRequirements 包含 method 和 path", () => {
const prd = generatePRD("做一个宠物管理 App");
for (const a of prd.apiRequirements) {
assert.ok(a.method);
assert.ok(a.path);
assert.ok(a.description);
assert.ok(["GET", "POST", "PUT", "DELETE"].includes(a.method));
}
});
it("devTasks 包含所有必需字段", () => {
const prd = generatePRD("做一个宠物管理 App");
for (const t of prd.devTasks) {
assert.ok(t.id.startsWith("T-"));
assert.ok(t.title);
assert.ok(t.description);
assert.ok(t.phase);
assert.ok(typeof t.estimatedHours === "number");
assert.ok(t.estimatedHours > 0);
assert.ok(t.priority);
}
});
it("MVP scope 仅包含 P0 功能", () => {
const prd = generatePRD("做一个电商小程序");
const p0Features = prd.features.filter(f => f.priority === "P0").map(f => f.name);
for (const f of prd.mvpScope.features) {
assert.ok(p0Features.includes(f), `${f} should be P0`);
}
});
it("meta 包含时间戳和输入长度", () => {
const prd = generatePRD("做一个宠物管理 App");
assert.ok(prd.meta.generatedAt);
assert.ok(typeof prd.meta.inputLength === "number");
assert.ok(typeof prd.meta.domainMatchCount === "number");
});
});
// ═══════════════════════════════════════════════════════
// 10 — 任务拆解
// ═══════════════════════════════════════════════════════
describe("10 — 任务拆解", () => {
it("任务包含 Foundation 阶段", () => {
const prd = generatePRD("做一个宠物管理 App");
const foundationTasks = prd.devTasks.filter(t => t.phase === "Foundation");
assert.ok(foundationTasks.length >= 2);
});
it("任务包含 UI Development 和 Backend Development", () => {
const prd = generatePRD("做一个电商小程序");
const phases = new Set(prd.devTasks.map(t => t.phase));
assert.ok(phases.has("UI Development"));
assert.ok(phases.has("Backend Development"));
});
it("任务包含 Integration 和 Testing", () => {
const prd = generatePRD("做一个在线教育平台");
const phases = new Set(prd.devTasks.map(t => t.phase));
assert.ok(phases.has("Integration"));
assert.ok(phases.has("Testing"));
});
it("总工时计算合理", () => {
const prd = generatePRD("做一个企业办公自动化系统");
const totalHours = prd.devTasks.reduce((s, t) => s + t.estimatedHours, 0);
assert.ok(totalHours > 40, `Expected > 40 hours, got ${totalHours}`);
assert.ok(totalHours < 500, `Expected < 500 hours, got ${totalHours}`);
});
it("所有领域的任务都有合理的工时", () => {
const inputs = [
"做一个宠物管理 App",
"做一个电商小程序",
"做一个在线教育平台",
"笔记应用",
"做一个企业办公自动化系统",
"做一个健身打卡 App",
];
for (const input of inputs) {
const prd = generatePRD(input);
// Just verify no zero-hour tasks
for (const t of prd.devTasks) {
assert.ok(t.estimatedHours > 0, `${input}: task ${t.id} has 0 hours`);
}
}
});
});
// ═══════════════════════════════════════════════════════
// 11 — Output File I/O
// ═══════════════════════════════════════════════════════
describe("11 — File I/O", () => {
it("loadInput 读取正确的 input 字段", () => {
const { data } = loadInput(resolve(FIXTURES_DIR, "pet-management.json"));
assert.ok(data.input);
assert.equal(data.input, "做一个宠物管理 App");
});
it("loadInput 对不存在的文件返回 error", () => {
const { data, error } = loadInput("/nonexistent/path.json");
assert.equal(data, null);
assert.ok(error);
});
it("writeOutput 写入文件", () => {
const prd = generatePRD("做一个宠物管理 App");
const tmpPath = resolve(WORKSPACE, ".tmp-prd-test-output.json");
writeOutput(prd, tmpPath, true);
assert.ok(existsSync(tmpPath));
// Clean up
rmSync(tmpPath);
});
});
// ═══════════════════════════════════════════════════════
// 12 — 领域匹配引擎
// ═══════════════════════════════════════════════════════
describe("12 — 领域匹配引擎", () => {
it("matchDomain 正确识别各领域", () => {
const cases = [
{ input: "宠物", expected: "pet" },
{ input: "电商商城购物", expected: "ecommerce" },
{ input: "在线课程教学", expected: "education" },
{ input: "企业OA审批", expected: "enterprise" },
{ input: "健身运动打卡", expected: "fitness" },
{ input: "笔记备忘录", expected: "note" },
{ input: "社交朋友圈", expected: "social" },
{ input: "外卖点餐", expected: "food" },
{ input: "旅游酒店机票", expected: "travel" },
];
for (const { input, expected } of cases) {
const { domainKey } = matchDomain(input);
assert.equal(domainKey, expected, `"${input}" → expected ${expected}, got ${domainKey}`);
}
});
it("多领域关键词选最多匹配的", () => {
// "宠物电商" → pet keywords: [宠物] = 1, ecommerce keywords: [电商] = 1 → tie, first match wins (pet)
const { domainKey } = matchDomain("宠物电商平台");
// pet has 1 match, ecommerce also 1 match but pet comes first
assert.ok(["pet", "ecommerce"].includes(domainKey));
});
it("无匹配回退到 generic", () => {
const { domainKey, matchCount } = matchDomain("量子计算区块链");
assert.equal(domainKey, "generic");
assert.equal(matchCount, 0);
});
});
// ═══════════════════════════════════════════════════════
// 13 — 平台检测
// ═══════════════════════════════════════════════════════
describe("13 — 平台检测", () => {
it("默认返回 mobile + web", () => {
const platforms = detectPlatforms("做一个任意应用");
assert.deepEqual(platforms, ["mobile", "web"]);
});
it("检测所有已知平台", () => {
const cases = [
{ input: "小程序", expected: "miniapp" },
{ input: "iOS App", expected: "ios" },
{ input: "安卓应用", expected: "android" },
{ input: "Web 网页", expected: "web" },
{ input: "桌面端PC", expected: "desktop" },
];
for (const { input, expected } of cases) {
const platforms = detectPlatforms(input);
assert.ok(platforms.includes(expected), `"${input}" should include ${expected}`);
}
});
it("检测多个平台", () => {
const platforms = detectPlatforms("做一个 iOS 和 Android 应用");
assert.ok(platforms.includes("ios"));
assert.ok(platforms.includes("android"));
});
});
// ═══════════════════════════════════════════════════════
// 14 — 额外特性检测
// ═══════════════════════════════════════════════════════
describe("14 — 额外特性检测", () => {
it("检测到支付特性", () => {
const features = detectExtraFeatures("需要微信支付和支付宝");
assert.ok(features.includes("wechat-pay"));
assert.ok(features.includes("alipay"));
});
it("检测到 AI 和推送", () => {
const features = detectExtraFeatures("AI 智能推荐 + 推送通知");
assert.ok(features.includes("ai-powered"));
assert.ok(features.includes("smart-recommendation"));
assert.ok(features.includes("push-notification"));
});
it("检测到离线模式和多语言", () => {
const features = detectExtraFeatures("需要支持离线模式和多语言国际化");
assert.ok(features.includes("offline-mode"));
assert.ok(features.includes("i18n"));
});
it("无特性时返回空数组", () => {
const features = detectExtraFeatures("做一个普通应用");
assert.deepEqual(features, []);
});
});
// ═══════════════════════════════════════════════════════
// 15 — CLI 端到端
// ═══════════════════════════════════════════════════════
describe("15 — CLI 端到端", () => {
it("通过 --input 直接传入需求", async () => {
const { execSync } = await import("node:child_process");
const result = execSync(
`node scripts/project-intake-agent.mjs --input "做一个宠物管理 App" --pretty`,
{ cwd: WORKSPACE, encoding: "utf-8" }
);
const parsed = JSON.parse(result);
assert.equal(parsed.projectName, "PetCare");
assert.equal(parsed.domain, "pet");
});
it("通过 --input-file 传入需求", async () => {
const { execSync } = await import("node:child_process");
const result = execSync(
`node scripts/project-intake-agent.mjs --input-file test/fixtures/project-intake/edu-platform.json --pretty`,
{ cwd: WORKSPACE, encoding: "utf-8" }
);
const parsed = JSON.parse(result);
assert.equal(parsed.projectName, "EduPlatform");
});
it("--help 显示帮助", async () => {
const { execSync } = await import("node:child_process");
const result = execSync(
`node scripts/project-intake-agent.mjs --help`,
{ cwd: WORKSPACE, encoding: "utf-8" }
);
assert.ok(result.includes("Usage"));
assert.ok(result.includes("project-intake-agent"));
});
it("缺少 input 时返回错误", async () => {
const { execSync } = await import("node:child_process");
try {
execSync(`node scripts/project-intake-agent.mjs`, { cwd: WORKSPACE, encoding: "utf-8" });
assert.fail("Should have thrown");
} catch (e) {
assert.ok(e.stderr.includes("required") || e.status !== 0);
}
});
it("--output 写入文件", async () => {
const { execSync } = await import("node:child_process");
const tmpPath = resolve(WORKSPACE, ".tmp-prd-e2e-output.json");
execSync(
`node scripts/project-intake-agent.mjs --input "笔记应用" --output ${tmpPath} --pretty`,
{ cwd: WORKSPACE, encoding: "utf-8" }
);
assert.ok(existsSync(tmpPath));
const content = JSON.parse(readFileSync(tmpPath, "utf-8"));
assert.equal(content.projectName, "NoteApp");
rmSync(tmpPath);
});
});