/** * 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); }); });