🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user