🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Domain Benchmark Suite — runs 10 domains through full pipeline.
|
||||
*
|
||||
* For each domain:
|
||||
* 1. Generate fullstack project
|
||||
* 2. npm install + tsc build
|
||||
* 3. Start server, test Health, Auth, CRUD
|
||||
*
|
||||
* Output: benchmark-report.md
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from "node:child_process";
|
||||
import { existsSync, rmSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as http from "node:http";
|
||||
|
||||
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
|
||||
const WORKSPACE = resolve(__dirname, "..", "..");
|
||||
const BENCH_DIR = resolve(WORKSPACE, ".tmp-benchmark");
|
||||
|
||||
const DOMAINS = [
|
||||
{
|
||||
name: "PetCare", label: "宠物管理",
|
||||
prd: { projectName: "PetCare", domain: "pet", summary: "宠物健康管理应用", features: [{ name: "宠物档案", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "pets", 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: "weight_kg", type: "REAL" }, { name: "created_at", type: "TEXT" }] }, { table: "schedules", 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" }, { name: "created_at", type: "TEXT" }] }, { table: "daily_logs", 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" }] }], apiDesign: [{ resource: "pets", basePath: "/api/pets", endpoints: [{ method: "GET", path: "/api/pets" }, { method: "POST", path: "/api/pets" }, { method: "GET", path: "/api/pets/:id" }, { method: "PUT", path: "/api/pets/:id" }, { method: "DELETE", path: "/api/pets/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "CRM", label: "客户关系管理",
|
||||
prd: { projectName: "CRM", domain: "enterprise", summary: "CRM系统", features: [{ name: "客户管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "customers", 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: "company", type: "TEXT" }, { name: "email", type: "TEXT" }, { name: "phone", type: "TEXT" }, { name: "status", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "deals", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "customer_id", type: "TEXT", constraints: "NOT NULL, FK → customers.id" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "amount", type: "REAL" }, { name: "stage", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "activities", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "customer_id", type: "TEXT", constraints: "NOT NULL, FK → customers.id" }, { name: "type", type: "TEXT", constraints: "NOT NULL" }, { name: "note", type: "TEXT", constraints: "NOT NULL" }, { name: "created_at", type: "TEXT" }] }], apiDesign: [{ resource: "customers", basePath: "/api/customers", endpoints: [{ method: "GET", path: "/api/customers" }, { method: "POST", path: "/api/customers" }, { method: "GET", path: "/api/customers/:id" }, { method: "PUT", path: "/api/customers/:id" }, { method: "DELETE", path: "/api/customers/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "Inventory", label: "库存管理",
|
||||
prd: { projectName: "Inventory", domain: "ecommerce", summary: "库存管理", features: [{ name: "商品管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "products", 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: "sku", type: "TEXT" }, { name: "quantity", type: "INTEGER" }, { name: "price", type: "REAL" }, { name: "created_at", type: "TEXT" }] }, { table: "transactions", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "product_id", type: "TEXT", constraints: "NOT NULL, FK → products.id" }, { name: "type", type: "TEXT", constraints: "NOT NULL" }, { name: "quantity", type: "INTEGER", constraints: "NOT NULL" }, { name: "note", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }], apiDesign: [{ resource: "products", basePath: "/api/products", endpoints: [{ method: "GET", path: "/api/products" }, { method: "POST", path: "/api/products" }, { method: "GET", path: "/api/products/:id" }, { method: "PUT", path: "/api/products/:id" }, { method: "DELETE", path: "/api/products/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "TicketSystem", label: "工单系统",
|
||||
prd: { projectName: "TicketSystem", domain: "enterprise", summary: "工单管理", features: [{ name: "工单管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "tickets", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "owner_id", type: "TEXT", constraints: "NOT NULL, FK → users.id" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "description", type: "TEXT", constraints: "NOT NULL" }, { name: "priority", type: "TEXT" }, { name: "status", type: "TEXT" }, { name: "assignee_id", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "comments", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "ticket_id", type: "TEXT", constraints: "NOT NULL, FK → tickets.id" }, { name: "user_id", type: "TEXT", constraints: "NOT NULL" }, { name: "content", type: "TEXT", constraints: "NOT NULL" }, { name: "created_at", type: "TEXT" }] }], apiDesign: [{ resource: "tickets", basePath: "/api/tickets", endpoints: [{ method: "GET", path: "/api/tickets" }, { method: "POST", path: "/api/tickets" }, { method: "GET", path: "/api/tickets/:id" }, { method: "PUT", path: "/api/tickets/:id" }, { method: "DELETE", path: "/api/tickets/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "BlogCMS", label: "博客CMS",
|
||||
prd: { projectName: "BlogCMS", domain: "note", summary: "博客系统", features: [{ name: "文章管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "posts", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "author_id", type: "TEXT", constraints: "NOT NULL, FK → users.id" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "content", type: "TEXT", constraints: "NOT NULL" }, { name: "slug", type: "TEXT" }, { name: "status", type: "TEXT" }, { name: "created_at", type: "TEXT" }, { name: "updated_at", type: "TEXT" }] }, { table: "tags", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "name", type: "TEXT", constraints: "UNIQUE NOT NULL" }, { name: "created_at", type: "TEXT" }] }, { table: "post_tags", fields: [{ name: "post_id", type: "TEXT", constraints: "NOT NULL, FK → posts.id" }, { name: "tag_id", type: "TEXT", constraints: "NOT NULL, FK → tags.id" }] }], apiDesign: [{ resource: "posts", basePath: "/api/posts", endpoints: [{ method: "GET", path: "/api/posts" }, { method: "POST", path: "/api/posts" }, { method: "GET", path: "/api/posts/:id" }, { method: "PUT", path: "/api/posts/:id" }, { method: "DELETE", path: "/api/posts/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "ProjectMgmt", label: "项目管理",
|
||||
prd: { projectName: "ProjectMgmt", domain: "enterprise", summary: "项目管理", features: [{ name: "项目管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "projects", 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: "description", type: "TEXT" }, { name: "status", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "tasks", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "project_id", type: "TEXT", constraints: "NOT NULL, FK → projects.id" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "assignee_id", type: "TEXT" }, { name: "status", type: "TEXT" }, { name: "priority", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }], apiDesign: [{ resource: "projects", basePath: "/api/projects", endpoints: [{ method: "GET", path: "/api/projects" }, { method: "POST", path: "/api/projects" }, { method: "GET", path: "/api/projects/:id" }, { method: "PUT", path: "/api/projects/:id" }, { method: "DELETE", path: "/api/projects/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "HRSystem", label: "人力资源",
|
||||
prd: { projectName: "HRSystem", domain: "enterprise", summary: "人力资源管理", features: [{ name: "员工管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "employees", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "user_id", type: "TEXT", constraints: "NOT NULL, FK → users.id" }, { name: "name", type: "TEXT", constraints: "NOT NULL" }, { name: "department", type: "TEXT", constraints: "NOT NULL" }, { name: "position", type: "TEXT", constraints: "NOT NULL" }, { name: "hire_date", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "attendances", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "employee_id", type: "TEXT", constraints: "NOT NULL, FK → employees.id" }, { name: "check_in", type: "TEXT" }, { name: "check_out", type: "TEXT" }, { name: "date", type: "TEXT", constraints: "NOT NULL" }, { name: "status", type: "TEXT" }] }, { table: "leaves", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "employee_id", type: "TEXT", constraints: "NOT NULL, FK → employees.id" }, { name: "type", type: "TEXT", constraints: "NOT NULL" }, { name: "start_date", type: "TEXT", constraints: "NOT NULL" }, { name: "end_date", type: "TEXT", constraints: "NOT NULL" }, { name: "status", type: "TEXT" }, { name: "reason", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }], apiDesign: [{ resource: "employees", basePath: "/api/employees", endpoints: [{ method: "GET", path: "/api/employees" }, { method: "POST", path: "/api/employees" }, { method: "GET", path: "/api/employees/:id" }, { method: "PUT", path: "/api/employees/:id" }, { method: "DELETE", path: "/api/employees/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "AssetMgmt", label: "资产管理",
|
||||
prd: { projectName: "AssetMgmt", domain: "enterprise", summary: "资产管理", features: [{ name: "资产管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "assets", 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: "category", type: "TEXT", constraints: "NOT NULL" }, { name: "serial_number", type: "TEXT" }, { name: "status", type: "TEXT" }, { name: "assigned_to", type: "TEXT" }, { name: "purchase_date", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "assignments", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "asset_id", type: "TEXT", constraints: "NOT NULL, FK → assets.id" }, { name: "user_id", type: "TEXT", constraints: "NOT NULL" }, { name: "assigned_at", type: "TEXT" }, { name: "returned_at", type: "TEXT" }, { name: "note", type: "TEXT" }] }], apiDesign: [{ resource: "assets", basePath: "/api/assets", endpoints: [{ method: "GET", path: "/api/assets" }, { method: "POST", path: "/api/assets" }, { method: "GET", path: "/api/assets/:id" }, { method: "PUT", path: "/api/assets/:id" }, { method: "DELETE", path: "/api/assets/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "CourseMgmt", label: "课程管理",
|
||||
prd: { projectName: "CourseMgmt", domain: "education", summary: "课程管理", features: [{ name: "课程管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "courses", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "instructor_id", type: "TEXT", constraints: "NOT NULL, FK → users.id" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "description", type: "TEXT" }, { name: "price", type: "REAL" }, { name: "status", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "lessons", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "course_id", type: "TEXT", constraints: "NOT NULL, FK → courses.id" }, { name: "title", type: "TEXT", constraints: "NOT NULL" }, { name: "content", type: "TEXT" }, { name: "sort_order", type: "INTEGER" }, { name: "created_at", type: "TEXT" }] }, { table: "enrollments", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "course_id", type: "TEXT", constraints: "NOT NULL, FK → courses.id" }, { name: "user_id", type: "TEXT", constraints: "NOT NULL" }, { name: "enrolled_at", type: "TEXT" }, { name: "progress", type: "INTEGER" }] }], apiDesign: [{ resource: "courses", basePath: "/api/courses", endpoints: [{ method: "GET", path: "/api/courses" }, { method: "POST", path: "/api/courses" }, { method: "GET", path: "/api/courses/:id" }, { method: "PUT", path: "/api/courses/:id" }, { method: "DELETE", path: "/api/courses/:id" }] }] },
|
||||
},
|
||||
{
|
||||
name: "AppointmentScheduling", label: "预约排程",
|
||||
prd: { projectName: "Appointment", domain: "enterprise", summary: "预约管理", features: [{ name: "预约管理", priority: "P0" }], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] },
|
||||
arch: { databaseSchema: [{ table: "appointments", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "user_id", type: "TEXT", constraints: "NOT NULL, FK → users.id" }, { name: "provider_id", type: "TEXT" }, { name: "service", type: "TEXT", constraints: "NOT NULL" }, { name: "scheduled_at", type: "TEXT", constraints: "NOT NULL" }, { name: "duration_min", type: "INTEGER" }, { name: "status", type: "TEXT" }, { name: "notes", type: "TEXT" }, { name: "created_at", type: "TEXT" }] }, { table: "availability", fields: [{ name: "id", type: "TEXT", constraints: "PK" }, { name: "provider_id", type: "TEXT", constraints: "NOT NULL" }, { name: "day_of_week", type: "INTEGER", constraints: "NOT NULL" }, { name: "start_time", type: "TEXT", constraints: "NOT NULL" }, { name: "end_time", type: "TEXT", constraints: "NOT NULL" }] }], apiDesign: [{ resource: "appointments", basePath: "/api/appointments", endpoints: [{ method: "GET", path: "/api/appointments" }, { method: "POST", path: "/api/appointments" }, { method: "GET", path: "/api/appointments/:id" }, { method: "PUT", path: "/api/appointments/:id" }, { method: "DELETE", path: "/api/appointments/:id" }] }] },
|
||||
},
|
||||
];
|
||||
|
||||
// --- HTTP helpers ---
|
||||
function httpPost(url, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(body);
|
||||
const u = new URL(url);
|
||||
const headers = { "Content-Type": "application/json", "Content-Length": payload.length.toString() };
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: "POST", headers, timeout: 10000 }, (res) => {
|
||||
let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve({ raw: d, status: res.statusCode }); } });
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function httpGet(url, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const headers = {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
http.get({ hostname: u.hostname, port: u.port, path: u.pathname, headers, timeout: 10000 }, (res) => {
|
||||
let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve({ raw: d, status: res.statusCode }); } });
|
||||
}).on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function httpPut(url, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(body);
|
||||
const u = new URL(url);
|
||||
const headers = { "Content-Type": "application/json", "Content-Length": payload.length.toString() };
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: "PUT", headers, timeout: 10000 }, (res) => {
|
||||
let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve({ raw: d, status: res.statusCode }); } });
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForServer(url, timeoutMs = 20000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
function check() {
|
||||
http.get(url, (res) => { res.resume(); res.on("end", resolve); }).on("error", () => {
|
||||
if (Date.now() - start > timeoutMs) reject(new Error("Server never started"));
|
||||
else setTimeout(check, 500);
|
||||
});
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
async function runCustomCommand(domainLabel, tmpDir, installOnly = false) {
|
||||
try {
|
||||
execSync("npm install --no-audit --no-fund", { cwd: tmpDir, encoding: "utf-8", timeout: 180000, stdio: "pipe" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runDomain(i, domain) {
|
||||
console.log(`\n${"─".repeat(60)}`);
|
||||
console.log(`[${i + 1}/${DOMAINS.length}] ${domain.label} (${domain.name})`);
|
||||
console.log(`${"─".repeat(60)}`);
|
||||
|
||||
const result = {
|
||||
name: domain.name,
|
||||
label: domain.label,
|
||||
files: 0,
|
||||
build: false,
|
||||
health: false,
|
||||
register: false,
|
||||
login: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
errors: [],
|
||||
timeMs: 0,
|
||||
};
|
||||
const t0 = Date.now();
|
||||
|
||||
const tmpDir = `${BENCH_DIR}/${domain.name}`;
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
// 1. Generate
|
||||
try {
|
||||
const { composeFullstack, writeFullstack } = await import("../../scripts/fullstack-composer-agent.mjs");
|
||||
const genResult = await composeFullstack(domain.prd, domain.arch);
|
||||
if (genResult.error) {
|
||||
result.errors.push(`Generation: ${genResult.error} ${genResult.message}`);
|
||||
result.timeMs = Date.now() - t0;
|
||||
return result;
|
||||
}
|
||||
writeFullstack(genResult, tmpDir);
|
||||
result.files = genResult.stats.totalFiles;
|
||||
console.log(` Generated ${result.files} files`);
|
||||
} catch (e) {
|
||||
result.errors.push(`Generation: ${e.message}`);
|
||||
result.timeMs = Date.now() - t0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clean stale db
|
||||
try { rmSync(tmpDir + "/apps/api/data", { recursive: true, force: true }); } catch {}
|
||||
|
||||
// 2. Install
|
||||
console.log(" npm install...");
|
||||
try {
|
||||
execSync("npm install --no-audit --no-fund", { cwd: tmpDir, encoding: "utf-8", timeout: 180000, stdio: "pipe" });
|
||||
console.log(" ✅ npm install");
|
||||
} catch (e) {
|
||||
result.errors.push(`npm install: ${(e.stderr || "").slice(0, 200)}`);
|
||||
console.log(" ❌ npm install failed");
|
||||
result.timeMs = Date.now() - t0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 3. Build (tsc)
|
||||
console.log(" tsc...");
|
||||
try {
|
||||
execSync("npx tsc --noEmit", { cwd: tmpDir + "/apps/api", encoding: "utf-8", timeout: 60000, stdio: "pipe" });
|
||||
result.build = true;
|
||||
console.log(" ✅ tsc");
|
||||
} catch (e) {
|
||||
result.errors.push(`tsc: ${(e.stdout || e.stderr || "").slice(0, 200)}`);
|
||||
console.log(" ❌ tsc failed");
|
||||
result.timeMs = Date.now() - t0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 4. Server + API
|
||||
const port = 4100 + i;
|
||||
const proc = spawn("npx", ["tsx", "src/index.ts"], {
|
||||
cwd: tmpDir + "/apps/api",
|
||||
env: { ...process.env, JWT_SECRET: `bench-${i}`, DATABASE_URL: ":memory:", PORT: String(port) },
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForServer(`http://localhost:${port}/api/health`);
|
||||
console.log(" ✅ Server started");
|
||||
} catch (e) {
|
||||
result.errors.push(`Server start: ${e.message}`);
|
||||
console.log(" ❌ Server failed to start");
|
||||
proc.kill("SIGTERM");
|
||||
result.timeMs = Date.now() - t0;
|
||||
return result;
|
||||
}
|
||||
result.health = true;
|
||||
|
||||
// Auth
|
||||
const username = `u${Date.now()}`;
|
||||
try {
|
||||
const reg = await httpPost(`http://localhost:${port}/api/auth/register`, { username, password: "123456" });
|
||||
if (!reg.token || !reg.user?.id) throw new Error(`malformed: ${JSON.stringify(reg).slice(0, 100)}`);
|
||||
const token = reg.token;
|
||||
const userId = reg.user.id;
|
||||
result.register = true;
|
||||
console.log(" ✅ Register");
|
||||
|
||||
// Login
|
||||
try {
|
||||
const login = await httpPost(`http://localhost:${port}/api/auth/login`, { username, password: "123456" });
|
||||
if (login.token) result.login = true;
|
||||
console.log(" ✅ Login");
|
||||
} catch (e) {
|
||||
result.errors.push(`Login: ${e.message}`);
|
||||
console.log(" ❌ Login");
|
||||
}
|
||||
|
||||
// CRUD — find resource endpoint with POST
|
||||
const resource = (domain.arch.apiDesign || [])[0];
|
||||
const crudPath = resource?.basePath || "";
|
||||
const base = `http://localhost:${port}${crudPath}`;
|
||||
|
||||
// Build create payload: FK→userId, required fields→sample
|
||||
const table = (domain.arch.databaseSchema || [])[0];
|
||||
const payload = {};
|
||||
for (const f of (table?.fields || [])) {
|
||||
if (f.name === "id") continue;
|
||||
const fname = f.name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
if (f.constraints?.includes("FK")) {
|
||||
payload[fname] = userId;
|
||||
} else if (f.constraints?.includes("NOT NULL")) {
|
||||
if (f.type?.toUpperCase().includes("INT") || f.type?.toUpperCase().includes("REAL")) payload[fname] = 42;
|
||||
else if (f.type?.toUpperCase().includes("TEXT")) payload[fname] = `bench-${domain.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Create
|
||||
let createdId;
|
||||
try {
|
||||
const create = await httpPost(base, payload, token);
|
||||
createdId = create.data?.id;
|
||||
if (createdId) {
|
||||
result.create = true;
|
||||
console.log(" ✅ Create");
|
||||
} else {
|
||||
result.errors.push(`Create: no id in ${JSON.stringify(create).slice(0, 200)}`);
|
||||
console.log(" ❌ Create: no id");
|
||||
}
|
||||
} catch (e) {
|
||||
result.errors.push(`Create: ${e.message}`);
|
||||
console.log(" ❌ Create");
|
||||
}
|
||||
|
||||
// Read
|
||||
if (createdId) {
|
||||
try {
|
||||
const list = await httpGet(base, token);
|
||||
if (list.data?.length > 0) result.read = true;
|
||||
console.log(` ${result.read ? "✅" : "❌"} Read`);
|
||||
} catch (e) {
|
||||
result.errors.push(`Read: ${e.message}`);
|
||||
console.log(" ❌ Read");
|
||||
}
|
||||
|
||||
// Update
|
||||
try {
|
||||
const upd = await httpPut(`${base}/${createdId}`, { ...payload }, token);
|
||||
if (upd.data?.id) result.update = true;
|
||||
console.log(` ${result.update ? "✅" : "❌"} Update`);
|
||||
|
||||
// Delete
|
||||
try {
|
||||
const del = await httpRequest("DELETE", `${base}/${createdId}`, null, token);
|
||||
if (del >= 200 && del < 300) result.delete = true;
|
||||
console.log(` ${result.delete ? "✅" : "❌"} Delete (HTTP ${del})`);
|
||||
} catch (e) {
|
||||
result.errors.push(`Delete: ${e.message}`);
|
||||
console.log(" ❌ Delete");
|
||||
}
|
||||
} catch (e) {
|
||||
// PUT failed but try DELETE anyway with POST (some frameworks lack PUT)
|
||||
try {
|
||||
const del = await httpRequest("DELETE", `${base}/${createdId}`, null, token);
|
||||
if (del >= 200 && del < 300) result.delete = true;
|
||||
console.log(` ${result.delete ? "✅" : "❌"} Delete (HTTP ${del})`);
|
||||
} catch (e2) {
|
||||
result.errors.push(`Update/Delete: ${e.message}; ${e2.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
result.errors.push(`Auth: ${e.message}`);
|
||||
console.log(" ❌ Auth failed");
|
||||
}
|
||||
|
||||
proc.kill("SIGTERM");
|
||||
result.timeMs = Date.now() - t0;
|
||||
return result;
|
||||
}
|
||||
|
||||
function httpRequest(method, url, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const headers = {};
|
||||
if (body) {
|
||||
const p = JSON.stringify(body);
|
||||
headers["Content-Type"] = "application/json";
|
||||
headers["Content-Length"] = p.length.toString();
|
||||
}
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method, headers, timeout: 10000 }, (res) => {
|
||||
let d = ""; res.on("data", c => d += c); res.on("end", () => resolve(res.statusCode));
|
||||
});
|
||||
req.on("error", reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Report ---
|
||||
function generateReport(results, totalMs) {
|
||||
const pass = r => r.build && r.create && r.read && r.update && r.delete;
|
||||
const passed = results.filter(pass);
|
||||
const lines = [];
|
||||
|
||||
lines.push("# SF-05 Domain Benchmark Report");
|
||||
lines.push("");
|
||||
lines.push(`> **Generated:** ${new Date().toISOString()}`);
|
||||
lines.push(`> **Domains Tested:** ${results.length}`);
|
||||
lines.push(`> **Total Time:** ${(totalMs / 1000).toFixed(1)}s`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Summary");
|
||||
lines.push("");
|
||||
lines.push(`| Metric | Value |`);
|
||||
lines.push(`|--------|-------|`);
|
||||
lines.push(`| Total Domains | ${results.length} |`);
|
||||
lines.push(`| Build Pass | ${results.filter(r => r.build).length}/${results.length} |`);
|
||||
lines.push(`| CRUD Pass | ${passed.length}/${results.length} |`);
|
||||
lines.push(`| PASS RATE | **${((passed.length / results.length) * 100).toFixed(0)}%** |`);
|
||||
lines.push(`| Avg Files | ${(results.reduce((s, r) => s + r.files, 0) / results.length).toFixed(0)} |`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Domain Results");
|
||||
lines.push("");
|
||||
lines.push(`| # | Domain | Files | Build | Health | Register | Login | Create | Read | Update | Delete | Result |`);
|
||||
lines.push(`|---|--------|-------|-------|--------|----------|-------|--------|------|--------|--------|--------|`);
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
const ik = v => v ? "✅" : "❌";
|
||||
const status = pass(r) ? "✅ PASS" : r.build ? "⚠️ PARTIAL" : "❌ FAIL";
|
||||
lines.push(`| ${i + 1} | ${r.name} | ${r.files} | ${ik(r.build)} | ${ik(r.health)} | ${ik(r.register)} | ${ik(r.login)} | ${ik(r.create)} | ${ik(r.read)} | ${ik(r.update)} | ${ik(r.delete)} | ${status} |`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
// Failures
|
||||
const failed = results.filter(r => !pass(r));
|
||||
if (failed.length > 0) {
|
||||
lines.push("## Failure Analysis");
|
||||
lines.push("");
|
||||
for (const r of failed) {
|
||||
lines.push(`### ${r.name} (${r.label})`);
|
||||
for (const e of r.errors) lines.push(`- ${e}`);
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("## ✅ Passed");
|
||||
if (passed.length > 0) {
|
||||
for (const r of passed) lines.push(`- **${r.name}** (${r.label}) — ${r.files} files, ${(r.timeMs / 1000).toFixed(1)}s`);
|
||||
} else {
|
||||
lines.push("None — all domains have issues.");
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Required Fixes");
|
||||
const errorCats = {};
|
||||
for (const r of failed) for (const e of r.errors) {
|
||||
const cat = e.split(":")[0];
|
||||
errorCats[cat] = (errorCats[cat] || 0) + 1;
|
||||
}
|
||||
for (const [cat, count] of Object.entries(errorCats).sort((a, b) => b[1] - a[1])) {
|
||||
lines.push(`- **${cat}**: ${count} occurrence(s)`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
async function main() {
|
||||
console.log("=".repeat(60));
|
||||
console.log("SF-05 Domain Benchmark Suite");
|
||||
console.log("=".repeat(60));
|
||||
|
||||
const t0 = Date.now();
|
||||
const results = [];
|
||||
|
||||
// Clear temp
|
||||
rmSync(BENCH_DIR, { recursive: true, force: true });
|
||||
|
||||
for (let i = 0; i < DOMAINS.length; i++) {
|
||||
const r = await runDomain(i, DOMAINS[i]);
|
||||
results.push(r);
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - t0;
|
||||
const report = generateReport(results, elapsed);
|
||||
writeFileSync(resolve(WORKSPACE, "benchmark-report.md"), report);
|
||||
|
||||
console.log(`\n${"=".repeat(60)}`);
|
||||
console.log(`📊 Report: benchmark-report.md`);
|
||||
console.log(`${"=".repeat(60)}`);
|
||||
console.log(report);
|
||||
}
|
||||
|
||||
main().catch(err => { console.error("FAIL:", err); process.exit(1); });
|
||||
Reference in New Issue
Block a user