623 lines
26 KiB
JavaScript
623 lines
26 KiB
JavaScript
/**
|
||
* 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("做一个健身打卡 App,iOS 优先");
|
||
assert.ok(platforms.includes("ios"));
|
||
});
|
||
|
||
it("识别为 fitness 领域", () => {
|
||
const prd = generatePRD("做一个健身打卡 App,iOS 优先");
|
||
assert.equal(prd.domain, "fitness");
|
||
assert.equal(prd.projectName, "FitTracker");
|
||
});
|
||
|
||
it("技术栈推荐 iOS 原生", () => {
|
||
const prd = generatePRD("做一个健身打卡 App,iOS 优先");
|
||
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);
|
||
});
|
||
});
|