704 lines
28 KiB
JavaScript
704 lines
28 KiB
JavaScript
/**
|
|
* Architecture Agent Test — SF-02
|
|
*
|
|
* 验证场景:
|
|
* 1. 电商小程序 → 技术选型(小程序 + 支付 + 物流)
|
|
* 2. 企业 OA → 审批/考勤架构
|
|
* 3. 教育平台 → 课程系统架构
|
|
* 4. 笔记应用 → 轻量架构
|
|
* 5. 宠物管理 → 完整架构生成
|
|
* 6. 空/invalid PRD → 错误处理
|
|
* 7. 通用 PRD → 降级处理
|
|
* 8. 架构图生成
|
|
* 9. 数据库设计
|
|
* 10. API 设计
|
|
* 11. 目录结构
|
|
* 12. 模块分解
|
|
* 13. 数据流
|
|
* 14. CLI 端到端
|
|
* 15. 集成 SF-01
|
|
*/
|
|
|
|
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/architecture-agent");
|
|
|
|
const ECO_PRJ = resolve(FIXTURES_DIR, "ecommerce-prd.json");
|
|
const ENT_PRJ = resolve(FIXTURES_DIR, "enterprise-prd.json");
|
|
const EDU_PRJ = resolve(FIXTURES_DIR, "education-prd.json");
|
|
const NOTE_PRJ = resolve(FIXTURES_DIR, "note-prd.json");
|
|
const EMPTY_PRJ = resolve(FIXTURES_DIR, "empty-prd.json");
|
|
const GENERIC_PRJ = resolve(FIXTURES_DIR, "generic-prd.json");
|
|
|
|
let generateArchitecture, resolveTechStack, generateArchDiagram;
|
|
let generateDataFlows, decomposeModules, generateDatabaseSchema;
|
|
let designAPI, generateDirectoryStructure;
|
|
let loadPRD, writeArchitecture;
|
|
|
|
before(async () => {
|
|
const mod = await import("../scripts/architecture-agent.mjs");
|
|
generateArchitecture = mod.generateArchitecture;
|
|
resolveTechStack = mod.resolveTechStack;
|
|
generateArchDiagram = mod.generateArchDiagram;
|
|
generateDataFlows = mod.generateDataFlows;
|
|
decomposeModules = mod.decomposeModules;
|
|
generateDatabaseSchema = mod.generateDatabaseSchema;
|
|
designAPI = mod.designAPI;
|
|
generateDirectoryStructure = mod.generateDirectoryStructure;
|
|
loadPRD = mod.loadPRD;
|
|
writeArchitecture = mod.writeArchitecture;
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 1 — 电商小程序架构
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("1 — 电商小程序架构", () => {
|
|
it("技术选型为小程序栈", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.ok(arch.techStack.frontend.includes("uni-app") || arch.techStack.frontend.includes("小程序"));
|
|
assert.ok(arch.techStack.considerations.some(c => c.includes("支付")));
|
|
assert.ok(arch.techStack.considerations.some(c => c.includes("物流")));
|
|
});
|
|
|
|
it("包含订单和物流表", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const tableNames = arch.databaseSchema.map(t => t.table);
|
|
assert.ok(tableNames.includes("orders"));
|
|
assert.ok(tableNames.includes("logistics"));
|
|
assert.ok(tableNames.includes("products"));
|
|
assert.ok(tableNames.includes("users"));
|
|
});
|
|
|
|
it("订单表有明细关联", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const orderItems = arch.databaseSchema.find(t => t.table === "order_items");
|
|
assert.ok(orderItems);
|
|
assert.ok(orderItems.fields.some(f => f.name === "order_id"));
|
|
assert.ok(orderItems.fields.some(f => f.name === "product_id"));
|
|
});
|
|
|
|
it("API 按资源分组", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const resources = arch.apiDesign.map(g => g.resource);
|
|
assert.ok(resources.includes("products"));
|
|
assert.ok(resources.includes("orders"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 2 — 企业 OA 架构
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("2 — 企业 OA 架构", () => {
|
|
it("包含审批流核心表", () => {
|
|
const { data } = loadPRD(ENT_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const tableNames = arch.databaseSchema.map(t => t.table);
|
|
assert.ok(tableNames.includes("approvals"));
|
|
assert.ok(tableNames.includes("approval_steps"));
|
|
assert.ok(tableNames.includes("attendance"));
|
|
assert.ok(tableNames.includes("departments"));
|
|
});
|
|
|
|
it("审批步骤有步骤序号", () => {
|
|
const { data } = loadPRD(ENT_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const steps = arch.databaseSchema.find(t => t.table === "approval_steps");
|
|
assert.ok(steps.fields.some(f => f.name === "step_order"));
|
|
assert.ok(steps.fields.some(f => f.name === "status"));
|
|
assert.ok(steps.fields.some(f => f.name === "approver_id"));
|
|
});
|
|
|
|
it("模块包含审批和考勤", () => {
|
|
const { data } = loadPRD(ENT_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const modNames = arch.modules.map(m => m.name);
|
|
assert.ok(modNames.some(n => n.includes("approval")));
|
|
assert.ok(modNames.some(n => n.includes("attendance")));
|
|
});
|
|
|
|
it("部门表支持自引用", () => {
|
|
const { data } = loadPRD(ENT_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const dept = arch.databaseSchema.find(t => t.table === "departments");
|
|
const parentField = dept.fields.find(f => f.name === "parent_id");
|
|
assert.ok(parentField.constraints.includes("自引用") || parentField.constraints.includes("departments.id"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 3 — 教育平台架构
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("3 — 教育平台架构", () => {
|
|
it("包含课程和学习进度表", () => {
|
|
const { data } = loadPRD(EDU_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const tableNames = arch.databaseSchema.map(t => t.table);
|
|
assert.ok(tableNames.includes("courses"));
|
|
assert.ok(tableNames.includes("lessons"));
|
|
assert.ok(tableNames.includes("exercises"));
|
|
assert.ok(tableNames.includes("user_progress"));
|
|
});
|
|
|
|
it("课时表有排序字段", () => {
|
|
const { data } = loadPRD(EDU_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const lessons = arch.databaseSchema.find(t => t.table === "lessons");
|
|
assert.ok(lessons.fields.some(f => f.name === "sort_order"));
|
|
assert.ok(lessons.fields.some(f => f.name === "video_url"));
|
|
});
|
|
|
|
it("API 包含 courses 和 progress", () => {
|
|
const { data } = loadPRD(EDU_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const resources = arch.apiDesign.map(g => g.resource);
|
|
assert.ok(resources.includes("courses"));
|
|
assert.ok(resources.includes("progress"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 4 — 笔记应用架构
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("4 — 笔记应用架构", () => {
|
|
it("包含全文搜索索引", () => {
|
|
const { data } = loadPRD(NOTE_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const notes = arch.databaseSchema.find(t => t.table === "notes");
|
|
const ftsIdx = notes.indexes?.find(i => i.includes("fts") || i.includes("GIN"));
|
|
assert.ok(ftsIdx, `Expected FTS index, got: ${JSON.stringify(notes.indexes)}`);
|
|
});
|
|
|
|
it("标签多对多关联", () => {
|
|
const { data } = loadPRD(NOTE_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.ok(arch.databaseSchema.some(t => t.table === "tags"));
|
|
assert.ok(arch.databaseSchema.some(t => t.table === "note_tags"));
|
|
});
|
|
|
|
it("笔记表支持 Markdown", () => {
|
|
const { data } = loadPRD(NOTE_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const notes = arch.databaseSchema.find(t => t.table === "notes");
|
|
assert.ok(notes.fields.some(f => f.name === "is_markdown"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 5 — 完整架构输出
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("5 — 完整架构输出结构", () => {
|
|
it("架构包含所有必需字段", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
const required = [
|
|
"projectName", "domain", "techStack", "architectureDiagram",
|
|
"dataFlows", "modules", "databaseSchema", "apiDesign",
|
|
"directoryStructure", "deployment", "meta",
|
|
];
|
|
for (const field of required) {
|
|
assert.ok(arch[field] !== undefined, `Missing: ${field}`);
|
|
}
|
|
});
|
|
|
|
it("techStack 包含四层", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.ok(arch.techStack.frontend);
|
|
assert.ok(arch.techStack.backend);
|
|
assert.ok(arch.techStack.database);
|
|
assert.ok(arch.techStack.deployment);
|
|
});
|
|
|
|
it("deployment 包含环境和 CI", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.ok(arch.deployment.environments.includes("production"));
|
|
assert.ok(arch.deployment.strategy);
|
|
assert.ok(arch.deployment.ci.includes("GitHub Actions") || arch.deployment.ci.includes("CI"));
|
|
});
|
|
|
|
it("meta 包含统计信息", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.ok(arch.meta.generatedAt);
|
|
assert.ok(typeof arch.meta.moduleCount === "number");
|
|
assert.ok(typeof arch.meta.tableCount === "number");
|
|
assert.ok(typeof arch.meta.apiEndpointCount === "number");
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 6 — 错误处理
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("6 — 错误处理", () => {
|
|
it("空的 error PRD 返回错误", () => {
|
|
const { data } = loadPRD(EMPTY_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.equal(arch.error, "INVALID_PRD");
|
|
assert.ok(arch.message);
|
|
});
|
|
|
|
it("null 输入返回错误", () => {
|
|
const arch = generateArchitecture(null);
|
|
assert.equal(arch.error, "INVALID_PRD");
|
|
});
|
|
|
|
it("缺少 projectName 返回错误", () => {
|
|
const arch = generateArchitecture({ features: [] });
|
|
assert.equal(arch.error, "INCOMPLETE_PRD");
|
|
});
|
|
|
|
it("缺少 features 返回错误", () => {
|
|
const arch = generateArchitecture({ projectName: "test" });
|
|
assert.equal(arch.error, "INCOMPLETE_PRD");
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 7 — 通用 PRD 降级
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("7 — 通用 PRD 降级", () => {
|
|
it("generic domain 生成合理的架构", () => {
|
|
const { data } = loadPRD(GENERIC_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.equal(arch.domain, "generic");
|
|
assert.ok(arch.techStack.frontend);
|
|
assert.ok(arch.techStack.backend);
|
|
assert.ok(arch.databaseSchema.length >= 1);
|
|
assert.ok(arch.modules.length >= 1);
|
|
});
|
|
|
|
it("通用 PRD 有最小表结构", () => {
|
|
const { data } = loadPRD(GENERIC_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
assert.ok(arch.databaseSchema.some(t => t.table === "users"));
|
|
assert.ok(arch.databaseSchema.some(t => t.table === "items"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 8 — 架构图生成
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("8 — 架构图生成", () => {
|
|
it("架构图包含项目名", () => {
|
|
const diagram = generateArchDiagram(
|
|
"PetCare",
|
|
{ frontend: "React Native", backend: "NestJS", database: "PostgreSQL", deployment: "Docker" },
|
|
["宠物管理", "健康日程", "用户中心"]
|
|
);
|
|
|
|
assert.ok(diagram.includes("PetCare"));
|
|
assert.ok(diagram.includes("System Architecture"));
|
|
});
|
|
|
|
it("架构图包含模块列表", () => {
|
|
const diagram = generateArchDiagram(
|
|
"TestApp", { frontend: "React", backend: "Node", database: "PG" },
|
|
["Auth", "API", "Dashboard"]
|
|
);
|
|
|
|
assert.ok(diagram.includes("Auth"));
|
|
assert.ok(diagram.includes("API"));
|
|
assert.ok(diagram.includes("Dashboard"));
|
|
});
|
|
|
|
it("架构图是 ASCII 格式", () => {
|
|
const diagram = generateArchDiagram("X", {}, []);
|
|
assert.ok(diagram.includes("┌"));
|
|
assert.ok(diagram.includes("└"));
|
|
assert.ok(diagram.includes("│"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 9 — 数据库设计
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("9 — 数据库设计", () => {
|
|
it("所有表都有 fields 数组", () => {
|
|
const domains = ["pet", "ecommerce", "education", "enterprise", "fitness", "note", "generic"];
|
|
for (const domain of domains) {
|
|
const tables = generateDatabaseSchema(
|
|
[{ name: "测试功能", description: "测试", priority: "P0" }],
|
|
[{ method: "GET", path: "/api/test", description: "test" }],
|
|
domain
|
|
);
|
|
for (const table of tables) {
|
|
assert.ok(table.table, `${domain}: table has name`);
|
|
assert.ok(Array.isArray(table.fields), `${domain}.${table.table}: fields is array`);
|
|
assert.ok(table.fields.length > 0, `${domain}.${table.table}: fields not empty`);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("所有域都有 users 表", () => {
|
|
const domains = ["pet", "ecommerce", "education", "enterprise"];
|
|
for (const domain of domains) {
|
|
const tables = generateDatabaseSchema([], [], domain);
|
|
assert.ok(tables.some(t => t.table === "users"), `${domain}: missing users table`);
|
|
}
|
|
});
|
|
|
|
it("表字段包含类型和约束", () => {
|
|
const tables = generateDatabaseSchema(
|
|
[{ name: "测试", description: "", priority: "P0" }],
|
|
[{ method: "GET", path: "/api/test", description: "" }],
|
|
"pet"
|
|
);
|
|
for (const table of tables) {
|
|
for (const field of table.fields) {
|
|
assert.ok(field.name);
|
|
assert.ok(field.type);
|
|
// constraints may be null/empty for some fields
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 10 — API 设计
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("10 — API 设计", () => {
|
|
it("按资源分组 API", () => {
|
|
const apis = [
|
|
{ method: "GET", path: "/api/pets", description: "列表" },
|
|
{ method: "POST", path: "/api/pets", description: "创建" },
|
|
{ method: "GET", path: "/api/schedules", description: "日程" },
|
|
];
|
|
const design = designAPI(apis);
|
|
|
|
assert.equal(design.length, 2);
|
|
const resources = design.map(g => g.resource);
|
|
assert.ok(resources.includes("pets"));
|
|
assert.ok(resources.includes("schedules"));
|
|
});
|
|
|
|
it("每个资源组有 basePath", () => {
|
|
const apis = [{ method: "GET", path: "/api/orders", description: "订单" }];
|
|
const design = designAPI(apis);
|
|
|
|
assert.equal(design[0].basePath, "/api/orders");
|
|
assert.equal(design[0].endpoints.length, 1);
|
|
});
|
|
|
|
it("endpoint 包含 method 和 path", () => {
|
|
const apis = [
|
|
{ method: "POST", path: "/api/pets", description: "创建宠物" },
|
|
{ method: "GET", path: "/api/pets/:id", description: "获取宠物" },
|
|
];
|
|
const design = designAPI(apis);
|
|
|
|
const group = design.find(g => g.resource === "pets");
|
|
assert.equal(group.endpoints.length, 2);
|
|
assert.ok(group.endpoints.some(e => e.method === "POST"));
|
|
assert.ok(group.endpoints.some(e => e.path.includes(":id")));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 11 — 目录结构
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("11 — 目录结构生成", () => {
|
|
it("包含 apps 和 server 目录", () => {
|
|
const dirs = generateDirectoryStructure("PetCare", "pet", [
|
|
{ name: "pet", label: "宠物管理", features: ["宠物档案"] },
|
|
]);
|
|
|
|
const text = dirs.join("\n");
|
|
assert.ok(text.includes("apps/"));
|
|
assert.ok(text.includes("server/"));
|
|
});
|
|
|
|
it("包含 docker-compose 和 CI 配置", () => {
|
|
const dirs = generateDirectoryStructure("TestApp", "generic", []);
|
|
const text = dirs.join("\n");
|
|
|
|
assert.ok(text.includes("docker-compose.yml"));
|
|
assert.ok(text.includes(".github/workflows"));
|
|
assert.ok(text.includes("ci.yml"));
|
|
});
|
|
|
|
it("模块目录包含 controller/service/module", () => {
|
|
const dirs = generateDirectoryStructure("PetCare", "pet", [
|
|
{ name: "pets", label: "宠物管理", features: [] },
|
|
]);
|
|
const text = dirs.join("\n");
|
|
|
|
assert.ok(text.includes("pets.controller.ts"));
|
|
assert.ok(text.includes("pets.service.ts"));
|
|
assert.ok(text.includes("pets.module.ts"));
|
|
});
|
|
|
|
it("包含 Prisma schema", () => {
|
|
const dirs = generateDirectoryStructure("App", "generic", []);
|
|
const text = dirs.join("\n");
|
|
|
|
assert.ok(text.includes("schema.prisma"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 12 — 模块分解
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("12 — 模块分解", () => {
|
|
it("按关键词分组合并功能", () => {
|
|
const features = [
|
|
{ name: "用户管理", description: "", priority: "P0" },
|
|
{ name: "权限控制", description: "", priority: "P0" },
|
|
{ name: "设置中心", description: "", priority: "P1" },
|
|
];
|
|
const modules = decomposeModules(features, "generic");
|
|
|
|
// "用户管理" → auth, "权限控制" → auth (matches "权限"), "设置中心" → settings (matches "设置")
|
|
assert.ok(modules.some(m => m.name === "auth"));
|
|
assert.ok(modules.some(m => m.name === "settings"));
|
|
});
|
|
|
|
it("未知功能各自独立模块", () => {
|
|
const features = [
|
|
{ name: "量子计算", description: "", priority: "P0" },
|
|
];
|
|
const modules = decomposeModules(features, "generic");
|
|
|
|
assert.ok(modules.some(m => m.name === "量子计算"));
|
|
});
|
|
|
|
it("电商域添加订单和商品模块", () => {
|
|
const features = [{ name: "商品浏览", description: "", priority: "P0" }];
|
|
const modules = decomposeModules(features, "ecommerce");
|
|
|
|
const names = modules.map(m => m.name);
|
|
assert.ok(names.includes("product"));
|
|
assert.ok(names.includes("order"));
|
|
});
|
|
|
|
it("企业域添加审批/考勤/部门模块", () => {
|
|
const features = [
|
|
{ name: "审批流程", description: "", priority: "P0" },
|
|
{ name: "考勤打卡", description: "", priority: "P0" },
|
|
{ name: "部门管理", description: "", priority: "P0" },
|
|
];
|
|
const modules = decomposeModules(features, "enterprise");
|
|
|
|
const names = modules.map(m => m.name);
|
|
assert.ok(names.includes("approval"));
|
|
assert.ok(names.includes("attendance"));
|
|
assert.ok(names.includes("department"));
|
|
});
|
|
|
|
it("每个模块有 responsibilities", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const arch = generateArchitecture(data);
|
|
|
|
for (const mod of arch.modules) {
|
|
assert.ok(mod.name);
|
|
assert.ok(mod.label);
|
|
assert.ok(Array.isArray(mod.features));
|
|
assert.ok(Array.isArray(mod.responsibilities));
|
|
assert.ok(mod.responsibilities.length > 0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 13 — 数据流
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("13 — 数据流生成", () => {
|
|
it("为页面生成数据流", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
const flows = generateDataFlows(data.pages, data.apiRequirements);
|
|
|
|
assert.ok(flows.length > 0);
|
|
for (const flow of flows) {
|
|
assert.ok(flow.page);
|
|
assert.ok(flow.route);
|
|
assert.ok(flow.description);
|
|
assert.ok(Array.isArray(flow.dataFlow));
|
|
assert.ok(flow.direction.includes("Database"));
|
|
}
|
|
});
|
|
|
|
it("API 为空时回退到通用数据流", () => {
|
|
const flows = generateDataFlows(
|
|
[{ name: "首页", route: "/home", description: "首页" }],
|
|
[{ method: "GET", path: "/api/health", description: "健康检查" }]
|
|
);
|
|
|
|
assert.ok(flows.length > 0);
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 14 — CLI 端到端
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("14 — CLI 端到端", () => {
|
|
it("--input 模式生成架构", async () => {
|
|
const { execSync } = await import("node:child_process");
|
|
const result = execSync(
|
|
`node scripts/architecture-agent.mjs --input test/fixtures/architecture-agent/ecommerce-prd.json --pretty`,
|
|
{ cwd: WORKSPACE, encoding: "utf-8" }
|
|
);
|
|
const parsed = JSON.parse(result);
|
|
assert.equal(parsed.projectName, "ShopApp");
|
|
assert.equal(parsed.domain, "ecommerce");
|
|
assert.ok(parsed.techStack.frontend.includes("uni-app") || parsed.techStack.frontend.includes("小程序"));
|
|
});
|
|
|
|
it("--input-text 模式自动调 SF-01", async () => {
|
|
const { execSync } = await import("node:child_process");
|
|
const result = execSync(
|
|
`node scripts/architecture-agent.mjs --input-text "做一个在线教育平台" --pretty`,
|
|
{ cwd: WORKSPACE, encoding: "utf-8" }
|
|
);
|
|
const parsed = JSON.parse(result);
|
|
assert.equal(parsed.projectName, "EduPlatform");
|
|
assert.equal(parsed.domain, "education");
|
|
});
|
|
|
|
it("--help 显示帮助", async () => {
|
|
const { execSync } = await import("node:child_process");
|
|
const result = execSync(
|
|
`node scripts/architecture-agent.mjs --help`,
|
|
{ cwd: WORKSPACE, encoding: "utf-8" }
|
|
);
|
|
assert.ok(result.includes("Usage"));
|
|
assert.ok(result.includes("architecture-agent"));
|
|
});
|
|
|
|
it("--output 写入文件", async () => {
|
|
const { execSync } = await import("node:child_process");
|
|
const tmpPath = resolve(WORKSPACE, ".tmp-arch-e2e.json");
|
|
execSync(
|
|
`node scripts/architecture-agent.mjs --input test/fixtures/architecture-agent/note-prd.json --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");
|
|
assert.ok(content.architectureDiagram.includes("NoteApp"));
|
|
|
|
rmSync(tmpPath);
|
|
});
|
|
|
|
it("invalid PRD 返回非零退出码", async () => {
|
|
const { execSync } = await import("node:child_process");
|
|
try {
|
|
execSync(
|
|
`node scripts/architecture-agent.mjs --input test/fixtures/architecture-agent/empty-prd.json`,
|
|
{ cwd: WORKSPACE, encoding: "utf-8" }
|
|
);
|
|
assert.fail("Should have thrown");
|
|
} catch (e) {
|
|
assert.ok(e.status !== 0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 15 — 技术栈解析
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("15 — 技术栈解析", () => {
|
|
it("wechat-miniapp 平台使用微信栈", () => {
|
|
const prd = {
|
|
techConstraints: { platforms: ["wechat-miniapp"], considerations: [] },
|
|
extraFeatures: [],
|
|
};
|
|
const stack = resolveTechStack(prd);
|
|
assert.ok(stack.frontend.includes("微信"));
|
|
});
|
|
|
|
it("iOS 平台推荐 SwiftUI", () => {
|
|
const prd = {
|
|
techConstraints: { platforms: ["ios"], considerations: [] },
|
|
extraFeatures: [],
|
|
};
|
|
const stack = resolveTechStack(prd);
|
|
assert.ok(stack.frontend.includes("SwiftUI"));
|
|
});
|
|
|
|
it("mobile+web 推荐跨平台框架", () => {
|
|
const prd = {
|
|
techConstraints: { platforms: ["mobile", "web"], considerations: [] },
|
|
extraFeatures: [],
|
|
};
|
|
const stack = resolveTechStack(prd);
|
|
assert.ok(
|
|
stack.frontend.includes("React Native") ||
|
|
stack.frontend.includes("Flutter")
|
|
);
|
|
});
|
|
|
|
it("实时特性添加 WebSocket", () => {
|
|
const prd = {
|
|
techConstraints: { platforms: ["web"], considerations: [] },
|
|
extraFeatures: ["real-time"],
|
|
};
|
|
const stack = resolveTechStack(prd);
|
|
assert.ok(stack.backend.includes("WebSocket") || stack.backend.includes("Socket.io"));
|
|
});
|
|
|
|
it("AI 特性添加 AI 服务", () => {
|
|
const prd = {
|
|
techConstraints: { platforms: ["web"], considerations: [] },
|
|
extraFeatures: ["ai-powered"],
|
|
};
|
|
const stack = resolveTechStack(prd);
|
|
assert.ok(stack.backend.includes("AI"));
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// 16 — File I/O
|
|
// ═══════════════════════════════════════════════════════
|
|
describe("16 — File I/O", () => {
|
|
it("loadPRD 读取文件", () => {
|
|
const { data } = loadPRD(ECO_PRJ);
|
|
assert.ok(data.projectName);
|
|
});
|
|
|
|
it("loadPRD 对不存在文件返回 error", () => {
|
|
const { data, error } = loadPRD("/nonexistent.json");
|
|
assert.equal(data, null);
|
|
assert.ok(error);
|
|
});
|
|
|
|
it("writeArchitecture 写入文件", () => {
|
|
const arch = generateArchitecture(JSON.parse(readFileSync(ECO_PRJ, "utf-8")));
|
|
const tmpPath = resolve(WORKSPACE, ".tmp-arch-write-test.json");
|
|
writeArchitecture(arch, tmpPath, true);
|
|
assert.ok(existsSync(tmpPath));
|
|
rmSync(tmpPath);
|
|
});
|
|
});
|