/** * Backend Builder Agent Test — SF-04 */ 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, ".."); let buildBackend, loadJSON, writeBackend; before(async () => { const mod = await import("../scripts/backend-builder-agent.mjs"); buildBackend = mod.buildBackend; loadJSON = mod.loadJSON; writeBackend = mod.writeBackend; }); function loadPRD(name) { return JSON.parse(readFileSync(resolve(WORKSPACE, `test/fixtures/architecture-agent/${name}-prd.json`), "utf-8")); } function loadArch() { return JSON.parse(readFileSync(resolve(WORKSPACE, "architecture-example.json"), "utf-8")); } // Demo RequirementPackage that exercises all generators const DEMO_PRD = { projectName: "PetCare", chineseName: "宠物管理", domain: "pet", summary: "一款宠物健康管理应用", features: [ { name: "宠物档案", priority: "P0" }, { name: "健康日程", priority: "P0" }, { name: "日常记录", priority: "P1" }, { name: "附近医院", priority: "P1" }, ], userStories: [ { id: "US-001", as: "宠物主人", want: "记录宠物信息", soThat: "管理宠物档案", priority: "P0" }, ], personas: [{ name: "宠物主人" }], }; const DEMO_ARCH = { projectName: "PetCare", databaseSchema: [ { table: "pets", description: "宠物档案表", fields: [ { name: "id", type: "TEXT", constraints: "PK" }, { name: "owner_id", type: "TEXT", constraints: "NOT NULL, FK → users.id" }, { name: "name", type: "TEXT", constraints: "NOT NULL" }, { name: "species", type: "TEXT" }, { name: "breed", type: "TEXT" }, { name: "birth_date", type: "TEXT" }, { name: "weight_kg", type: "REAL" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TEXT", constraints: "NOT NULL, DEFAULT (datetime('now'))" }, { name: "updated_at", type: "TEXT", constraints: "NOT NULL, DEFAULT (datetime('now'))" }, ], indexes: ["CREATE INDEX IF NOT EXISTS idx_pets_owner ON pets(owner_id)"], }, { table: "schedules", description: "健康日程表", fields: [ { name: "id", type: "TEXT", constraints: "PK" }, { name: "pet_id", type: "TEXT", constraints: "NOT NULL, FK → pets.id" }, { name: "type", type: "TEXT", constraints: "NOT NULL" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "scheduled_at", type: "TEXT", constraints: "NOT NULL" }, { name: "completed", type: "INTEGER", constraints: "DEFAULT 0" }, { name: "created_at", type: "TEXT", constraints: "NOT NULL, DEFAULT (datetime('now'))" }, ], }, { table: "daily_logs", description: "日常记录表", fields: [ { name: "id", type: "TEXT", constraints: "PK" }, { name: "pet_id", type: "TEXT", constraints: "NOT NULL, FK → pets.id" }, { name: "category", type: "TEXT", constraints: "NOT NULL" }, { name: "value", type: "TEXT", constraints: "NOT NULL" }, { name: "logged_at", type: "TEXT", constraints: "NOT NULL, DEFAULT (datetime('now'))" }, ], }, ], apiDesign: [ { resource: "pets", basePath: "/api/pets", endpoints: [ { method: "GET", path: "/api/pets", description: "获取宠物列表" }, { method: "GET", path: "/api/pets/:id", description: "获取宠物详情" }, { method: "POST", path: "/api/pets", description: "创建宠物" }, { method: "PUT", path: "/api/pets/:id", description: "更新宠物" }, { method: "DELETE", path: "/api/pets/:id", description: "删除宠物" }, ], }, { resource: "schedules", basePath: "/api/schedules", endpoints: [ { method: "GET", path: "/api/schedules", description: "获取日程列表" }, { method: "POST", path: "/api/schedules", description: "创建日程" }, ], }, { resource: "daily-logs", basePath: "/api/daily-logs", endpoints: [ { method: "POST", path: "/api/daily-logs", description: "记录日常" }, { method: "GET", path: "/api/daily-logs", description: "查询日常记录" }, ], }, ], }; // ═══════════════════════════════════════════════════════ describe("1 — 目录结构完整性", () => { it("生成完整后端项目目录", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(!result.error, `Unexpected error: ${result.error}`); assert.ok(result.stats.totalFiles >= 15, `Expected >= 15 files, got ${result.stats.totalFiles}`); }); it("生成 package.json", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["package.json"], "Missing package.json"); const pkg = JSON.parse(result.files["package.json"]); assert.ok(pkg.dependencies.fastify, "fastify dependency missing"); assert.ok(pkg.dependencies["sql.js"], "sql.js dependency missing"); assert.ok(pkg.dependencies["@fastify/jwt"], "@fastify/jwt dependency missing"); assert.ok(pkg.dependencies.bcrypt, "bcrypt dependency missing"); assert.ok(pkg.devDependencies.typescript, "typescript devDependency missing"); assert.ok(pkg.scripts.build, "build script missing"); assert.ok(pkg.scripts.test, "test script missing"); assert.ok(pkg.scripts.dev, "dev script missing"); }); it("生成 tsconfig.json", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["tsconfig.json"], "Missing tsconfig.json"); const cfg = JSON.parse(result.files["tsconfig.json"]); assert.equal(cfg.compilerOptions.strict, true); assert.equal(cfg.compilerOptions.module, "ESNext"); assert.ok(cfg.compilerOptions.outDir); }); it("生成 .env.example", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files[".env.example"]); const env = result.files[".env.example"]; assert.ok(env.includes("JWT_SECRET")); assert.ok(env.includes("DATABASE_URL")); assert.ok(env.includes("PORT")); }); it("生成 README.md", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["README.md"]); const readme = result.files["README.md"]; assert.ok(readme.includes("Fastify")); assert.ok(readme.includes("SQLite")); assert.ok(readme.includes("npm install")); assert.ok(readme.includes("## API Endpoints")); }); it("生成 .gitignore", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files[".gitignore"]); assert.ok(result.files[".gitignore"].includes("node_modules")); }); }); // ═══════════════════════════════════════════════════════ describe("2 — src/index.ts(服务入口)", () => { it("生成 src/index.ts", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["src/index.ts"]); const content = result.files["src/index.ts"]; assert.ok(content.includes("Fastify"), "Missing Fastify import"); assert.ok(content.includes("buildApp"), "Missing buildApp function"); assert.ok(content.includes("@fastify/jwt"), "Missing JWT plugin"); assert.ok(content.includes("@fastify/cors"), "Missing CORS plugin"); assert.ok(content.includes("authRoutes"), "Missing auth routes"); assert.ok(content.includes("petsRoutes") || content.includes("/api/pets"), "Missing pets routes"); assert.ok(content.includes("/api/health"), "Missing health check"); }); }); // ═══════════════════════════════════════════════════════ describe("3 — src/db/(数据库层)", () => { it("生成 src/db/schema.ts", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["src/db/schema.ts"]); const content = result.files["src/db/schema.ts"]; assert.ok(content.includes("CREATE TABLE"), "Missing CREATE TABLE"); assert.ok(content.includes("users"), "Missing users table"); assert.ok(content.includes("pets"), "Missing pets table"); assert.ok(content.includes("schedules"), "Missing schedules table"); assert.ok(content.includes("daily_logs"), "Missing daily_logs table"); }); it("生成 src/db/client.ts", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["src/db/client.ts"]); const content = result.files["src/db/client.ts"]; assert.ok(content.includes("sql.js"), "Missing sql.js import"); assert.ok(content.includes("getDb"), "Missing getDb function"); assert.ok(content.includes("queryAll"), "Missing queryAll helper"); assert.ok(content.includes("queryOne"), "Missing queryOne helper"); assert.ok(content.includes("execute"), "Missing execute helper"); assert.ok(content.includes("initDb"), "Missing initDb function"); }); }); // ═══════════════════════════════════════════════════════ describe("4 — src/routes/(路由层)", () => { it("生成 CRUD 路由文件", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.stats.routes >= 3, `Expected >= 3 routes, got ${result.stats.routes}`); assert.ok(result.files["src/routes/pets.ts"], "Missing pets routes"); assert.ok(result.files["src/routes/schedules.ts"], "Missing schedules routes"); assert.ok(result.files["src/routes/daily_logs.ts"], "Missing daily_logs routes"); }); it("生成 auth 路由", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["src/routes/auth.ts"]); const content = result.files["src/routes/auth.ts"]; assert.ok(content.includes("register"), "Missing register route"); assert.ok(content.includes("login"), "Missing login route"); assert.ok(content.includes("/me"), "Missing /me route"); assert.ok(content.includes("bcrypt"), "Missing bcrypt import"); assert.ok(content.includes("jwt.sign"), "Missing JWT sign"); }); it("路由文件包含 CRUD 方法", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const petsRoute = result.files["src/routes/pets.ts"]; assert.ok(petsRoute.includes("authenticate"), "Missing auth middleware"); assert.ok(petsRoute.includes("app.get"), "Missing GET"); assert.ok(petsRoute.includes("app.post"), "Missing POST"); assert.ok(petsRoute.includes("app.put"), "Missing PUT"); assert.ok(petsRoute.includes("app.delete"), "Missing DELETE"); }); }); // ═══════════════════════════════════════════════════════ describe("5 — src/middleware/(中间件)", () => { it("生成 JWT 认证中间件", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["src/middleware/auth.ts"]); const content = result.files["src/middleware/auth.ts"]; assert.ok(content.includes("authenticate"), "Missing authenticate"); assert.ok(content.includes("requireAdmin"), "Missing requireAdmin"); assert.ok(content.includes("optionalAuth"), "Missing optionalAuth"); assert.ok(content.includes("jwtVerify"), "Missing jwtVerify call"); assert.ok(content.includes("status(401)"), "Missing 401 response"); }); }); // ═══════════════════════════════════════════════════════ describe("6 — src/services/(服务层)", () => { it("生成服务文件", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.stats.services >= 3, `Expected >= 3 services, got ${result.stats.services}`); assert.ok(result.files["src/services/pet.ts"], "Missing pet service"); assert.ok(result.files["src/services/schedule.ts"], "Missing schedule service"); }); it("服务文件包含 CRUD 方法", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const svc = result.files["src/services/pet.ts"]; assert.ok(svc.includes("list()"), "Missing list()"); assert.ok(svc.includes("getById"), "Missing getById()"); assert.ok(svc.includes("create"), "Missing create()"); assert.ok(svc.includes("update"), "Missing update()"); assert.ok(svc.includes("delete"), "Missing delete()"); assert.ok(svc.includes("class"), "Missing class definition"); }); }); // ═══════════════════════════════════════════════════════ describe("7 — src/types/(类型定义)", () => { it("生成类型文件", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.files["src/types/index.ts"]); const types = result.files["src/types/index.ts"]; assert.ok(types.includes("export interface"), "Missing interfaces"); }); it("包含数据模型类型", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const types = result.files["src/types/index.ts"]; assert.ok(types.includes("interface Pet"), "Missing Pet type"); assert.ok(types.includes("interface Schedule"), "Missing Schedule type"); assert.ok(types.includes("interface User"), "Missing User type"); }); it("包含 Create/Update 输入类型", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const types = result.files["src/types/index.ts"]; assert.ok(types.includes("CreatePetInput"), "Missing CreatePetInput"); assert.ok(types.includes("UpdatePetInput"), "Missing UpdatePetInput"); }); it("包含 Auth 类型", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const types = result.files["src/types/index.ts"]; assert.ok(types.includes("LoginInput"), "Missing LoginInput"); assert.ok(types.includes("RegisterInput"), "Missing RegisterInput"); assert.ok(types.includes("AuthResponse"), "Missing AuthResponse"); }); it("包含 JWT payload 类型", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const types = result.files["src/types/index.ts"]; assert.ok(types.includes("JwtPayload"), "Missing JwtPayload"); }); it("包含 API 响应类型", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const types = result.files["src/types/index.ts"]; assert.ok(types.includes("ApiResponse"), "Missing ApiResponse"); assert.ok(types.includes("PaginatedResponse"), "Missing PaginatedResponse"); assert.ok(types.includes("ErrorResponse"), "Missing ErrorResponse"); }); }); // ═══════════════════════════════════════════════════════ describe("8 — 测试生成", () => { it("生成测试文件", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); assert.ok(result.stats.tests >= 2, `Expected >= 2 test files, got ${result.stats.tests}`); assert.ok(result.files["src/__tests__/auth.test.ts"], "Missing auth test"); }); it("auth 测试包含 register/login/me", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const test = result.files["src/__tests__/auth.test.ts"]; assert.ok(test.includes("registers a new user"), "Missing register test"); assert.ok(test.includes("logs in with correct credentials"), "Missing login test"); assert.ok(test.includes("returns current user"), "Missing me test"); assert.ok(test.includes("rejects without token"), "Missing auth fail test"); }); it("CRUD 测试包含完整生命周期", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const test = result.files["src/__tests__/pets.test.ts"]; if (!test) return; // pets test may not exist depending on naming assert.ok(test.includes("creates"), "Missing create test"); assert.ok(test.includes("updates"), "Missing update test"); assert.ok(test.includes("deletes"), "Missing delete test"); assert.ok(test.includes("returns 404 after delete"), "Missing 404-after-delete test"); }); }); // ═══════════════════════════════════════════════════════ describe("9 — 错误处理", () => { it("invalid PRD 返回错误", () => { const result = buildBackend({ error: "test" }, {}); assert.equal(result.error, "INVALID_PRD"); }); it("invalid Architecture 返回错误", () => { const result = buildBackend(DEMO_PRD, { error: "test" }); assert.equal(result.error, "INVALID_ARCH"); }); it("null PRD 返回错误", () => { const result = buildBackend(null, {}); assert.equal(result.error, "INVALID_PRD"); }); it("空的 databaseSchema 仍然生成基础项目", () => { const result = buildBackend(DEMO_PRD, { ...DEMO_ARCH, databaseSchema: [] }); assert.ok(!result.error); assert.ok(result.files["src/index.ts"]); assert.ok(result.files["src/routes/auth.ts"]); assert.ok(result.stats.totalFiles >= 8, `Expected >= 8 files with empty schema, got ${result.stats.totalFiles}`); }); }); // ═══════════════════════════════════════════════════════ describe("10 — 文件写入 I/O", () => { it("writeBackend 写入完整项目", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const tmpDir = resolve(WORKSPACE, ".tmp-backend-test"); writeBackend(result, tmpDir); assert.ok(existsSync(resolve(tmpDir, "package.json"))); assert.ok(existsSync(resolve(tmpDir, "tsconfig.json"))); assert.ok(existsSync(resolve(tmpDir, ".env.example"))); assert.ok(existsSync(resolve(tmpDir, "README.md"))); assert.ok(existsSync(resolve(tmpDir, "src/index.ts"))); assert.ok(existsSync(resolve(tmpDir, "src/db/schema.ts"))); assert.ok(existsSync(resolve(tmpDir, "src/db/client.ts"))); assert.ok(existsSync(resolve(tmpDir, "src/middleware/auth.ts"))); assert.ok(existsSync(resolve(tmpDir, "src/routes/auth.ts"))); assert.ok(existsSync(resolve(tmpDir, "src/types/index.ts"))); // Check at least one route & service file const routesDir = resolve(tmpDir, "src/routes"); const servicesDir = resolve(tmpDir, "src/services"); assert.ok(existsSync(routesDir)); assert.ok(existsSync(servicesDir)); rmSync(tmpDir, { recursive: true, force: true }); }); it("loadJSON 读取文件", () => { const { data } = loadJSON(resolve(WORKSPACE, "prd-example.json")); assert.ok(data.projectName); }); it("loadJSON 不存在的文件返回 error", () => { const { data, error } = loadJSON("/nonexistent.json"); assert.equal(data, null); assert.ok(error); }); }); // ═══════════════════════════════════════════════════════ describe("11 — npm install & build", () => { it("npm install 成功", async () => { const generateAndInstall = async () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const tmpDir = resolve(WORKSPACE, ".tmp-backend-build"); writeBackend(result, tmpDir); const { execSync } = await import("node:child_process"); try { execSync("npm install --no-audit --no-fund --prefer-offline", { cwd: tmpDir, encoding: "utf-8", timeout: 120_000, stdio: "pipe", }); return { success: true, tmpDir }; } catch (e) { return { success: false, error: e.stderr || e.message, tmpDir }; } }; const { success, error, tmpDir } = await generateAndInstall(); if (!success) { console.error("npm install failed:", error?.slice(0, 500)); } assert.ok(success, `npm install failed: ${error?.slice(0, 200)}`); // Cleanup rmSync(tmpDir, { recursive: true, force: true }); }); it("npm run build 成功", async () => { const generateAndBuild = async () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const tmpDir = resolve(WORKSPACE, ".tmp-backend-build"); writeBackend(result, tmpDir); const { execSync } = await import("node:child_process"); try { execSync("npm install --no-audit --no-fund --prefer-offline", { cwd: tmpDir, encoding: "utf-8", timeout: 120_000, stdio: "pipe", }); execSync("npx tsc --noEmit", { cwd: tmpDir, encoding: "utf-8", timeout: 60_000, stdio: "pipe", }); return { success: true, tmpDir }; } catch (e) { return { success: false, error: e.stderr || e.message, tmpDir }; } }; const { success, error, tmpDir } = await generateAndBuild(); if (!success) { console.error("Build failed:", error?.slice(0, 1000)); } assert.ok(success, `Build failed: ${error?.slice(0, 300)}`); // Cleanup rmSync(tmpDir, { recursive: true, force: true }); }); }); // ═══════════════════════════════════════════════════════ describe("12 — CLI 集成", () => { it("--prd + --arch CLI 模式", async () => { const result = buildBackend( loadPRD("ecommerce"), loadArch() ); writeBackend(result, resolve(WORKSPACE, ".tmp-cli-backend")); assert.ok(!result.error); rmSync(resolve(WORKSPACE, ".tmp-cli-backend"), { recursive: true, force: true }); }); it("--help 显示帮助", async () => { const { execSync } = await import("node:child_process"); const output = execSync("node scripts/backend-builder-agent.mjs --help", { cwd: WORKSPACE, encoding: "utf-8", }); assert.ok(output.includes("Backend Builder")); assert.ok(output.includes("Usage")); }); }); // ═══════════════════════════════════════════════════════ describe("13 — 多领域覆盖", () => { const domains = ["ecommerce", "enterprise", "education", "note"]; for (const domain of domains) { it(`${domain} 域生成成功`, () => { const prd = loadPRD(domain); const result = buildBackend(prd, loadArch()); assert.ok(!result.error, `${domain}: ${result.message}`); assert.ok(result.stats.totalFiles >= 8, `${domain}: expected >= 8 files, got ${result.stats.totalFiles}`); assert.ok(result.stats.routes >= 2, `${domain}: expected >= 2 routes, got ${result.stats.routes}`); assert.ok(result.stats.services >= 2, `${domain}: expected >= 2 services, got ${result.stats.services}`); }); } }); // ═══════════════════════════════════════════════════════ describe("14 — 生成的内容非空且有意义", () => { it("所有生成的文件内容非空(>50 字符)", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); for (const [path, content] of Object.entries(result.files)) { assert.ok(content.length > 50, `${path}: too short (${content.length} chars)`); } }); it("package.json 可解析为合法 JSON", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const pkg = JSON.parse(result.files["package.json"]); assert.ok(pkg.name); assert.ok(pkg.dependencies); assert.ok(pkg.scripts); }); it("tsconfig.json 可解析为合法 JSON", () => { const result = buildBackend(DEMO_PRD, DEMO_ARCH); const cfg = JSON.parse(result.files["tsconfig.json"]); assert.ok(cfg.compilerOptions); }); });