🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* PR-C Deprecation Warning System — Tests
|
||||
*
|
||||
* Run: node --test test/architecture/deprecation-warning.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { warnOnce, resetWarnings, getWarningCount, warnMany, runDeprecationCheck } from "../../scripts/lib/deprecation-warning.mjs";
|
||||
|
||||
const CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
||||
const TEST_DIR = join(homedir(), ".openclaw", "workspace", "test", "architecture");
|
||||
const TMP_CONFIG = join(homedir(), ".openclaw", "openclaw-prc-test.json");
|
||||
|
||||
beforeEach(() => {
|
||||
resetWarnings();
|
||||
});
|
||||
|
||||
describe("PR-C: Deprecation Warning System", () => {
|
||||
// ─── warnOnce utility tests ──────────────────────────────
|
||||
describe("warnOnce utility", () => {
|
||||
it("warn-01: same key only warns once", () => {
|
||||
const r1 = warnOnce("test.key1", "first message");
|
||||
const r2 = warnOnce("test.key1", "second message");
|
||||
const r3 = warnOnce("test.key1", "third message");
|
||||
|
||||
assert.equal(r1, true, "first call must emit");
|
||||
assert.equal(r2, false, "second call must be suppressed");
|
||||
assert.equal(r3, false, "third call must be suppressed");
|
||||
assert.equal(getWarningCount(), 1, "only 1 unique key warned");
|
||||
});
|
||||
|
||||
it("warn-02: different keys both warn", () => {
|
||||
const r1 = warnOnce("test.key1", "msg1");
|
||||
const r2 = warnOnce("test.key2", "msg2");
|
||||
|
||||
assert.equal(r1, true);
|
||||
assert.equal(r2, true);
|
||||
assert.equal(getWarningCount(), 2);
|
||||
});
|
||||
|
||||
it("warn-03: force option bypasses deduplication", () => {
|
||||
warnOnce("test.force", "first");
|
||||
const r = warnOnce("test.force", "second", { force: true });
|
||||
|
||||
assert.equal(r, true, "force must emit even if already warned");
|
||||
assert.equal(getWarningCount(), 1, "key count unchanged (same key)");
|
||||
});
|
||||
|
||||
it("warn-04: resetWarnings clears all state", () => {
|
||||
warnOnce("test.a", "a");
|
||||
warnOnce("test.b", "b");
|
||||
assert.equal(getWarningCount(), 2);
|
||||
|
||||
resetWarnings();
|
||||
assert.equal(getWarningCount(), 0);
|
||||
|
||||
// Can warn again after reset
|
||||
const r = warnOnce("test.a", "a again");
|
||||
assert.equal(r, true);
|
||||
assert.equal(getWarningCount(), 1);
|
||||
});
|
||||
|
||||
it("warn-05: warnMany deduplicates across batch", () => {
|
||||
const emitted = warnMany([
|
||||
{ key: "a", message: "msg a" },
|
||||
{ key: "b", message: "msg b" },
|
||||
{ key: "a", message: "msg a again" }, // duplicate
|
||||
]);
|
||||
|
||||
assert.equal(emitted.length, 2, "only 2 unique warnings emitted");
|
||||
assert.equal(getWarningCount(), 2);
|
||||
});
|
||||
|
||||
it("warn-06: runDeprecationCheck catches errors", () => {
|
||||
const result = runDeprecationCheck("test-check", () => {
|
||||
return [{ key: "test.ok", message: "all good" }];
|
||||
});
|
||||
assert.equal(result.name, "test-check");
|
||||
assert.equal(result.warnings.length, 1);
|
||||
|
||||
const errResult = runDeprecationCheck("bad-check", () => {
|
||||
throw new Error("simulated failure");
|
||||
});
|
||||
assert.equal(errResult.warnings[0].key, "bad-check");
|
||||
assert.ok(errResult.warnings[0].message.includes("simulated failure"));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Config detection tests ─────────────────────────────
|
||||
describe("config detection", () => {
|
||||
it("detect-01: real config loads without crashing", () => {
|
||||
assert.ok(existsSync(CONFIG_PATH), "openclaw.json must exist");
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
assert.ok(typeof config === "object");
|
||||
assert.ok(config.gateway || config.agents, "config must have expected sections");
|
||||
});
|
||||
|
||||
it("detect-02: active-memory blocking mode detected", () => {
|
||||
// Read the actual config to check current state
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const activeCfg = config?.plugins?.entries?.["active-memory"];
|
||||
|
||||
if (activeCfg) {
|
||||
const mode = activeCfg.config?.mode;
|
||||
console.log(` [DETECT] active-memory mode: "${mode || "default"}"`);
|
||||
|
||||
if (mode === "blocking") {
|
||||
const r = warnOnce("active-memory.blocking",
|
||||
"active-memory blocking mode is deprecated. Set mode to 'precompute'.");
|
||||
assert.equal(r, true);
|
||||
}
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-03: dreaming REM phase detected", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const memoryCoreCfg = config?.plugins?.entries?.["memory-core"];
|
||||
|
||||
if (memoryCoreCfg?.config?.dreaming) {
|
||||
const dreaming = memoryCoreCfg.config.dreaming;
|
||||
const remEnabled = dreaming?.phases?.rem?.enabled;
|
||||
console.log(` [DETECT] dreaming REM enabled: ${remEnabled ?? "not configured"}`);
|
||||
|
||||
if (remEnabled === true) {
|
||||
warnOnce("dreaming.rem-phase",
|
||||
"Dreaming REM phase is deprecated. Will be replaced by Collect/Promote.");
|
||||
}
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-04: memory-wiki plugin reference detectable", () => {
|
||||
const wikiPath = join(
|
||||
"/opt/homebrew/lib/node_modules/openclaw",
|
||||
"dist/extensions/memory-wiki/openclaw.plugin.json"
|
||||
);
|
||||
const exists = existsSync(wikiPath);
|
||||
console.log(` [DETECT] memory-wiki plugin: ${exists ? "present" : "not found"}`);
|
||||
|
||||
if (exists) {
|
||||
const plugin = JSON.parse(readFileSync(wikiPath, "utf8"));
|
||||
assert.equal(plugin.id, "memory-wiki");
|
||||
warnOnce("memory-wiki.plugin",
|
||||
"memory-wiki plugin is legacy. Tools remain available during migration.");
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-05: QMD reference in config detectable", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const qmdRefs = [];
|
||||
|
||||
// Check memory search
|
||||
if (config?.agents?.defaults?.memorySearch?.provider === "qmd") {
|
||||
qmdRefs.push("memorySearch.provider=qmd");
|
||||
}
|
||||
if (config?.agents?.defaults?.memorySearch?.backend === "qmd") {
|
||||
qmdRefs.push("memorySearch.backend=qmd");
|
||||
}
|
||||
|
||||
// Check active-memory
|
||||
const activeQmd = config?.plugins?.entries?.["active-memory"]?.config?.qmd;
|
||||
if (activeQmd?.searchMode) {
|
||||
qmdRefs.push("active-memory.qmd.searchMode");
|
||||
}
|
||||
|
||||
console.log(` [DETECT] QMD references found: ${qmdRefs.length}`);
|
||||
for (const ref of qmdRefs) {
|
||||
console.log(` • ${ref}`);
|
||||
warnOnce(`qmd.${ref}`, "QMD memory engine is legacy. Migrate to Builtin MemoryCore.");
|
||||
}
|
||||
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-06: Honcho/LanceDB plugins detectable", () => {
|
||||
const extDir = "/opt/homebrew/lib/node_modules/openclaw/dist/extensions";
|
||||
const legacyPlugins = ["memory-honcho", "memory-lancedb"];
|
||||
let found = 0;
|
||||
|
||||
for (const name of legacyPlugins) {
|
||||
// Check if directory or plugin reference exists
|
||||
// These are NOT in the current install, which is expected (they're optional)
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const enabled = config?.plugins?.entries?.[name]?.enabled;
|
||||
console.log(` [DETECT] ${name}: ${enabled === true ? "enabled" : "not enabled"}`);
|
||||
|
||||
if (enabled === true) {
|
||||
warnOnce(`${name}.plugin`, `${name} memory plugin is legacy. Data readable during migration.`);
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` [DETECT] legacy memory plugins enabled: ${found}`);
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-07: commitments auto-infer detectable", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const autoInfer = config?.commitments?.autoInfer;
|
||||
console.log(` [DETECT] commitments auto-infer: ${autoInfer ?? "not configured"}`);
|
||||
|
||||
if (autoInfer === true) {
|
||||
warnOnce("commitments.auto-infer",
|
||||
"Commitments auto-infer is deprecated. Use explicit cron tasks.");
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Integration tests ──────────────────────────────────
|
||||
describe("integration", () => {
|
||||
it("integ-01: check-deprecations script runs without error", () => {
|
||||
const scriptPath = join(import.meta.dirname, "../../scripts/check-deprecations.mjs");
|
||||
const result = spawnSync("node", [scriptPath], {
|
||||
encoding: "utf8",
|
||||
timeout: 15000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
console.log(" [INTEG] check-deprecations exit code:", result.status);
|
||||
console.log(" [INTEG] check-deprecations stdout:",
|
||||
result.stdout?.slice(0, 400) || "(empty)");
|
||||
|
||||
if (result.stderr) {
|
||||
console.log(" [INTEG] stderr:", result.stderr.slice(0, 200));
|
||||
}
|
||||
|
||||
// Script should never crash — always exit 0
|
||||
assert.equal(result.status, 0,
|
||||
"check-deprecations must exit 0 even with warnings");
|
||||
});
|
||||
|
||||
it("integ-02: deprecation warnings do NOT cause process exit", () => {
|
||||
// warnOnce writes to console.warn — it should never throw
|
||||
assert.doesNotThrow(() => {
|
||||
warnOnce("test.integration", "This is a test deprecation warning");
|
||||
}, "warnOnce must never throw");
|
||||
|
||||
assert.equal(getWarningCount(), 1);
|
||||
});
|
||||
|
||||
it("integ-03: all 8 deprecation categories checkable", () => {
|
||||
const categories = [
|
||||
"active-memory.blocking",
|
||||
"dreaming.rem-phase",
|
||||
"dreaming.dream-diary",
|
||||
"qmd.reference",
|
||||
"honcho.plugin",
|
||||
"lancedb.plugin",
|
||||
"memory-wiki.plugin",
|
||||
"commitments.auto-infer",
|
||||
];
|
||||
|
||||
// Each category should have a defined key pattern
|
||||
for (const cat of categories) {
|
||||
assert.ok(typeof cat === "string" && cat.length > 0,
|
||||
`category ${cat} must be a non-empty string`);
|
||||
}
|
||||
|
||||
console.log(` [INTEG] ${categories.length} deprecation categories defined`);
|
||||
assert.equal(categories.length, 8, "must cover all 8 deprecation categories");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* PR-1 Freeze Architecture Test
|
||||
*
|
||||
* Run: node --test test/architecture/freeze.test.mjs
|
||||
*
|
||||
* Tests:
|
||||
* 1. architecture-freeze-v2.md exists
|
||||
* 2. Document contains core/adapter/legacy/experimental sections
|
||||
* 3. Document contains 90-day legacy policy
|
||||
* 4. check-deprecations output includes replacement info
|
||||
* 5. check-deprecations output includes migration window
|
||||
* 6. production-check still passes
|
||||
* 7. guard still passes
|
||||
* 8. baseline still passes
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const ROOT = __dirname;
|
||||
const SCRIPTS = join(ROOT, "scripts");
|
||||
const DOCS = join(ROOT, "docs");
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────
|
||||
|
||||
function getFileSize(path) {
|
||||
try { return statSync(path).size; } catch { return 0; }
|
||||
}
|
||||
|
||||
describe("PR-1: Freeze Architecture", () => {
|
||||
|
||||
// ─── Test 1 ──────────────────────────────────────────
|
||||
it("architecture-freeze-v2.md exists and is non-empty", () => {
|
||||
const path = join(DOCS, "architecture-freeze-v2.md");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
const size = getFileSize(path);
|
||||
assert.ok(size > 1000, `File too small: ${size} bytes (expected >1000)`);
|
||||
});
|
||||
|
||||
// ─── Test 2 ──────────────────────────────────────────
|
||||
it("document contains core / adapter / legacy / experimental sections", () => {
|
||||
const content = readFileSync(join(DOCS, "architecture-freeze-v2.md"), "utf8");
|
||||
|
||||
const required = [
|
||||
"Core Module",
|
||||
"Adapter Module",
|
||||
"Legacy Module",
|
||||
"Experimental Module",
|
||||
];
|
||||
|
||||
for (const section of required) {
|
||||
assert.ok(content.includes(section),
|
||||
`Missing section: "${section}"`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 3 ──────────────────────────────────────────
|
||||
it("document contains 90-day legacy migration policy", () => {
|
||||
const content = readFileSync(join(DOCS, "architecture-freeze-v2.md"), "utf8");
|
||||
|
||||
assert.ok(content.includes("90-day") || content.includes("90-Day") ||
|
||||
content.includes("90 day") || content.includes("Migration Window"),
|
||||
"Missing 90-day migration policy reference");
|
||||
|
||||
assert.ok(content.includes("2026-09-04"),
|
||||
"Missing removal target date 2026-09-04");
|
||||
});
|
||||
|
||||
// ─── Test 4 ──────────────────────────────────────────
|
||||
it("check-deprecations output includes replacement info", { timeout: 30000 }, () => {
|
||||
const result = spawnSync("node", [join(SCRIPTS, "check-deprecations.mjs")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 25000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-1000));
|
||||
|
||||
// Deprecation check always exits 0 (never blocks on deprecation count)
|
||||
assert.equal(result.status, 0, `Script crashed (exit ${result.status})`);
|
||||
|
||||
// Must contain replacement info in enhanced output
|
||||
assert.ok(
|
||||
output.includes("Replacement:") || output.includes("replacement"),
|
||||
"Missing replacement info in deprecation output"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Test 5 ──────────────────────────────────────────
|
||||
it("check-deprecations output includes migration window", { timeout: 30000 }, () => {
|
||||
const result = spawnSync("node", [join(SCRIPTS, "check-deprecations.mjs")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 25000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
|
||||
// Must contain migration window
|
||||
assert.ok(
|
||||
output.includes("Migration Window") || output.includes("migrationWindow") ||
|
||||
output.includes("Migration Timeline") || output.includes("Removal Target"),
|
||||
"Missing migration window info in deprecation output"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Test 6 ──────────────────────────────────────────
|
||||
it("production-check still passes after freeze", { timeout: 120000 }, () => {
|
||||
const result = spawnSync("bash", [join(SCRIPTS, "production-check.sh")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 110000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-800));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Production check failed after freeze (exit ${result.status}):\n${output.slice(-500)}`);
|
||||
assert.ok(output.includes("PRODUCTION CHECK PASSED") ||
|
||||
output.includes("Overall:"),
|
||||
"Production check summary not found");
|
||||
});
|
||||
|
||||
// ─── Test 7 ──────────────────────────────────────────
|
||||
it("architecture guard still passes after freeze", { timeout: 60000 }, () => {
|
||||
const result = spawnSync("bash", [join(SCRIPTS, "guard-all.sh")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 55000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-500));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Guard failed after freeze (exit ${result.status}):\n${output.slice(-300)}`);
|
||||
});
|
||||
|
||||
// ─── Test 8 ──────────────────────────────────────────
|
||||
it("baseline tests still pass after freeze", { timeout: 60000 }, () => {
|
||||
const result = spawnSync("node", ["--test", "test/baseline/test-*.test.mjs"], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 55000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Baseline tests failed after freeze (exit ${result.status})`);
|
||||
assert.ok(output.includes("fail 0") || !output.includes("fail"),
|
||||
"Baseline tests have failures");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* PR-B Architecture Guard — Unified Test
|
||||
* Runs all 6 guards as node:test sub-tests.
|
||||
*
|
||||
* Run: node --test test/architecture/guard.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const SCRIPTS_DIR = join(__dirname, "scripts");
|
||||
|
||||
function runGuard(name, script, expectExit0 = true) {
|
||||
const result = spawnSync("node", [script], {
|
||||
cwd: __dirname,
|
||||
encoding: "utf8",
|
||||
timeout: 30000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
|
||||
return {
|
||||
name,
|
||||
exitCode: result.status,
|
||||
signal: result.signal,
|
||||
output: output.slice(-500), // Keep last 500 chars for report
|
||||
passed: result.status === 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe("PR-B: Architecture Guard", () => {
|
||||
it("Guard-01: Memory backend count ≤ allowed", () => {
|
||||
const r = runGuard("Memory Backend", join(SCRIPTS_DIR, "guard-memory-backend.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 1 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-02: Runtime core count ≤ 3", () => {
|
||||
const r = runGuard("Runtime Core", join(SCRIPTS_DIR, "guard-runtime-core.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 2 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-03: Tool path inventory (soft)", () => {
|
||||
const r = runGuard("Tool Path", join(SCRIPTS_DIR, "guard-tool-path.mjs"));
|
||||
console.log(r.output);
|
||||
// Soft guard — informational only
|
||||
assert.ok(r.exitCode === 0 || r.exitCode === null,
|
||||
`Guard 3 unexpected error (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-04: MEMORY.md direct write paths unchanged", () => {
|
||||
const r = runGuard("MEMORY.md Write", join(SCRIPTS_DIR, "guard-memory-write.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 4 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-05: Tool trace coverage (soft)", () => {
|
||||
const r = runGuard("Tool Trace", join(SCRIPTS_DIR, "guard-tool-trace.mjs"));
|
||||
console.log(r.output);
|
||||
// Soft guard — informational only
|
||||
assert.ok(r.exitCode === 0 || r.exitCode === null,
|
||||
`Guard 5 unexpected error (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-06: Dreaming phase count ≤ 3", () => {
|
||||
const r = runGuard("Dreaming Phase", join(SCRIPTS_DIR, "guard-dreaming-phase.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 6 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* PR-D Production Check — Architecture Test
|
||||
*
|
||||
* Run: node --test test/architecture/production-check.test.mjs
|
||||
*
|
||||
* Tests:
|
||||
* 1. production-check.sh file exists
|
||||
* 2. production-check.sh is executable
|
||||
* 3. smoke-test.mjs file exists
|
||||
* 4. package.json contains production-check script
|
||||
* 5. Smoke test runs independently
|
||||
* 6. production-check runs end-to-end and exits 0
|
||||
*/
|
||||
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, statSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const ROOT = __dirname;
|
||||
const SCRIPTS = join(ROOT, "scripts");
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────
|
||||
|
||||
function isExecutable(path) {
|
||||
try {
|
||||
const mode = statSync(path).mode;
|
||||
// Check owner execute bit
|
||||
return (mode & 0o100) !== 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────
|
||||
|
||||
describe("PR-D: Production Check Architecture", () => {
|
||||
// Test 1
|
||||
it("production-check.sh file exists", () => {
|
||||
const path = join(SCRIPTS, "production-check.sh");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
});
|
||||
|
||||
// Test 2
|
||||
it("production-check.sh is executable", () => {
|
||||
const path = join(SCRIPTS, "production-check.sh");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
assert.ok(isExecutable(path), `Not executable: ${path}. Run: chmod +x ${path}`);
|
||||
});
|
||||
|
||||
// Test 3
|
||||
it("smoke-test.mjs file exists", () => {
|
||||
const path = join(SCRIPTS, "smoke-test.mjs");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
});
|
||||
|
||||
// Test 4
|
||||
it("package.json contains production-check, smoke, check:deprecations, test:baseline, guard scripts", () => {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
||||
const scripts = pkg.scripts || {};
|
||||
|
||||
assert.ok(typeof scripts["production-check"] === "string",
|
||||
"Missing scripts.production-check in package.json");
|
||||
assert.ok(typeof scripts["smoke"] === "string",
|
||||
"Missing scripts.smoke in package.json");
|
||||
assert.ok(typeof scripts["check:deprecations"] === "string",
|
||||
"Missing scripts.check:deprecations in package.json");
|
||||
assert.ok(typeof scripts["test:baseline"] === "string",
|
||||
"Missing scripts.test:baseline in package.json");
|
||||
assert.ok(typeof scripts["guard"] === "string",
|
||||
"Missing scripts.guard in package.json");
|
||||
});
|
||||
|
||||
// Test 5
|
||||
it("smoke test runs independently and exits 0", { timeout: 30000 }, () => {
|
||||
const script = join(SCRIPTS, "smoke-test.mjs");
|
||||
const result = spawnSync("node", [script], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 25000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-800));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Smoke test failed (exit ${result.status}):\n${output.slice(-500)}`);
|
||||
assert.ok(output.includes("Smoke test PASSED") || output.includes("PASS"),
|
||||
`Expected "Smoke test PASSED" in output:\n${output.slice(-300)}`);
|
||||
});
|
||||
|
||||
// Test 6
|
||||
it("production-check runs end-to-end and exits 0", { timeout: 120000 }, () => {
|
||||
const script = join(SCRIPTS, "production-check.sh");
|
||||
const result = spawnSync("bash", [script], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 110000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-1200));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Production check failed (exit ${result.status}):\n${output.slice(-800)}`);
|
||||
|
||||
// Verify all 6 steps ran
|
||||
const steps = [
|
||||
"Step 1: Environment Check",
|
||||
"Step 2: Baseline Tests",
|
||||
"Step 3: Architecture Guards",
|
||||
"Step 4: Deprecation Check",
|
||||
"Step 5: Smoke Test",
|
||||
"Step 6: Summary Report",
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
assert.ok(output.includes(step),
|
||||
`Missing step in output: "${step}"`);
|
||||
}
|
||||
|
||||
assert.ok(output.includes("Overall:"),
|
||||
"Missing Overall summary");
|
||||
assert.ok(output.includes("Duration:"),
|
||||
"Missing Duration in summary");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* PR-A Baseline Smoke Test
|
||||
* Quick end-to-end sanity check before running full test suite.
|
||||
* Verifies that essential tools are available and the system is in a testable state.
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
describe("PR-A Smoke Test", () => {
|
||||
it("smoke: node version >= 22", () => {
|
||||
const v = process.version;
|
||||
const major = parseInt(v.split(".")[0].replace("v", ""));
|
||||
console.log("[SMOKE] Node.js version:", v);
|
||||
assert.ok(major >= 22, `node >= 22 required, got ${v}`);
|
||||
});
|
||||
|
||||
it("smoke: openclaw CLI available", () => {
|
||||
const out = execSync("openclaw --version 2>&1", {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
console.log("[SMOKE] openclaw:", out.trim().split("\n")[0]);
|
||||
assert.ok(out.includes("OpenClaw"), "openclaw CLI must be available");
|
||||
});
|
||||
|
||||
it("smoke: gateway is reachable", () => {
|
||||
const out = execSync("openclaw status 2>&1", {
|
||||
timeout: 15000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const isRunning = out.includes("running") || out.includes("active") || out.includes("reachable");
|
||||
console.log("[SMOKE] gateway running:", isRunning);
|
||||
assert.ok(isRunning, "gateway must be running");
|
||||
});
|
||||
|
||||
it("smoke: workspace exists", () => {
|
||||
const ws = join(homedir(), ".openclaw", "workspace");
|
||||
assert.ok(existsSync(ws), "workspace must exist");
|
||||
console.log("[SMOKE] workspace:", ws);
|
||||
});
|
||||
|
||||
it("smoke: memory system functional", () => {
|
||||
const out = execSync("openclaw memory status --json 2>/dev/null", {
|
||||
timeout: 15000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const status = JSON.parse(out);
|
||||
console.log("[SMOKE] memory status: OK");
|
||||
assert.ok(Array.isArray(status), "memory status must be an array");
|
||||
});
|
||||
|
||||
it("smoke: config loads without error", () => {
|
||||
const out = execSync("openclaw config get gateway.port 2>&1", {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
console.log("[SMOKE] gateway port:", out.trim());
|
||||
assert.ok(out.length > 0, "config must return value");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* PR-A Baseline Test 01: Gateway Startup
|
||||
* Verifies: gateway process is running and reachable
|
||||
* Freezes: gateway startup contract
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync } from "node:child_process";
|
||||
import { request } from "node:http";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const AGENTS_HOME = join(homedir(), ".openclaw");
|
||||
let _config, _port;
|
||||
|
||||
function loadConfig() {
|
||||
if (!_config) {
|
||||
_config = JSON.parse(readFileSync(join(AGENTS_HOME, "openclaw.json"), "utf8"));
|
||||
_port = _config?.gateway?.port || 18789;
|
||||
}
|
||||
return { config: _config, gatewayPort: _port };
|
||||
}
|
||||
|
||||
function httpGet(host, port, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request({ hostname: host, port, path, method: "GET", timeout: 10000 }, (res) => {
|
||||
let body = "";
|
||||
res.on("data", (d) => (body += d));
|
||||
res.on("end", () => resolve({ status: res.statusCode, body }));
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
describe("test-01: Gateway Startup Baseline", () => {
|
||||
it("01.01 - gateway config exists", () => {
|
||||
const { config } = loadConfig();
|
||||
assert.ok(config, "config should be loaded");
|
||||
assert.ok(config.gateway, "gateway section should exist");
|
||||
});
|
||||
|
||||
it("01.02 - gateway bind port is defined", () => {
|
||||
const { gatewayPort } = loadConfig();
|
||||
assert.ok(gatewayPort > 0 && gatewayPort < 65536, `port ${gatewayPort} must be valid`);
|
||||
});
|
||||
|
||||
it("01.03 - gateway is reachable via HTTP", async () => {
|
||||
const { gatewayPort } = loadConfig();
|
||||
const result = await httpGet("127.0.0.1", gatewayPort, "/");
|
||||
assert.ok(result.status >= 200 && result.status < 400,
|
||||
`gateway HTTP must respond with 2xx/3xx, got ${result.status}`);
|
||||
});
|
||||
|
||||
it("01.04 - gateway status reports running", () => {
|
||||
const out = execSync("openclaw status --json 2>/dev/null", { timeout: 15000, encoding: "utf8" }).trim();
|
||||
assert.ok(out.length > 0, "openclaw status must produce output");
|
||||
try {
|
||||
const s = JSON.parse(out);
|
||||
assert.ok(s, "status must be valid JSON");
|
||||
} catch {
|
||||
assert.ok(!out.toLowerCase().includes("error"), "status must not report error");
|
||||
}
|
||||
});
|
||||
|
||||
it("01.05 - gateway process is not crashing (baseline snapshot)", () => {
|
||||
const out = execSync("openclaw status 2>&1", { timeout: 15000, encoding: "utf8" }).trim();
|
||||
const isRunning = out.includes("running") || out.includes("active") || out.includes("reachable");
|
||||
console.log("[BASELINE] gateway status snapshot:", out.substring(0, 300));
|
||||
assert.ok(isRunning, "gateway must be running");
|
||||
});
|
||||
|
||||
it("01.06 - memory index is accessible", () => {
|
||||
const out = execSync("openclaw memory status --json 2>/dev/null", { timeout: 15000, encoding: "utf8" }).trim();
|
||||
const s = JSON.parse(out);
|
||||
assert.ok(Array.isArray(s) && s.length > 0, "at least one agent memory entry");
|
||||
const main = s.find((e) => e.agentId === "main");
|
||||
assert.ok(main, "main agent must have memory");
|
||||
console.log("[BASELINE] memory backend: %s, files: %d, chunks: %d",
|
||||
main.status.backend, main.status.files, main.status.chunks);
|
||||
});
|
||||
|
||||
it("01.07 - gateway auth mode recorded", () => {
|
||||
const { config } = loadConfig();
|
||||
const mode = config?.gateway?.auth?.mode;
|
||||
console.log("[BASELINE] gateway auth mode:", mode);
|
||||
assert.ok(mode, "gateway auth mode must be defined");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* PR-A Baseline Test 02: Session Creation
|
||||
* Verifies: sessions can be created, read, and have persistent state
|
||||
* Freezes: session creation contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const AGENTS_HOME = join(homedir(), ".openclaw");
|
||||
const SESSIONS_DIR = join(AGENTS_HOME, "agents", "main", "sessions");
|
||||
|
||||
describe("test-02: Session Creation Baseline", () => {
|
||||
it("02.01 - sessions directory exists", () => {
|
||||
const exists = existsSync(SESSIONS_DIR);
|
||||
console.log("[BASELINE] sessions dir:", SESSIONS_DIR);
|
||||
console.log("[BASELINE] sessions dir exists:", exists);
|
||||
// Directory might not exist if no sessions created yet — that's valid baseline
|
||||
if (!exists) {
|
||||
console.log("[BASELINE] No sessions directory — no sessions created yet (valid baseline)");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: sessions dir check");
|
||||
});
|
||||
|
||||
it("02.02 - can list existing sessions", () => {
|
||||
const out = execSync("openclaw status --json 2>/dev/null", {
|
||||
timeout: 15000,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
try {
|
||||
const s = JSON.parse(out);
|
||||
console.log("[BASELINE] sessions count:", s.sessionCount ?? "unknown");
|
||||
assert.ok(typeof s === "object", "status must be an object");
|
||||
} catch {
|
||||
// Text output is fine
|
||||
assert.ok(out.length > 0, "status must produce output");
|
||||
}
|
||||
});
|
||||
|
||||
it("02.03 - session store directory structure recorded", () => {
|
||||
if (existsSync(SESSIONS_DIR)) {
|
||||
const files = readdirSync(SESSIONS_DIR).filter(
|
||||
(f) => f.endsWith(".json") || f.endsWith(".md")
|
||||
);
|
||||
console.log("[BASELINE] session files count:", files.length);
|
||||
console.log("[BASELINE] session file types:",
|
||||
[...new Set(files.map(f => f.slice(f.lastIndexOf('.'))))].join(", "));
|
||||
assert.ok(Array.isArray(files), "session files must be listable");
|
||||
} else {
|
||||
console.log("[BASELINE] No session files to inspect (valid baseline)");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: session dir structure");
|
||||
});
|
||||
|
||||
it("02.04 - workspace files are accessible (session context)", () => {
|
||||
const workspaceDir = join(AGENTS_HOME, "workspace");
|
||||
const keyFiles = ["AGENTS.md", "SOUL.md", "MEMORY.md"];
|
||||
for (const f of keyFiles) {
|
||||
const path = join(workspaceDir, f);
|
||||
const exists = existsSync(path);
|
||||
const size = exists ? readFileSync(path, "utf8").length : 0;
|
||||
console.log(`[BASELINE] workspace/${f}: exists=${exists}, size=${size}`);
|
||||
}
|
||||
assert.ok(existsSync(join(workspaceDir, "AGENTS.md")), "AGENTS.md must exist");
|
||||
assert.ok(existsSync(join(workspaceDir, "MEMORY.md")), "MEMORY.md must exist");
|
||||
});
|
||||
|
||||
it("02.05 - session write lock mechanism exists", () => {
|
||||
// Check if proper-lockfile is available (used for session locking)
|
||||
try {
|
||||
require.resolve("proper-lockfile");
|
||||
console.log("[BASELINE] proper-lockfile: available (session locking)");
|
||||
assert.ok(true, "session locking library available");
|
||||
} catch {
|
||||
console.log("[BASELINE] proper-lockfile: not resolvable (may be bundled)");
|
||||
assert.ok(true, "baseline frozen: lockfile check");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* PR-A Baseline Test 03: memory_search Current Behavior
|
||||
* Verifies: memory_search tool exists, has stable schema, returns stable format
|
||||
* Freezes: memory_search contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
describe("test-03: memory_search Baseline", () => {
|
||||
it("03.01 - memory_search CLI returns JSON", () => {
|
||||
const out = execSync(
|
||||
'openclaw memory search "hello world" --json 2>/dev/null',
|
||||
{ timeout: 15000, encoding: "utf8" }
|
||||
).trim();
|
||||
assert.ok(out.length > 0, "memory search must produce output");
|
||||
try {
|
||||
const result = JSON.parse(out);
|
||||
assert.ok(typeof result === "object", "must be JSON object");
|
||||
console.log("[BASELINE] memory_search result keys:", Object.keys(result).join(", "));
|
||||
console.log("[BASELINE] memory_search results count:",
|
||||
result.results?.length ?? "N/A");
|
||||
} catch (e) {
|
||||
console.log("[BASELINE] memory_search output (not JSON):", out.substring(0, 200));
|
||||
assert.ok(!out.toLowerCase().includes("error"), "must not error");
|
||||
}
|
||||
});
|
||||
|
||||
it("03.02 - memory_search returns results array", () => {
|
||||
const out = execSync(
|
||||
'openclaw memory search "AGENTS.md" --json 2>/dev/null',
|
||||
{ timeout: 15000, encoding: "utf8" }
|
||||
).trim();
|
||||
try {
|
||||
const result = JSON.parse(out);
|
||||
const results = result.results || [];
|
||||
console.log("[BASELINE] search 'AGENTS.md' results:", results.length);
|
||||
if (results.length > 0) {
|
||||
const first = results[0];
|
||||
console.log("[BASELINE] first result keys:", Object.keys(first).join(", "));
|
||||
console.log("[BASELINE] first result file:", first.file || first.path || "N/A");
|
||||
console.log("[BASELINE] first result score:", first.score ?? "N/A");
|
||||
}
|
||||
assert.ok(Array.isArray(results), "results must be array");
|
||||
} catch {
|
||||
console.log("[BASELINE] search output raw:", out.substring(0, 300));
|
||||
}
|
||||
assert.ok(true, "baseline frozen: search results shape");
|
||||
});
|
||||
|
||||
it("03.03 - memory_search schema stable (empty query)", () => {
|
||||
const out = execSync(
|
||||
'openclaw memory search "___nonexistent_xyzzy___" --json 2>/dev/null',
|
||||
{ timeout: 15000, encoding: "utf8" }
|
||||
).trim();
|
||||
try {
|
||||
const result = JSON.parse(out);
|
||||
const results = result.results || [];
|
||||
console.log("[BASELINE] empty search results:", results.length);
|
||||
// Empty results should not error
|
||||
} catch {
|
||||
console.log("[BASELINE] empty search raw:", out.substring(0, 200));
|
||||
}
|
||||
assert.ok(true, "baseline frozen: empty search behavior");
|
||||
});
|
||||
|
||||
it("03.04 - memory_search handles special characters", () => {
|
||||
const queries = [
|
||||
"中文测试",
|
||||
"test-123",
|
||||
"hello/world",
|
||||
"a.b.c",
|
||||
];
|
||||
for (const q of queries) {
|
||||
const out = execSync(
|
||||
`openclaw memory search "${q}" --json 2>/dev/null`,
|
||||
{ timeout: 15000, encoding: "utf8" }
|
||||
).trim();
|
||||
try {
|
||||
JSON.parse(out);
|
||||
console.log(`[BASELINE] search "${q}": OK (valid JSON)`);
|
||||
} catch {
|
||||
console.log(`[BASELINE] search "${q}": non-JSON output`);
|
||||
}
|
||||
assert.ok(!out.toLowerCase().includes("fatal"), `query "${q}" must not crash`);
|
||||
}
|
||||
});
|
||||
|
||||
it("03.05 - memory_search contract: result object shape", () => {
|
||||
const out = execSync(
|
||||
'openclaw memory search "architecture" --json 2>/dev/null',
|
||||
{ timeout: 15000, encoding: "utf8" }
|
||||
).trim();
|
||||
try {
|
||||
const result = JSON.parse(out);
|
||||
// Freeze the top-level keys
|
||||
const topKeys = Object.keys(result).sort();
|
||||
console.log("[BASELINE] memory_search response keys:", topKeys.join(", "));
|
||||
// There must be a 'results' key
|
||||
assert.ok("results" in result || topKeys.length > 0,
|
||||
"must have results key or be non-empty object");
|
||||
} catch {
|
||||
console.log("[BASELINE] raw response:", out.substring(0, 300));
|
||||
}
|
||||
assert.ok(true, "baseline frozen: contract shape");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* PR-A Baseline Test 04: memory_get Current Behavior
|
||||
* Verifies: memory_get tool exists, can read files, stable error format
|
||||
* Freezes: memory_get contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const WORKSPACE = join(homedir(), ".openclaw", "workspace");
|
||||
|
||||
describe("test-04: memory_get Baseline", () => {
|
||||
it("04.01 - MEMORY.md exists and is readable", () => {
|
||||
const path = join(WORKSPACE, "MEMORY.md");
|
||||
assert.ok(existsSync(path), "MEMORY.md must exist");
|
||||
const content = readFileSync(path, "utf8");
|
||||
const lines = content.split("\n").length;
|
||||
const size = content.length;
|
||||
console.log("[BASELINE] MEMORY.md: size=%d bytes, lines=%d", size, lines);
|
||||
assert.ok(size > 0, "MEMORY.md must not be empty");
|
||||
});
|
||||
|
||||
it("04.02 - daily memory files exist", () => {
|
||||
const dailyDir = join(WORKSPACE, "memory");
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const dailyFile = join(dailyDir, `${today}.md`);
|
||||
console.log("[BASELINE] today:", today);
|
||||
console.log("[BASELINE] daily file:", dailyFile);
|
||||
console.log("[BASELINE] daily file exists:", existsSync(dailyFile));
|
||||
assert.ok(existsSync(dailyDir), "memory/ directory must exist");
|
||||
});
|
||||
|
||||
it("04.03 - workspace files measurable", () => {
|
||||
const files = ["AGENTS.md", "SOUL.md", "MEMORY.md", "USER.md", "TOOLS.md"];
|
||||
for (const f of files) {
|
||||
const path = join(WORKSPACE, f);
|
||||
if (existsSync(path)) {
|
||||
const stat = statSync(path);
|
||||
console.log(`[BASELINE] ${f}: size=${stat.size}, mtime=${stat.mtime.toISOString()}`);
|
||||
} else {
|
||||
console.log(`[BASELINE] ${f}: NOT FOUND`);
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: workspace file inventory");
|
||||
});
|
||||
|
||||
it("04.04 - memory_get for nonexistent file returns stable error", () => {
|
||||
// memory_get behavior for missing file: should not crash, should return structured error
|
||||
const nonexistent = "___nonexistent_file_xyzzy___.md";
|
||||
const path = join(WORKSPACE, nonexistent);
|
||||
assert.ok(!existsSync(path), "test file must not exist");
|
||||
console.log("[BASELINE] nonexistent file check: path=", path);
|
||||
// This just verifies the file doesn't exist — the tool behavior
|
||||
// when requesting a missing file would be tested via the actual tool API
|
||||
assert.ok(true, "baseline frozen: missing file expectation");
|
||||
});
|
||||
|
||||
it("04.05 - memory file encoding is UTF-8", () => {
|
||||
const path = join(WORKSPACE, "MEMORY.md");
|
||||
const buf = readFileSync(path);
|
||||
// Check for BOM (should not be present)
|
||||
const hasBOM = buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF;
|
||||
console.log("[BASELINE] MEMORY.md has BOM:", hasBOM);
|
||||
// Read as utf8 should succeed
|
||||
const content = readFileSync(path, "utf8");
|
||||
assert.ok(content.length > 0, "MEMORY.md must decode as UTF-8");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* PR-A Baseline Test 05: exec Tool Current Behavior
|
||||
* Verifies: shell command execution returns stdout/stderr/exitCode correctly
|
||||
* Freezes: exec tool contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const GATEWAY_URL = "http://127.0.0.1:18789";
|
||||
|
||||
describe("test-05: exec Tool Baseline", () => {
|
||||
it("05.01 - local shell execution works (node child_process)", () => {
|
||||
const result = spawnSync("echo", ["hello"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
});
|
||||
console.log("[BASELINE] echo hello stdout:", JSON.stringify(result.stdout.trim()));
|
||||
console.log("[BASELINE] echo hello stderr:", JSON.stringify(result.stderr));
|
||||
console.log("[BASELINE] echo hello status:", result.status);
|
||||
assert.equal(result.stdout.trim(), "hello", "echo must output hello");
|
||||
assert.equal(result.status, 0, "exit code must be 0");
|
||||
});
|
||||
|
||||
it("05.02 - shell error exit codes are preserved", () => {
|
||||
const result = spawnSync("sh", ["-c", "exit 42"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
});
|
||||
console.log("[BASELINE] exit 42 status:", result.status);
|
||||
assert.equal(result.status, 42, "exit code must be preserved exactly");
|
||||
});
|
||||
|
||||
it("05.03 - shell stderr is captured separately", () => {
|
||||
const result = spawnSync("sh", ["-c", "echo stdout-text && echo stderr-text >&2"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
});
|
||||
console.log("[BASELINE] stdout:", JSON.stringify(result.stdout.trim()));
|
||||
console.log("[BASELINE] stderr:", JSON.stringify(result.stderr.trim()));
|
||||
assert.ok(result.stdout.includes("stdout-text"), "stdout must contain message");
|
||||
assert.ok(result.stderr.includes("stderr-text"), "stderr must contain message");
|
||||
});
|
||||
|
||||
it("05.04 - shell output encoding is UTF-8", () => {
|
||||
const result = spawnSync("sh", ["-c", "echo 你好世界"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
});
|
||||
console.log("[BASELINE] UTF-8 output:", JSON.stringify(result.stdout.trim()));
|
||||
assert.ok(result.stdout.includes("你好世界"), "UTF-8 must be preserved");
|
||||
});
|
||||
|
||||
it("05.05 - shell timeout kills process", () => {
|
||||
const start = Date.now();
|
||||
const result = spawnSync("sleep", ["10"], {
|
||||
encoding: "utf8",
|
||||
timeout: 2000, // 2 second timeout
|
||||
});
|
||||
const elapsed = Date.now() - start;
|
||||
console.log("[BASELINE] sleep 10 timeout: elapsed=%dms, killed=%s",
|
||||
elapsed, result.signal || "none");
|
||||
// Process should be killed by timeout
|
||||
assert.ok(result.signal === "SIGTERM" || result.status !== 0 || elapsed < 10000,
|
||||
"long-running process must be terminated by timeout");
|
||||
});
|
||||
|
||||
it("05.06 - exec tool: binary output handling", () => {
|
||||
// Create a null-byte test
|
||||
const result = spawnSync("printf", ["\\x00hello\\x00"], {
|
||||
encoding: "buffer",
|
||||
timeout: 5000,
|
||||
});
|
||||
console.log("[BASELINE] binary stdout length:", result.stdout.length);
|
||||
console.log("[BASELINE] binary stdout bytes:",
|
||||
Array.from(result.stdout.slice(0, 10)).map(b => '0x' + b.toString(16)).join(' '));
|
||||
assert.ok(result.stdout.includes(0x68), "output must contain 'h' (0x68)");
|
||||
});
|
||||
|
||||
it("05.07 - exec tool: large output handled", () => {
|
||||
// Generate 10KB of output
|
||||
const result = spawnSync("sh", ["-c", "yes head | head -1000"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const lines = result.stdout.trim().split("\n").length;
|
||||
console.log("[BASELINE] large output: lines=%d, bytes=%d", lines, result.stdout.length);
|
||||
assert.ok(lines > 100, "large output must be captured");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* PR-A Baseline Test 06: Agent Complete Reply
|
||||
* Verifies: agent can produce a reply via openclaw CLI
|
||||
* Freezes: agent reply contract
|
||||
*
|
||||
* Uses openclaw agent CLI to send a test message and verify reply structure.
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
describe("test-06: Agent Reply Baseline", () => {
|
||||
it("06.01 - agent CLI exists and shows help", () => {
|
||||
const out = execSync("openclaw agent --help 2>&1", {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
console.log("[BASELINE] agent help first 300 chars:", out.substring(0, 300));
|
||||
assert.ok(out.includes("agent") || out.includes("turn"),
|
||||
"agent CLI must exist and show help");
|
||||
});
|
||||
|
||||
it("06.02 - agent run command structure recorded", () => {
|
||||
// Freeze the CLI interface shape
|
||||
const out = execSync("openclaw agent --help 2>&1", {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const hasMessage = out.includes("message") || out.includes("prompt") || out.includes("text");
|
||||
const hasTimeout = out.includes("timeout") || out.includes("Time");
|
||||
const hasModel = out.includes("model") || out.includes("Model");
|
||||
console.log("[BASELINE] agent has --message param:", hasMessage);
|
||||
console.log("[BASELINE] agent has --timeout param:", hasTimeout);
|
||||
console.log("[BASELINE] agent has --model param:", hasModel);
|
||||
assert.ok(true, "baseline frozen: agent CLI interface shape");
|
||||
});
|
||||
|
||||
it("06.03 - gateway agent endpoint is documented", () => {
|
||||
// Check the gateway protocol docs for agent method
|
||||
const docPath = "/opt/homebrew/lib/node_modules/openclaw/docs/gateway/protocol.md";
|
||||
try {
|
||||
const doc = execSync(`head -200 "${docPath}" 2>/dev/null`, {
|
||||
timeout: 5000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
console.log("[BASELINE] protocol docs exist:", doc.length > 0);
|
||||
} catch {
|
||||
console.log("[BASELINE] protocol docs: not directly readable (npm install)");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: agent endpoint documentation");
|
||||
});
|
||||
|
||||
it("06.04 - session/agent persistence directory exists", () => {
|
||||
const out = execSync("openclaw status --json 2>/dev/null", {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
try {
|
||||
const s = JSON.parse(out);
|
||||
console.log("[BASELINE] agent info:", JSON.stringify({
|
||||
agents: s.agents,
|
||||
sessions: s.sessionCount,
|
||||
defaultAgent: s.defaultAgent,
|
||||
}, null, 2));
|
||||
assert.ok(typeof s === "object", "status must be object");
|
||||
} catch {
|
||||
assert.ok(out.length > 0, "status must produce output");
|
||||
}
|
||||
});
|
||||
|
||||
it("06.05 - model providers are configured", () => {
|
||||
try {
|
||||
const configPath = homedir() + "/.openclaw/openclaw.json";
|
||||
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
||||
const providers = config?.models?.providers ?? {};
|
||||
const providerIds = Object.keys(providers);
|
||||
console.log("[BASELINE] configured model providers:", providerIds.join(", "));
|
||||
assert.ok(providerIds.length > 0, "at least one model provider must be configured");
|
||||
} catch (e) {
|
||||
console.log("[BASELINE] config read error:", e.message);
|
||||
assert.ok(true, "baseline frozen: provider config existence");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* PR-A Baseline Test 07: MCP Tool Discovery
|
||||
* Verifies: MCP integration exists, tools can be discovered
|
||||
* Freezes: MCP tool discovery contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const OPENCLAW_HOME = "/opt/homebrew/lib/node_modules/openclaw";
|
||||
const EXTENSIONS_DIR = join(OPENCLAW_HOME, "dist", "extensions");
|
||||
const MCP_DIR = join(OPENCLAW_HOME, "dist", "mcp");
|
||||
|
||||
describe("test-07: MCP Tool Discovery Baseline", () => {
|
||||
it("07.01 - MCP SDK is a dependency", () => {
|
||||
const pkgPath = join(OPENCLAW_HOME, "package.json");
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
||||
const mcpDep = pkg.dependencies?.["@modelcontextprotocol/sdk"];
|
||||
console.log("[BASELINE] MCP SDK version:", mcpDep);
|
||||
assert.ok(mcpDep, "@modelcontextprotocol/sdk must be a dependency");
|
||||
});
|
||||
|
||||
it("07.02 - MCP module exists in dist", () => {
|
||||
const exists = existsSync(MCP_DIR);
|
||||
console.log("[BASELINE] MCP dir exists:", exists);
|
||||
if (exists) {
|
||||
const files = readdirSync(MCP_DIR);
|
||||
console.log("[BASELINE] MCP dir files:", files.join(", "));
|
||||
}
|
||||
assert.ok(exists || existsSync(EXTENSIONS_DIR),
|
||||
"MCP or extensions directory must exist");
|
||||
});
|
||||
|
||||
it("07.03 - MCP API module exports recorded", () => {
|
||||
const apiPath = join(MCP_DIR, "api.js");
|
||||
if (existsSync(apiPath)) {
|
||||
const size = readFileSync(apiPath, "utf8").length;
|
||||
console.log("[BASELINE] MCP api.js size:", size);
|
||||
assert.ok(size > 100, "MCP api.js must be substantial");
|
||||
} else {
|
||||
console.log("[BASELINE] MCP api.js not at expected path (checking alternative)");
|
||||
// Check plugin-tools-serve
|
||||
const servePath = join(MCP_DIR, "plugin-tools-serve.js");
|
||||
if (existsSync(servePath)) {
|
||||
console.log("[BASELINE] MCP plugin-tools-serve.js: exists");
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: MCP module existence");
|
||||
});
|
||||
|
||||
it("07.04 - MCP index exports recorded", () => {
|
||||
const indexPath = join(MCP_DIR, "index.js");
|
||||
if (existsSync(indexPath)) {
|
||||
const content = readFileSync(indexPath, "utf8");
|
||||
const exports = content.match(/export\s+(?:const|function|class|default)\s+(\w+)/g) || [];
|
||||
console.log("[BASELINE] MCP index exports:", exports.map(e => e.split(/\s+/)[2]).join(", "));
|
||||
assert.ok(exports.length > 0, "MCP index must have exports");
|
||||
} else {
|
||||
console.log("[BASELINE] MCP index.js: not found at expected path");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: MCP exports");
|
||||
});
|
||||
|
||||
it("07.05 - MCP protocol SDK version checked", () => {
|
||||
// Find actual MCP SDK
|
||||
try {
|
||||
const mcpSdkPath = require.resolve("@modelcontextprotocol/sdk/package.json", {
|
||||
paths: [OPENCLAW_HOME],
|
||||
});
|
||||
const mcpPkg = JSON.parse(readFileSync(mcpSdkPath, "utf8"));
|
||||
console.log("[BASELINE] MCP SDK actual version:", mcpPkg.version);
|
||||
console.log("[BASELINE] MCP SDK location:", mcpSdkPath);
|
||||
assert.ok(mcpPkg.version, "MCP SDK must have version");
|
||||
} catch (e) {
|
||||
console.log("[BASELINE] MCP SDK not resolvable:", e.message);
|
||||
// SDK might be bundled differently in production
|
||||
}
|
||||
assert.ok(true, "baseline frozen: MCP SDK check");
|
||||
});
|
||||
|
||||
it("07.06 - codex-mcp-projection module exists", () => {
|
||||
// Check plugin-sdk exports
|
||||
const sdkPath = join(OPENCLAW_HOME, "dist", "plugin-sdk", "index.js");
|
||||
if (existsSync(sdkPath)) {
|
||||
const content = readFileSync(sdkPath, "utf8");
|
||||
const hasMCP = content.includes("codex-mcp-projection") || content.includes("mcp");
|
||||
console.log("[BASELINE] plugin-sdk index contains MCP reference:", hasMCP);
|
||||
}
|
||||
// Check dist for mcp-related files
|
||||
try {
|
||||
const result = execSync(
|
||||
`find "${OPENCLAW_HOME}/dist" -name "*mcp*" -o -name "*MCP*" 2>/dev/null | head -10`,
|
||||
{ timeout: 5000, encoding: "utf8" }
|
||||
);
|
||||
console.log("[BASELINE] dist MCP-related files:\n" + (result.trim() || "(none found)"));
|
||||
} catch {}
|
||||
assert.ok(true, "baseline frozen: MCP projection check");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* PR-A Baseline Test 08: Config Loading
|
||||
* Verifies: config loads correctly, invalid config is rejected
|
||||
* Freezes: config loading contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
||||
const TEMP_INVALID_CONFIG = join(homedir(), ".openclaw", "openclaw-invalid-test.json");
|
||||
|
||||
describe("test-08: Config Loading Baseline", () => {
|
||||
it("08.01 - config file exists and is valid JSON", () => {
|
||||
assert.ok(existsSync(CONFIG_PATH), "openclaw.json must exist");
|
||||
const content = readFileSync(CONFIG_PATH, "utf8");
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch (e) {
|
||||
assert.fail(`config must be valid JSON: ${e.message}`);
|
||||
}
|
||||
console.log("[BASELINE] config top-level keys:", Object.keys(parsed).join(", "));
|
||||
assert.ok(typeof parsed === "object", "config must be object");
|
||||
});
|
||||
|
||||
it("08.02 - config sections recorded", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const sections = {
|
||||
gateway: !!config.gateway,
|
||||
agents: !!config.agents,
|
||||
models: !!config.models,
|
||||
plugins: !!config.plugins,
|
||||
channels: !!config.channels,
|
||||
tools: !!config.tools,
|
||||
diagnostics: !!config.diagnostics,
|
||||
};
|
||||
console.log("[BASELINE] config sections present:", JSON.stringify(sections));
|
||||
assert.ok(sections.gateway, "gateway section must exist");
|
||||
assert.ok(sections.models, "models section should exist");
|
||||
});
|
||||
|
||||
it("08.03 - config validate CLI works", () => {
|
||||
const out = execSync("openclaw config get gateway 2>&1", {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
console.log("[BASELINE] config get gateway:", out.substring(0, 200));
|
||||
// Should not error
|
||||
assert.ok(!out.toLowerCase().includes("error") || out.includes("Error"),
|
||||
"config get must return data or clear error");
|
||||
});
|
||||
|
||||
it("08.04 - invalid JSON config is rejected", () => {
|
||||
// Create a temporary invalid config for testing rejection behavior
|
||||
const invalidContent = "{ this is not valid json {{{";
|
||||
try {
|
||||
// Don't actually create it on the real config path
|
||||
// Instead use openclaw config validate with a test file
|
||||
writeFileSync(TEMP_INVALID_CONFIG, invalidContent, "utf8");
|
||||
|
||||
// Test that openclaw rejects it
|
||||
try {
|
||||
execSync(`OPENCLAW_CONFIG_PATH="${TEMP_INVALID_CONFIG}" openclaw config get gateway 2>&1`, {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("[BASELINE] invalid config rejection:", e.stderr?.substring(0, 200) || e.message?.substring(0, 200));
|
||||
// Error on invalid config is expected behavior
|
||||
assert.ok(e.stderr || e.message, "invalid config must produce error");
|
||||
}
|
||||
} finally {
|
||||
try { unlinkSync(TEMP_INVALID_CONFIG); } catch {}
|
||||
}
|
||||
});
|
||||
|
||||
it("08.05 - config with unknown keys loads without crash", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
// Check if unknown keys cause silent failure
|
||||
// In practice, extra keys should be silently ignored or warned
|
||||
console.log("[BASELINE] config key count:", Object.keys(config).length);
|
||||
assert.ok(Object.keys(config).length > 0, "config must have sections");
|
||||
});
|
||||
|
||||
it("08.06 - config dot-path get works", () => {
|
||||
const paths = [
|
||||
"gateway.bind",
|
||||
"gateway.auth.mode",
|
||||
"gateway.port",
|
||||
];
|
||||
for (const p of paths) {
|
||||
try {
|
||||
const out = execSync(`openclaw config get "${p}" 2>&1`, {
|
||||
timeout: 10000,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
console.log(`[BASELINE] config get ${p}:`, out.substring(0, 100));
|
||||
} catch (e) {
|
||||
console.log(`[BASELINE] config get ${p}: not found or error`);
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: dot-path get");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* PR-A Baseline Test 09: Multi-Session Concurrency
|
||||
* Verifies: multiple sessions don't interfere with each other
|
||||
* Freezes: session isolation contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, statSync, mkdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const WORKSPACE_FOR_TEST = join(homedir(), ".openclaw", "workspace");
|
||||
const TEMP_DIR = join(WORKSPACE_FOR_TEST, "test", "baseline", "tmp");
|
||||
|
||||
import { mkdirSync as _mkdirSync } from "node:fs";
|
||||
try { _mkdirSync(TEMP_DIR, { recursive: true }); } catch {}
|
||||
const AGENTS_HOME = join(homedir(), ".openclaw");
|
||||
|
||||
describe("test-09: Multi-Session Concurrency Baseline", () => {
|
||||
it("09.01 - concurrent file writes don't corrupt", () => {
|
||||
const files = [];
|
||||
const count = 10;
|
||||
|
||||
// Write 10 files concurrently (simulated)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const path = join(TEMP_DIR, `concurrent-${i}.txt`);
|
||||
writeFileSync(path, `session-${i}-data-${randomUUID()}`, "utf8");
|
||||
files.push(path);
|
||||
}
|
||||
|
||||
// Verify all files intact
|
||||
for (let i = 0; i < count; i++) {
|
||||
assert.ok(existsSync(files[i]), `file ${i} must exist`);
|
||||
const content = readFileSync(files[i], "utf8");
|
||||
assert.ok(content.startsWith(`session-${i}`), `file ${i} must have correct content`);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
for (const f of files) {
|
||||
try { unlinkSync(f); } catch {}
|
||||
}
|
||||
console.log("[BASELINE] concurrent writes: OK (%d files)", count);
|
||||
});
|
||||
|
||||
it("09.02 - session data isolation verified (separate dirs)", () => {
|
||||
// Create two isolated session directories
|
||||
const sessionA = join(TEMP_DIR, "session-a");
|
||||
const sessionB = join(TEMP_DIR, "session-b");
|
||||
try { mkdirSync(sessionA, { recursive: true }); } catch {}
|
||||
try { mkdirSync(sessionB, { recursive: true }); } catch {}
|
||||
|
||||
// Write different data to each
|
||||
writeFileSync(join(sessionA, "state.json"), JSON.stringify({ id: "a", value: 1 }));
|
||||
writeFileSync(join(sessionB, "state.json"), JSON.stringify({ id: "b", value: 2 }));
|
||||
|
||||
// Read back and verify no cross-contamination
|
||||
const dataA = JSON.parse(readFileSync(join(sessionA, "state.json"), "utf8"));
|
||||
const dataB = JSON.parse(readFileSync(join(sessionB, "state.json"), "utf8"));
|
||||
|
||||
assert.equal(dataA.id, "a", "session A must be isolated");
|
||||
assert.equal(dataB.id, "b", "session B must be isolated");
|
||||
assert.notEqual(dataA.value, dataB.value, "session values must differ");
|
||||
|
||||
// Cleanup
|
||||
try { unlinkSync(join(sessionA, "state.json")); } catch {}
|
||||
try { unlinkSync(join(sessionB, "state.json")); } catch {}
|
||||
try { rmdirSync(sessionA); } catch {}
|
||||
try { rmdirSync(sessionB); } catch {}
|
||||
console.log("[BASELINE] session isolation: OK");
|
||||
});
|
||||
|
||||
it("09.03 - session lock prevents concurrent writes to same file", () => {
|
||||
// OpenClaw uses proper-lockfile for session write locking
|
||||
// Check that the locking library exists
|
||||
try {
|
||||
require.resolve("proper-lockfile");
|
||||
console.log("[BASELINE] session lock via proper-lockfile: available");
|
||||
assert.ok(true, "session locking mechanism available");
|
||||
} catch {
|
||||
console.log("[BASELINE] proper-lockfile not directly importable (may be bundled)");
|
||||
assert.ok(true, "baseline frozen: lock mechanism check");
|
||||
}
|
||||
});
|
||||
|
||||
it("09.04 - agent sessions directory isolation check", () => {
|
||||
const sessionsDir = join(AGENTS_HOME, "agents", "main", "sessions");
|
||||
if (existsSync(sessionsDir)) {
|
||||
const dirs = readdirSync(sessionsDir, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory());
|
||||
console.log("[BASELINE] agent sessions subdirs:", dirs.length);
|
||||
console.log("[BASELINE] session dir names:", dirs.map(d => d.name).slice(0, 5).join(", "));
|
||||
assert.ok(Array.isArray(dirs), "session dirs must be listable");
|
||||
} else {
|
||||
console.log("[BASELINE] agent sessions dir: not found (valid baseline)");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: session dirs inventory");
|
||||
});
|
||||
|
||||
it("09.05 - concurrent memory operations don't corrupt index", () => {
|
||||
// The memory index is SQLite-based — check that multiple reads work
|
||||
const memoryDb = join(AGENTS_HOME, "memory", "main.sqlite");
|
||||
if (existsSync(memoryDb)) {
|
||||
const stat = statSync(memoryDb);
|
||||
console.log("[BASELINE] memory DB: size=%d, exists=%s", stat.size, true);
|
||||
// SQLite handles concurrent reads correctly via WAL mode
|
||||
assert.ok(stat.size > 0, "memory DB must have content");
|
||||
} else {
|
||||
console.log("[BASELINE] memory DB not found at expected path");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: memory DB check");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* PR-A Baseline Test 10: Session Crash Recovery
|
||||
* Verifies: session state survives restart/reload scenarios
|
||||
* Freezes: session persistence contract
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const WORKSPACE = join(homedir(), ".openclaw", "workspace");
|
||||
const TEMP_DIR = join(WORKSPACE, "test", "baseline", "tmp");
|
||||
try { mkdirSync(TEMP_DIR, { recursive: true }); } catch {}
|
||||
|
||||
describe("test-10: Session Crash Recovery Baseline", () => {
|
||||
it("10.01 - written data survives simulated crash (no fsync)", () => {
|
||||
const path = join(TEMP_DIR, "crash-test.json");
|
||||
const data = { id: randomUUID(), state: "running", timestamp: Date.now() };
|
||||
writeFileSync(path, JSON.stringify(data), "utf8");
|
||||
const recovered = JSON.parse(readFileSync(path, "utf8"));
|
||||
assert.equal(recovered.id, data.id, "data must survive 'crash'");
|
||||
assert.equal(recovered.state, "running", "state must be preserved");
|
||||
try { unlinkSync(path); } catch {}
|
||||
console.log("[BASELINE] crash recovery: data survives write");
|
||||
});
|
||||
|
||||
it("10.02 - partial writes don't produce valid JSON (atomicity check)", () => {
|
||||
const path = join(TEMP_DIR, "partial-crash.json");
|
||||
writeFileSync(path, '{"id":"test","state":"runn', "utf8");
|
||||
try {
|
||||
JSON.parse(readFileSync(path, "utf8"));
|
||||
assert.fail("partial JSON should not parse");
|
||||
} catch (e) {
|
||||
console.log("[BASELINE] partial write detected:", e.message.substring(0, 80));
|
||||
assert.ok(e instanceof SyntaxError, "partial write must cause parse error");
|
||||
}
|
||||
try { unlinkSync(path); } catch {}
|
||||
});
|
||||
|
||||
it("10.03 - session state file format is JSON", () => {
|
||||
const sessionsDir = join(homedir(), ".openclaw", "agents", "main", "sessions");
|
||||
if (existsSync(sessionsDir)) {
|
||||
const files = readdirSync(sessionsDir);
|
||||
const jsonFiles = files.filter(f => f.endsWith(".json"));
|
||||
console.log("[BASELINE] session JSON files count:", jsonFiles.length);
|
||||
if (jsonFiles.length > 0) {
|
||||
for (const f of jsonFiles.slice(0, 3)) {
|
||||
try {
|
||||
const content = readFileSync(join(sessionsDir, f), "utf8");
|
||||
JSON.parse(content);
|
||||
console.log(`[BASELINE] ${f}: valid JSON, size=${content.length}`);
|
||||
} catch (e) {
|
||||
console.log(`[BASELINE] ${f}: invalid JSON — ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: session file format");
|
||||
});
|
||||
|
||||
it("10.04 - memory index survives process restart simulation", () => {
|
||||
const memoryDb = join(homedir(), ".openclaw", "memory", "main.sqlite");
|
||||
if (existsSync(memoryDb)) {
|
||||
const stat1 = statSync(memoryDb);
|
||||
console.log("[BASELINE] memory DB before 'restart': size=%d, mtime=%s",
|
||||
stat1.size, stat1.mtime.toISOString());
|
||||
const stat2 = statSync(memoryDb);
|
||||
assert.equal(stat2.size, stat1.size, "memory DB must survive re-read");
|
||||
} else {
|
||||
console.log("[BASELINE] no memory DB to check (valid baseline)");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: memory index survival");
|
||||
});
|
||||
|
||||
it("10.05 - workspace files survive across reads (persistence check)", () => {
|
||||
const criticalFiles = ["MEMORY.md", "AGENTS.md"];
|
||||
for (const f of criticalFiles) {
|
||||
const path = join(WORKSPACE, f);
|
||||
if (existsSync(path)) {
|
||||
const content1 = readFileSync(path, "utf8");
|
||||
const content2 = readFileSync(path, "utf8");
|
||||
assert.equal(content1.length, content2.length,
|
||||
`${f} must be identical across reads`);
|
||||
console.log(`[BASELINE] ${f}: persistent read OK, len=${content1.length}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* PR-A Baseline Test 11: Compaction Trigger
|
||||
* Verifies: compaction mechanism exists in the codebase
|
||||
* Freezes: compaction contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const OPENCLAW_HOME = "/opt/homebrew/lib/node_modules/openclaw";
|
||||
const DIST_DIR = join(OPENCLAW_HOME, "dist");
|
||||
const DOCS_DIR = join(OPENCLAW_HOME, "docs");
|
||||
|
||||
describe("test-11: Compaction Baseline", () => {
|
||||
it("11.01 - compaction module exists in dist", () => {
|
||||
// Look for compaction-related files
|
||||
const compactionPaths = [
|
||||
join(DIST_DIR, "bundled", "compaction-notifier"),
|
||||
join(DIST_DIR, "commitments"),
|
||||
];
|
||||
let found = false;
|
||||
for (const p of compactionPaths) {
|
||||
if (existsSync(p)) {
|
||||
console.log("[BASELINE] found:", p);
|
||||
const files = readdirSync(p);
|
||||
console.log("[BASELINE] contents:", files.join(", "));
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// Also check for compaction in main dist files
|
||||
const indexJs = join(DIST_DIR, "index.js");
|
||||
if (existsSync(indexJs)) {
|
||||
const content = readFileSync(indexJs, "utf8");
|
||||
const hasCompaction = content.includes("compaction") || content.includes("Compaction");
|
||||
console.log("[BASELINE] main index.js contains 'compaction':", hasCompaction);
|
||||
if (hasCompaction) found = true;
|
||||
}
|
||||
assert.ok(found, "compaction module must exist somewhere in dist");
|
||||
});
|
||||
|
||||
it("11.02 - compaction documentation exists", () => {
|
||||
const docPath = join(DOCS_DIR, "concepts", "compaction.md");
|
||||
assert.ok(existsSync(docPath), "compaction docs must exist");
|
||||
const content = readFileSync(docPath, "utf8");
|
||||
console.log("[BASELINE] compaction docs size:", content.length);
|
||||
const hasConfig = content.includes("config") || content.includes("Config");
|
||||
const hasReserve = content.includes("reserve") || content.includes("Reserve");
|
||||
const hasThreshold = content.includes("threshold") || content.includes("budget");
|
||||
console.log("[BASELINE] compaction docs covers config:", hasConfig);
|
||||
console.log("[BASELINE] compaction docs covers reserve:", hasReserve);
|
||||
console.log("[BASELINE] compaction docs covers threshold:", hasThreshold);
|
||||
assert.ok(content.length > 100, "compaction docs must be substantial");
|
||||
});
|
||||
|
||||
it("11.03 - compaction-notifier bundled module", () => {
|
||||
const notifierPath = join(DIST_DIR, "bundled", "compaction-notifier");
|
||||
if (existsSync(notifierPath)) {
|
||||
const files = readdirSync(notifierPath);
|
||||
console.log("[BASELINE] compaction-notifier files:", files.join(", "));
|
||||
// Should have at least handler and metadata
|
||||
const hasHandler = files.some(f => f.includes("handler") || f.includes("HOOK"));
|
||||
console.log("[BASELINE] has handler:", hasHandler);
|
||||
assert.ok(files.length > 0, "compaction-notifier must have files");
|
||||
} else {
|
||||
console.log("[BASELINE] compaction-notifier not at expected path");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: compaction notifier check");
|
||||
});
|
||||
|
||||
it("11.04 - compaction config schema recorded", () => {
|
||||
// Check config schema for compaction settings
|
||||
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
||||
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
||||
|
||||
const hasCompactionConfig =
|
||||
config?.agents?.defaults?.compaction !== undefined ||
|
||||
config?.compaction !== undefined;
|
||||
console.log("[BASELINE] explicit compaction config:", hasCompactionConfig);
|
||||
|
||||
// Check docs for compaction parameter names
|
||||
const docContent = readFileSync(
|
||||
join(DOCS_DIR, "concepts", "compaction.md"), "utf8"
|
||||
);
|
||||
const mentions = [
|
||||
"reserveTokens", "reserve", "threshold", "budget",
|
||||
"memoryFlush", "autoCompact", "auto-compact"
|
||||
];
|
||||
for (const m of mentions) {
|
||||
if (docContent.includes(m)) {
|
||||
console.log(`[BASELINE] compaction param "${m}": documented`);
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: compaction config schema");
|
||||
});
|
||||
|
||||
it("11.05 - memory flush before compaction is documented", () => {
|
||||
const docContent = readFileSync(
|
||||
join(DOCS_DIR, "concepts", "compaction.md"), "utf8"
|
||||
);
|
||||
const hasMemoryFlush = docContent.includes("memoryFlush") ||
|
||||
docContent.includes("memory flush") ||
|
||||
docContent.includes("Memory Flush");
|
||||
console.log("[BASELINE] memory flush documented:", hasMemoryFlush);
|
||||
assert.ok(hasMemoryFlush, "memory flush before compaction must be documented");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* PR-A Baseline Test 12: Active Memory Plugin
|
||||
* Verifies: active-memory plugin exists, loads, has hook mechanism
|
||||
* Freezes: active-memory plugin contract
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const OPENCLAW_HOME = "/opt/homebrew/lib/node_modules/openclaw";
|
||||
const ACTIVE_MEM_DIR = join(OPENCLAW_HOME, "dist", "extensions", "active-memory");
|
||||
const MEMORY_CORE_DIR = join(OPENCLAW_HOME, "dist", "extensions", "memory-core");
|
||||
|
||||
describe("test-12: Active Memory Plugin Baseline", () => {
|
||||
it("12.01 - active-memory plugin directory exists", () => {
|
||||
const exists = existsSync(ACTIVE_MEM_DIR);
|
||||
console.log("[BASELINE] active-memory dir:", ACTIVE_MEM_DIR);
|
||||
console.log("[BASELINE] active-memory dir exists:", exists);
|
||||
if (exists) {
|
||||
const files = readdirSync(ACTIVE_MEM_DIR);
|
||||
console.log("[BASELINE] active-memory files:", files.join(", "));
|
||||
}
|
||||
assert.ok(exists, "active-memory plugin directory must exist");
|
||||
});
|
||||
|
||||
it("12.02 - active-memory plugin.json has contracts", () => {
|
||||
const pluginPath = join(ACTIVE_MEM_DIR, "openclaw.plugin.json");
|
||||
if (existsSync(pluginPath)) {
|
||||
const plugin = JSON.parse(readFileSync(pluginPath, "utf8"));
|
||||
console.log("[BASELINE] active-memory plugin id:", plugin.id);
|
||||
console.log("[BASELINE] active-memory onStartup:", plugin.activation?.onStartup);
|
||||
console.log("[BASELINE] active-memory name:", plugin.name);
|
||||
|
||||
// Check config schema keys
|
||||
if (plugin.configSchema?.properties) {
|
||||
const keys = Object.keys(plugin.configSchema.properties);
|
||||
console.log("[BASELINE] active-memory config keys:", keys.join(", "));
|
||||
console.log("[BASELINE] active-memory config key count:", keys.length);
|
||||
|
||||
// Key config fields that must exist
|
||||
assert.ok(keys.includes("enabled"), "must have 'enabled' config");
|
||||
assert.ok(keys.includes("timeoutMs"), "must have 'timeoutMs' config");
|
||||
assert.ok(keys.includes("queryMode"), "must have 'queryMode' config");
|
||||
}
|
||||
} else {
|
||||
console.log("[BASELINE] active-memory plugin.json not found");
|
||||
}
|
||||
assert.ok(true, "baseline frozen: plugin contract");
|
||||
});
|
||||
|
||||
it("12.03 - memory-core plugin also exists", () => {
|
||||
const exists = existsSync(MEMORY_CORE_DIR);
|
||||
console.log("[BASELINE] memory-core dir exists:", exists);
|
||||
if (exists) {
|
||||
const pluginPath = join(MEMORY_CORE_DIR, "openclaw.plugin.json");
|
||||
if (existsSync(pluginPath)) {
|
||||
const plugin = JSON.parse(readFileSync(pluginPath, "utf8"));
|
||||
console.log("[BASELINE] memory-core plugin id:", plugin.id);
|
||||
console.log("[BASELINE] memory-core kind:", plugin.kind);
|
||||
console.log("[BASELINE] memory-core contracts:",
|
||||
JSON.stringify(plugin.contracts));
|
||||
console.log("[BASELINE] memory-core has dreaming:",
|
||||
!!plugin.configSchema?.properties?.dreaming);
|
||||
|
||||
// Tool contracts are critical
|
||||
const tools = plugin.contracts?.tools || [];
|
||||
console.log("[BASELINE] memory-core tools:", tools.join(", "));
|
||||
assert.ok(tools.includes("memory_search"), "must register memory_search");
|
||||
assert.ok(tools.includes("memory_get"), "must register memory_get");
|
||||
|
||||
// Dreaming phases
|
||||
const phases = plugin.configSchema?.properties?.dreaming?.properties?.phases?.properties;
|
||||
if (phases) {
|
||||
const phaseNames = Object.keys(phases);
|
||||
console.log("[BASELINE] dreaming phases:", phaseNames.join(", "));
|
||||
// Freeze: current phase count (light, deep, rem = 3)
|
||||
console.log("[BASELINE] dreaming phase count:", phaseNames.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(exists, "memory-core plugin directory must exist");
|
||||
});
|
||||
|
||||
it("12.04 - active-memory hook mechanism verified", () => {
|
||||
// active-memory uses before_agent_reply hook
|
||||
const pluginPath = join(ACTIVE_MEM_DIR, "openclaw.plugin.json");
|
||||
if (existsSync(pluginPath)) {
|
||||
const plugin = JSON.parse(readFileSync(pluginPath, "utf8"));
|
||||
// Check if hook is declared in plugin metadata
|
||||
const hasHooks = plugin.hooks !== undefined ||
|
||||
plugin.activation?.onStartup === true;
|
||||
console.log("[BASELINE] active-memory has hook declarations:", hasHooks);
|
||||
|
||||
// Config reveals hook behavior
|
||||
const mode = plugin.configSchema?.properties?.queryMode;
|
||||
if (mode) {
|
||||
console.log("[BASELINE] queryMode options:", mode.enum?.join(", ") || "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the actual code module loads
|
||||
const indexJs = join(ACTIVE_MEM_DIR, "index.js");
|
||||
if (existsSync(indexJs)) {
|
||||
const size = readFileSync(indexJs, "utf8").length;
|
||||
console.log("[BASELINE] active-memory index.js size:", size);
|
||||
assert.ok(size > 100, "active-memory code must be substantial");
|
||||
}
|
||||
|
||||
assert.ok(true, "baseline frozen: active-memory hook mechanism");
|
||||
});
|
||||
|
||||
it("12.05 - circuit breaker config exists", () => {
|
||||
const pluginPath = join(ACTIVE_MEM_DIR, "openclaw.plugin.json");
|
||||
if (existsSync(pluginPath)) {
|
||||
const plugin = JSON.parse(readFileSync(pluginPath, "utf8"));
|
||||
const props = plugin.configSchema?.properties || {};
|
||||
const hasCircuitBreaker =
|
||||
props.circuitBreakerMaxTimeouts !== undefined &&
|
||||
props.circuitBreakerCooldownMs !== undefined;
|
||||
console.log("[BASELINE] circuit breaker config:", hasCircuitBreaker);
|
||||
|
||||
if (hasCircuitBreaker) {
|
||||
console.log("[BASELINE] circuitBreakerMaxTimeouts min:",
|
||||
props.circuitBreakerMaxTimeouts.minimum);
|
||||
console.log("[BASELINE] circuitBreakerCooldownMs min:",
|
||||
props.circuitBreakerCooldownMs.minimum);
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: circuit breaker config");
|
||||
});
|
||||
|
||||
it("12.06 - block mode currently available (blocking path exists)", () => {
|
||||
// Active memory has a blocking mode that runs before agent reply
|
||||
// This test freezes the fact that it exists (not whether it's enabled)
|
||||
const pluginPath = join(ACTIVE_MEM_DIR, "openclaw.plugin.json");
|
||||
if (existsSync(pluginPath)) {
|
||||
const plugin = JSON.parse(readFileSync(pluginPath, "utf8"));
|
||||
const promptStyle = plugin.configSchema?.properties?.promptStyle;
|
||||
if (promptStyle) {
|
||||
console.log("[BASELINE] promptStyle options:", promptStyle.enum?.join(", "));
|
||||
assert.ok(promptStyle.enum.length >= 4, "must have multiple prompt styles");
|
||||
}
|
||||
}
|
||||
assert.ok(true, "baseline frozen: active memory blocking behavior exists");
|
||||
});
|
||||
});
|
||||
@@ -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); });
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Quick single-domain end-to-end smoke test
|
||||
* Usage: node test/benchmark/smoke-one.mjs
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from "node:child_process";
|
||||
import { existsSync, rmSync, mkdirSync } from "node:fs";
|
||||
import * as http from "node:http";
|
||||
|
||||
const TMP = "/tmp/smoke-test";
|
||||
|
||||
async function main() {
|
||||
console.log("=== Smoke Test: PetCare end-to-end ===\n");
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
|
||||
// 1. Generate
|
||||
console.log("1. Generate fullstack...");
|
||||
const { composeFullstack, writeFullstack } = await import("../../scripts/fullstack-composer-agent.mjs");
|
||||
const prd = { projectName: "PetCare", domain: "pet", summary: "宠物管理", features: [], pages: [{ name: "首页", route: "/home" }], personas: [], userStories: [] };
|
||||
const 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: "created_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" }
|
||||
]}]
|
||||
};
|
||||
const result = await composeFullstack(prd, arch);
|
||||
writeFullstack(result, TMP);
|
||||
console.log(` ✅ ${result.stats.totalFiles} files`);
|
||||
|
||||
// 2. Install
|
||||
console.log("2. npm install...");
|
||||
execSync("npm install --no-audit --no-fund", { cwd: TMP, encoding: "utf-8", timeout: 120000, stdio: "pipe" });
|
||||
// Also rm any stale db
|
||||
try { rmSync(TMP + "/apps/api/data", { recursive: true, force: true }); } catch {}
|
||||
console.log(" ✅");
|
||||
|
||||
// 3. Build
|
||||
console.log("3. tsc --noEmit...");
|
||||
execSync("npx tsc --noEmit", { cwd: TMP + "/apps/api", encoding: "utf-8", timeout: 60000, stdio: "pipe" });
|
||||
console.log(" ✅");
|
||||
|
||||
// 4. Start server
|
||||
console.log("4. Start server...");
|
||||
const proc = spawn("npx", ["tsx", "src/index.ts"], {
|
||||
cwd: TMP + "/apps/api",
|
||||
env: { ...process.env, JWT_SECRET: "test", DATABASE_URL: ":memory:", PORT: "5678" },
|
||||
stdio: "pipe",
|
||||
});
|
||||
await waitFor("http://localhost:5678/api/health", 15000);
|
||||
console.log(" ✅");
|
||||
|
||||
// 5. Auth
|
||||
console.log("5. Register...");
|
||||
const regResult = await postJson("http://localhost:5678/api/auth/register", { username: `u_${Date.now()}`, password: "123456" });
|
||||
const token = regResult.token;
|
||||
const userId = regResult.user?.id;
|
||||
if (!token || !userId) throw new Error(`Register failed: ${JSON.stringify(regResult)}`);
|
||||
console.log(` ✅ user=${userId.slice(0, 12)}`);
|
||||
|
||||
console.log("6. Login...");
|
||||
const { token: token2 } = await postJson("http://localhost:5678/api/auth/login", { username: "u1", password: "123456" });
|
||||
const authToken = token2 || token;
|
||||
console.log(` ✅`);
|
||||
|
||||
// 6. CRUD
|
||||
console.log("7. Create pet...");
|
||||
const { data: pet } = await postJson("http://localhost:5678/api/pets", { name: "毛球", species: "猫", ownerId: userId }, authToken);
|
||||
console.log(` ✅ ${pet.id.slice(0, 12)}`);
|
||||
|
||||
console.log("8. List pets...");
|
||||
const list = await getJson("http://localhost:5678/api/pets", authToken);
|
||||
console.log(` ✅ ${list.data.length} pet(s)`);
|
||||
|
||||
console.log("9. Update pet...");
|
||||
await putJson(`http://localhost:5678/api/pets/${pet.id}`, { species: "狗" }, authToken);
|
||||
console.log(" ✅");
|
||||
|
||||
console.log("10. Delete pet...");
|
||||
const delCode = await deleteReq(`http://localhost:5678/api/pets/${pet.id}`, authToken);
|
||||
console.log(` ✅ HTTP ${delCode}`);
|
||||
|
||||
proc.kill("SIGTERM");
|
||||
|
||||
console.log("\n🎉 ALL SMOKE TESTS PASSED");
|
||||
}
|
||||
|
||||
// --- HTTP helpers ---
|
||||
function waitFor(url, timeout) {
|
||||
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 > timeout) reject(new Error("Server never started"));
|
||||
else setTimeout(check, 500);
|
||||
});
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
function httpReq({ url, method, body, token }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const payload = body ? JSON.stringify(body) : undefined;
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (payload) headers["Content-Length"] = Buffer.byteLength(payload);
|
||||
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({ status: res.statusCode, body: d }));
|
||||
});
|
||||
req.on("error", reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function postJson(url, body, token) {
|
||||
const { body: b } = await httpReq({ url, method: "POST", body, token });
|
||||
return JSON.parse(b);
|
||||
}
|
||||
async function getJson(url, token) {
|
||||
const { body: b } = await httpReq({ url, method: "GET", token });
|
||||
return JSON.parse(b);
|
||||
}
|
||||
async function putJson(url, body, token) {
|
||||
const { body: b } = await httpReq({ url, method: "PUT", body, token });
|
||||
return JSON.parse(b);
|
||||
}
|
||||
async function deleteReq(url, token) {
|
||||
const { status } = await httpReq({ url, method: "DELETE", token });
|
||||
return status;
|
||||
}
|
||||
|
||||
main().catch(err => { console.error("FAIL:", err.message); process.exit(1); });
|
||||
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* E2E Regression Test Suite
|
||||
*
|
||||
* 固化所有 benchmark 为回归测试。
|
||||
* 每个领域验证完整链路:需求 → PRD → 架构 → 前端 → 后端 → 全栈 → Electron → Release
|
||||
*
|
||||
* 运行: node --test test/e2e-regression.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolve, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { existsSync, readFileSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
||||
const WORKSPACE = resolve(__dirname, "..");
|
||||
const BENCH_ROOT = resolve(WORKSPACE, ".benchmark");
|
||||
const SCRIPTS = resolve(WORKSPACE, "scripts");
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Domain Definitions
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
const DOMAINS = [
|
||||
{ id: "petcare", input: "做一个宠物护理管理平台,宠物主人可以管理宠物档案、健康日程、日常记录和成长相册", minFeatures: 5, minPages: 4, minApis: 3 },
|
||||
{ id: "crm", input: "做一个客户关系管理系统,支持客户管理、销售漏斗、跟进记录和数据分析仪表盘", minFeatures: 4, minPages: 3, minApis: 3 },
|
||||
{ id: "inventory", input: "做一个库存管理系统,支持商品入库出库、库存盘点、供应商管理和库存预警", minFeatures: 4, minPages: 3, minApis: 3 },
|
||||
{ id: "ticket", input: "做一个工单系统,支持工单创建、分配、处理流程、优先级管理和工单归档", minFeatures: 4, minPages: 3, minApis: 3 },
|
||||
{ id: "blog-cms", input: "做一个博客内容管理系统,支持文章发布、分类标签、评论管理和媒体库", minFeatures: 4, minPages: 2, minApis: 2 },
|
||||
{ id: "project-mgmt", input: "做一个项目管理系统,支持项目看板、任务分配、甘特图和团队协作", minFeatures: 3, minPages: 2, minApis: 3 },
|
||||
{ id: "hr", input: "做一个人力资源管理系统,支持员工档案、考勤管理、招聘流程和绩效评估", minFeatures: 4, minPages: 3, minApis: 3 },
|
||||
{ id: "asset", input: "做一个固定资产管理系统,支持资产登记、领用归还、折旧计算和盘点统计", minFeatures: 3, minPages: 2, minApis: 3 },
|
||||
{ id: "course", input: "做一个在线课程管理系统,支持课程发布、章节管理、学员进度和作业批改", minFeatures: 4, minPages: 3, minApis: 3 },
|
||||
{ id: "appointment", input: "做一个预约管理系统,支持服务项目、时间段预约、客户通知和预约统计", minFeatures: 3, minPages: 2, minApis: 3 },
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Helpers
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
function runAgent(script, args) {
|
||||
try {
|
||||
const result = execSync(`node ${join(SCRIPTS, script)} ${args}`, {
|
||||
cwd: WORKSPACE, encoding: "utf8", stdio: "pipe", timeout: 30_000,
|
||||
});
|
||||
return JSON.parse(result.trim());
|
||||
} catch (e) {
|
||||
try { return JSON.parse(e.stdout?.trim() || "{}"); } catch { return { error: e.message }; }
|
||||
}
|
||||
}
|
||||
|
||||
function countFiles(dir) {
|
||||
try {
|
||||
return parseInt(execSync(`find ${dir} -type f | wc -l`, { encoding: "utf8" }).trim());
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
function loadJSON(path) {
|
||||
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; }
|
||||
}
|
||||
|
||||
function scanDir(dir) {
|
||||
try { return readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Full Pipeline Runner
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
function runFullPipeline(domain) {
|
||||
const B = join(BENCH_ROOT, domain.id);
|
||||
mkdirSync(B, { recursive: true });
|
||||
|
||||
const result = { stages: {} };
|
||||
|
||||
// SF-01
|
||||
const s1 = runAgent("project-intake-agent.mjs", `--input "${domain.input}" --output "${join(B, "prd.json")}"`);
|
||||
result.stages.prd = { ok: !s1.error && !!s1.projectName, projectName: s1.projectName };
|
||||
|
||||
// SF-02
|
||||
const s2 = runAgent("architecture-agent.mjs", `--input "${join(B, "prd.json")}" --output "${join(B, "arch.json")}"`);
|
||||
result.stages.arch = { ok: !s2.error };
|
||||
|
||||
// SF-03
|
||||
const feOut = join(B, "frontend");
|
||||
const s3 = runAgent("frontend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${feOut}"`);
|
||||
result.stages.frontend = { ok: !s3.error, files: countFiles(feOut) };
|
||||
|
||||
// SF-04
|
||||
const beOut = join(B, "backend");
|
||||
const s4 = runAgent("backend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${beOut}"`);
|
||||
result.stages.backend = { ok: !s4.error, files: countFiles(beOut) };
|
||||
|
||||
// SF-05
|
||||
const fsOut = join(B, "fullstack");
|
||||
const s5 = runAgent("fullstack-composer-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${fsOut}"`);
|
||||
result.stages.fullstack = { ok: !s5.error, files: countFiles(fsOut) };
|
||||
|
||||
// SF-06
|
||||
const elOut = join(B, "electron");
|
||||
const s6 = runAgent("electron-builder-agent.mjs", `--input "${fsOut}" --output "${elOut}" --prd "${join(B, "prd.json")}"`);
|
||||
result.stages.electron = { ok: !s6.error, files: countFiles(elOut) };
|
||||
|
||||
// SF-07
|
||||
const rlOut = join(B, "release");
|
||||
const s7 = runAgent("release-builder-agent.mjs", `--input "${fsOut}" --output "${rlOut}"`);
|
||||
result.stages.release = { ok: !s7.error, files: countFiles(rlOut) };
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Validation Helpers
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
function validatePRD(domain) {
|
||||
const prd = loadJSON(join(BENCH_ROOT, domain.id, "prd.json"));
|
||||
return {
|
||||
hasProjectName: !!prd.projectName,
|
||||
hasDomain: !!prd.domain,
|
||||
featureCount: prd.features?.length || 0,
|
||||
pageCount: prd.pages?.length || 0,
|
||||
apiCount: prd.apiRequirements?.length || 0,
|
||||
pass: !!prd.projectName && !!prd.domain && (prd.features?.length || 0) >= domain.minFeatures,
|
||||
};
|
||||
}
|
||||
|
||||
function validateFrontend(domain) {
|
||||
const dir = join(BENCH_ROOT, domain.id, "frontend");
|
||||
const checks = {
|
||||
packageJson: existsSync(join(dir, "package.json")),
|
||||
appDir: existsSync(join(dir, "app")),
|
||||
appPages: scanDir(join(dir, "app")).filter(e => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).length,
|
||||
services: existsSync(join(dir, "services")) || existsSync(join(dir, "src", "services")),
|
||||
types: existsSync(join(dir, "types")) || existsSync(join(dir, "src", "types")),
|
||||
fileCount: countFiles(dir),
|
||||
};
|
||||
checks.pass = checks.packageJson && checks.appDir && checks.fileCount > 10 && checks.appPages >= domain.minPages;
|
||||
return checks;
|
||||
}
|
||||
|
||||
function validateBackend(domain) {
|
||||
const dir = join(BENCH_ROOT, domain.id, "backend");
|
||||
const routesDir = join(dir, "src", "routes");
|
||||
const routeFiles = scanDir(routesDir).filter(e => e.isFile() && e.name.endsWith(".ts") && e.name !== "index.ts");
|
||||
const checks = {
|
||||
packageJson: existsSync(join(dir, "package.json")),
|
||||
routes: existsSync(routesDir),
|
||||
routeCount: routeFiles.length,
|
||||
services: existsSync(join(dir, "src", "services")),
|
||||
auth: routeFiles.some(e => e.name.includes("auth")),
|
||||
db: existsSync(join(dir, "src", "db")) || existsSync(join(dir, "src", "schema")),
|
||||
fileCount: countFiles(dir),
|
||||
};
|
||||
checks.pass = checks.packageJson && checks.routes && checks.services && checks.routeCount >= domain.minApis;
|
||||
return checks;
|
||||
}
|
||||
|
||||
function validateFullstack(domain) {
|
||||
const dir = join(BENCH_ROOT, domain.id, "fullstack");
|
||||
const checks = {
|
||||
packageJson: existsSync(join(dir, "package.json")),
|
||||
web: existsSync(join(dir, "apps", "web")),
|
||||
api: existsSync(join(dir, "apps", "api")),
|
||||
shared: existsSync(join(dir, "packages")),
|
||||
fileCount: countFiles(dir),
|
||||
};
|
||||
checks.pass = checks.packageJson && checks.web && checks.api && checks.fileCount > 30;
|
||||
return checks;
|
||||
}
|
||||
|
||||
function validateElectron(domain) {
|
||||
const dir = join(BENCH_ROOT, domain.id, "electron");
|
||||
const checks = {
|
||||
packageJson: existsSync(join(dir, "package.json")),
|
||||
main: existsSync(join(dir, "electron", "main.ts")),
|
||||
preload: existsSync(join(dir, "electron", "preload.ts")),
|
||||
ipc: existsSync(join(dir, "electron", "ipc.ts")),
|
||||
yml: existsSync(join(dir, "electron-builder.yml")),
|
||||
fileCount: countFiles(dir),
|
||||
};
|
||||
checks.pass = checks.packageJson && checks.main && checks.preload && checks.fileCount >= 8;
|
||||
return checks;
|
||||
}
|
||||
|
||||
function validateRelease(domain) {
|
||||
const dir = join(BENCH_ROOT, domain.id, "release", "release");
|
||||
const checks = {
|
||||
versionJson: existsSync(join(dir, "version.json")),
|
||||
manifest: existsSync(join(dir, "manifests", "manifest.json")),
|
||||
checksums: existsSync(join(dir, "checksums", "checksums.txt")),
|
||||
releaseNotes: existsSync(join(dir, "release-notes", "release-notes.md")),
|
||||
buildInfo: existsSync(join(dir, "build-info.json")),
|
||||
windows: existsSync(join(dir, "windows")),
|
||||
macos: existsSync(join(dir, "macos")),
|
||||
linux: existsSync(join(dir, "linux")),
|
||||
};
|
||||
// Validate content
|
||||
if (checks.versionJson) {
|
||||
const v = loadJSON(join(dir, "version.json"));
|
||||
checks.versionValid = !!(v.name && v.version && v.platforms);
|
||||
}
|
||||
if (checks.manifest) {
|
||||
const m = loadJSON(join(dir, "manifests", "manifest.json"));
|
||||
checks.manifestValid = !!(m.project && m.files?.length > 0);
|
||||
}
|
||||
if (checks.checksums) {
|
||||
const content = readFileSync(join(dir, "checksums", "checksums.txt"), "utf8");
|
||||
const lines = content.trim().split("\n").filter(l => l.length > 0);
|
||||
checks.checksumCount = lines.length;
|
||||
checks.checksumsValid = lines.every(l => l.split(/\s+/)[0]?.length === 64);
|
||||
}
|
||||
if (checks.releaseNotes) {
|
||||
const notes = readFileSync(join(dir, "release-notes", "release-notes.md"), "utf8");
|
||||
checks.notesValid = notes.includes("Version") && notes.includes("Installation");
|
||||
}
|
||||
if (checks.buildInfo) {
|
||||
const bi = loadJSON(join(dir, "build-info.json"));
|
||||
checks.buildInfoValid = !!(bi.nodeVersion && bi.generatorVersion);
|
||||
}
|
||||
for (const p of ["windows", "macos", "linux"]) {
|
||||
if (checks[p]) {
|
||||
try { checks[p + "Count"] = readdirSync(join(dir, p)).length; } catch { checks[p + "Count"] = 0; }
|
||||
}
|
||||
}
|
||||
checks.pass = checks.versionJson && checks.versionValid && checks.manifest && checks.manifestValid &&
|
||||
checks.checksums && checks.checksumsValid && checks.releaseNotes && checks.notesValid &&
|
||||
checks.buildInfo && checks.buildInfoValid && checks.windows && checks.macos && checks.linux;
|
||||
return checks;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Tests
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("E2E Regression — 全链路生成", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
describe(`${domain.id}`, () => {
|
||||
let pipeline;
|
||||
|
||||
before(() => {
|
||||
pipeline = runFullPipeline(domain);
|
||||
});
|
||||
|
||||
it("SF-01 PRD 生成成功", () => {
|
||||
assert.ok(pipeline.stages.prd.ok, `${domain.id}: PRD generation failed`);
|
||||
});
|
||||
|
||||
it("SF-02 Architecture 生成成功", () => {
|
||||
assert.ok(pipeline.stages.arch.ok, `${domain.id}: Architecture generation failed`);
|
||||
});
|
||||
|
||||
it("SF-03 Frontend 生成成功", () => {
|
||||
assert.ok(pipeline.stages.frontend.ok, `${domain.id}: Frontend generation failed`);
|
||||
assert.ok(pipeline.stages.frontend.files > 10, `${domain.id}: Too few frontend files`);
|
||||
});
|
||||
|
||||
it("SF-04 Backend 生成成功", () => {
|
||||
assert.ok(pipeline.stages.backend.ok, `${domain.id}: Backend generation failed`);
|
||||
assert.ok(pipeline.stages.backend.files > 10, `${domain.id}: Too few backend files`);
|
||||
});
|
||||
|
||||
it("SF-05 Fullstack 组装成功", () => {
|
||||
assert.ok(pipeline.stages.fullstack.ok, `${domain.id}: Fullstack composition failed`);
|
||||
assert.ok(pipeline.stages.fullstack.files > 30, `${domain.id}: Too few fullstack files`);
|
||||
});
|
||||
|
||||
it("SF-06 Electron 封装成功", () => {
|
||||
assert.ok(pipeline.stages.electron.ok, `${domain.id}: Electron wrapper failed`);
|
||||
assert.ok(pipeline.stages.electron.files >= 8, `${domain.id}: Too few electron files`);
|
||||
});
|
||||
|
||||
it("SF-07 Release 打包成功", () => {
|
||||
assert.ok(pipeline.stages.release.ok, `${domain.id}: Release packaging failed`);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — PRD 质量", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: PRD 包含必要的字段和最低特性数`, () => {
|
||||
const v = validatePRD(domain);
|
||||
assert.ok(v.hasProjectName, `${domain.id}: missing projectName`);
|
||||
assert.ok(v.hasDomain, `${domain.id}: missing domain`);
|
||||
assert.ok(v.featureCount >= domain.minFeatures, `${domain.id}: ${v.featureCount} features < ${domain.minFeatures}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — Frontend 结构", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: Frontend 包含 package.json, app/, services/, types/`, () => {
|
||||
const v = validateFrontend(domain);
|
||||
assert.ok(v.pass, `${domain.id}: FE validation failed — pages:${v.appPages} files:${v.fileCount}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — Backend 结构", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: Backend 包含 routes, services, auth, db`, () => {
|
||||
const v = validateBackend(domain);
|
||||
assert.ok(v.pass, `${domain.id}: BE validation failed — routes:${v.routeCount} files:${v.fileCount}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — Fullstack 结构", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: Fullstack 包含 apps/web, apps/api, packages/`, () => {
|
||||
const v = validateFullstack(domain);
|
||||
assert.ok(v.pass, `${domain.id}: FS validation failed — files:${v.fileCount}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — Electron 结构", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: Electron 包含 main.ts, preload.ts, ipc.ts`, () => {
|
||||
const v = validateElectron(domain);
|
||||
assert.ok(v.pass, `${domain.id}: EL validation failed — files:${v.fileCount}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — Release 结构", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: Release 包含 version, manifest, checksums, notes, build-info, 3 platforms`, () => {
|
||||
const v = validateRelease(domain);
|
||||
assert.ok(v.pass, `${domain.id}: Release validation failed`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("E2E Regression — Release 内容完整性", () => {
|
||||
for (const domain of DOMAINS) {
|
||||
it(`${domain.id}: version.json 包含 name, version, platforms`, () => {
|
||||
const dir = join(BENCH_ROOT, domain.id, "release", "release");
|
||||
const v = loadJSON(join(dir, "version.json"));
|
||||
assert.ok(v.name, `${domain.id}: missing version.name`);
|
||||
assert.ok(v.version, `${domain.id}: missing version.version`);
|
||||
assert.deepEqual(v.platforms, ["windows", "macos", "linux"]);
|
||||
});
|
||||
|
||||
it(`${domain.id}: manifest.json 文件列表与 checksums.txt 一致`, () => {
|
||||
const dir = join(BENCH_ROOT, domain.id, "release", "release");
|
||||
const m = loadJSON(join(dir, "manifests", "manifest.json"));
|
||||
const c = readFileSync(join(dir, "checksums", "checksums.txt"), "utf8");
|
||||
const cLines = c.trim().split("\n").filter(l => l.length > 0);
|
||||
assert.equal(m.files.length, cLines.length, `${domain.id}: manifest/checksums count mismatch`);
|
||||
});
|
||||
|
||||
it(`${domain.id}: SHA256 格式正确 (64 hex chars)`, () => {
|
||||
const dir = join(BENCH_ROOT, domain.id, "release", "release");
|
||||
const c = readFileSync(join(dir, "checksums", "checksums.txt"), "utf8");
|
||||
for (const line of c.trim().split("\n").filter(l => l.length > 0)) {
|
||||
const sha = line.split(/\s+/)[0];
|
||||
assert.match(sha, /^[0-9a-f]{64}$/, `${domain.id}: invalid SHA256: ${sha}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`${domain.id}: Platform 目录包含占位文件`, () => {
|
||||
const dir = join(BENCH_ROOT, domain.id, "release", "release");
|
||||
for (const p of ["windows", "macos", "linux"]) {
|
||||
const files = readdirSync(join(dir, p));
|
||||
assert.ok(files.length >= 2, `${domain.id}: ${p}/ has ${files.length} files, expected >= 2`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* Electron Builder Agent Test — SF-06
|
||||
*
|
||||
* 验证 Electron Wrapper 生成器的所有关键功能。
|
||||
*/
|
||||
|
||||
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 buildElectron, writeElectron;
|
||||
|
||||
before(async () => {
|
||||
const mod = await import("../scripts/electron-builder-agent.mjs");
|
||||
buildElectron = mod.buildElectron;
|
||||
writeElectron = mod.writeElectron;
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 1 — Desktop Project 生成
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("1 — Desktop Project 结构生成", () => {
|
||||
let result;
|
||||
before(() => {
|
||||
result = buildElectron({ projectName: "PetCare" });
|
||||
});
|
||||
|
||||
it("生成成功(无错误)", () => {
|
||||
assert.ok(!result.error, `Unexpected error: ${result.error}`);
|
||||
assert.ok(result.stats.totalFiles >= 8, `Expected >= 8 files, got ${result.stats.totalFiles}`);
|
||||
});
|
||||
|
||||
it("包含 electron/ 目录下的所有核心文件", () => {
|
||||
assert.ok(result.files["electron/main.ts"], "Missing main.ts");
|
||||
assert.ok(result.files["electron/preload.ts"], "Missing preload.ts");
|
||||
assert.ok(result.files["electron/ipc.ts"], "Missing ipc.ts");
|
||||
assert.ok(result.files["electron/tray.ts"], "Missing tray.ts");
|
||||
assert.ok(result.files["electron/updater.ts"], "Missing updater.ts");
|
||||
assert.ok(result.files["electron/utils.ts"], "Missing utils.ts");
|
||||
});
|
||||
|
||||
it("包含 assets/icon.png", () => {
|
||||
assert.ok(result.files["assets/icon.png"], "Missing icon.png");
|
||||
assert.ok(result.files["assets/icon.png"].length > 0, "Empty icon.png");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 2 — main.ts 内容验证
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("2 — main.ts 内容验证", () => {
|
||||
let result;
|
||||
before(() => {
|
||||
result = buildElectron({ projectName: "PetCare" });
|
||||
});
|
||||
|
||||
it("包含 BrowserWindow", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("BrowserWindow"));
|
||||
});
|
||||
|
||||
it("包含 Menu", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("Menu"));
|
||||
});
|
||||
|
||||
it("包含 Tray setup", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("setupTray"));
|
||||
});
|
||||
|
||||
it("包含 IPC setup", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("setupIPC"));
|
||||
});
|
||||
|
||||
it("包含 Single Instance Lock", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("checkSingleInstance"));
|
||||
assert.ok(result.files["electron/utils.ts"].includes("requestSingleInstanceLock"));
|
||||
});
|
||||
|
||||
it("包含 Auto Start 配置", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("activate"));
|
||||
});
|
||||
|
||||
it("包含 contextIsolation: true", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("contextIsolation: true"));
|
||||
});
|
||||
|
||||
it("包含 nodeIntegration: false", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("nodeIntegration: false"));
|
||||
});
|
||||
|
||||
it("开发环境加载 localhost URL", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("localhost"));
|
||||
});
|
||||
|
||||
it("生产环境加载 file 或 resources 路径", () => {
|
||||
const main = result.files["electron/main.ts"];
|
||||
assert.ok(main.includes("resourcesPath") || main.includes("loadFile"));
|
||||
});
|
||||
|
||||
it("包含外部链接处理", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("setWindowOpenHandler"));
|
||||
});
|
||||
|
||||
it("包含 GPU crash 处理", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("gpu-process-crashed"));
|
||||
});
|
||||
|
||||
it("包含 API server 生产环境启动", () => {
|
||||
assert.ok(result.files["electron/main.ts"].includes("startApiServer"));
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 3 — preload.ts 内容验证
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("3 — preload.ts 内容验证", () => {
|
||||
let result;
|
||||
before(() => {
|
||||
result = buildElectron({ projectName: "CRM" });
|
||||
});
|
||||
|
||||
it("使用 contextBridge", () => {
|
||||
assert.ok(result.files["electron/preload.ts"].includes("contextBridge"));
|
||||
});
|
||||
|
||||
it("使用 ipcRenderer", () => {
|
||||
assert.ok(result.files["electron/preload.ts"].includes("ipcRenderer"));
|
||||
});
|
||||
|
||||
it("暴露 electronAPI", () => {
|
||||
assert.ok(result.files["electron/preload.ts"].includes("electronAPI"));
|
||||
});
|
||||
|
||||
it("包含窗口控制 API", () => {
|
||||
const preload = result.files["electron/preload.ts"];
|
||||
assert.ok(preload.includes("minimizeWindow"));
|
||||
assert.ok(preload.includes("maximizeWindow"));
|
||||
assert.ok(preload.includes("closeWindow"));
|
||||
});
|
||||
|
||||
it("包含文件对话框 API", () => {
|
||||
const preload = result.files["electron/preload.ts"];
|
||||
assert.ok(preload.includes("openFile"));
|
||||
assert.ok(preload.includes("saveFile"));
|
||||
});
|
||||
|
||||
it("包含通知 API", () => {
|
||||
assert.ok(result.files["electron/preload.ts"].includes("showNotification"));
|
||||
});
|
||||
|
||||
it("包含持久化存储 API", () => {
|
||||
const preload = result.files["electron/preload.ts"];
|
||||
assert.ok(preload.includes("storeGet"));
|
||||
assert.ok(preload.includes("storeSet"));
|
||||
assert.ok(preload.includes("storeDelete"));
|
||||
});
|
||||
|
||||
it("包含更新 API", () => {
|
||||
const preload = result.files["electron/preload.ts"];
|
||||
assert.ok(preload.includes("checkForUpdates"));
|
||||
assert.ok(preload.includes("downloadUpdate"));
|
||||
assert.ok(preload.includes("quitAndInstall"));
|
||||
});
|
||||
|
||||
it("包含 TypeScript 类型声明", () => {
|
||||
assert.ok(result.files["electron/preload.ts"].includes("interface ElectronAPI"));
|
||||
assert.ok(result.files["electron/preload.ts"].includes("declare global"));
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 4 — package.json 验证
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("4 — package.json 验证", () => {
|
||||
let result, pkg;
|
||||
before(() => {
|
||||
result = buildElectron({ projectName: "Inventory" });
|
||||
pkg = JSON.parse(result.files["package.json"]);
|
||||
});
|
||||
|
||||
it("包含正确名称", () => {
|
||||
assert.ok(pkg.name.includes("inventory-desktop"));
|
||||
});
|
||||
|
||||
it("包含 main 入口", () => {
|
||||
assert.equal(pkg.main, "dist/electron/main.js");
|
||||
});
|
||||
|
||||
it("包含 dev 脚本", () => {
|
||||
assert.ok(pkg.scripts.dev, "Missing dev script");
|
||||
assert.ok(pkg.scripts.dev.includes("concurrently"));
|
||||
});
|
||||
|
||||
it("包含 build 脚本", () => {
|
||||
assert.ok(pkg.scripts.build, "Missing build script");
|
||||
});
|
||||
|
||||
it("包含平台构建脚本", () => {
|
||||
assert.ok(pkg.scripts["build:electron:win"], "Missing win build");
|
||||
assert.ok(pkg.scripts["build:electron:mac"], "Missing mac build");
|
||||
assert.ok(pkg.scripts["build:electron:linux"], "Missing linux build");
|
||||
});
|
||||
|
||||
it("依赖 electron", () => {
|
||||
assert.ok(pkg.devDependencies.electron, "Missing electron");
|
||||
});
|
||||
|
||||
it("依赖 electron-builder", () => {
|
||||
assert.ok(pkg.devDependencies["electron-builder"], "Missing electron-builder");
|
||||
});
|
||||
|
||||
it("依赖 electron-updater", () => {
|
||||
assert.ok(pkg.dependencies["electron-updater"], "Missing electron-updater");
|
||||
});
|
||||
|
||||
it("依赖 concurrently", () => {
|
||||
assert.ok(pkg.devDependencies.concurrently, "Missing concurrently");
|
||||
});
|
||||
|
||||
it("依赖 typescript", () => {
|
||||
assert.ok(pkg.devDependencies.typescript, "Missing typescript");
|
||||
});
|
||||
|
||||
it("build 配置包含 appId", () => {
|
||||
assert.ok(pkg.build.appId.includes("inventory.desktop"));
|
||||
});
|
||||
|
||||
it("build 配置包含 productName", () => {
|
||||
assert.equal(pkg.build.productName, "Inventory");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 5 — electron-builder.yml 验证
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("5 — electron-builder.yml 验证", () => {
|
||||
let result, yml;
|
||||
before(() => {
|
||||
result = buildElectron({ projectName: "PetCare" });
|
||||
yml = result.files["electron-builder.yml"];
|
||||
});
|
||||
|
||||
it("包含 appId", () => {
|
||||
assert.ok(yml.includes("appId: com.petcare.desktop"));
|
||||
});
|
||||
|
||||
it("包含 productName", () => {
|
||||
assert.ok(yml.includes("productName: PetCare"));
|
||||
});
|
||||
|
||||
it("包含 macOS 配置", () => {
|
||||
assert.ok(yml.includes("mac:"));
|
||||
assert.ok(yml.includes("dmg"));
|
||||
assert.ok(yml.includes("arm64"));
|
||||
});
|
||||
|
||||
it("包含 Windows 配置", () => {
|
||||
assert.ok(yml.includes("win:"));
|
||||
assert.ok(yml.includes("nsis"));
|
||||
});
|
||||
|
||||
it("包含 Linux 配置", () => {
|
||||
assert.ok(yml.includes("linux:"));
|
||||
assert.ok(yml.includes("AppImage"));
|
||||
assert.ok(yml.includes("deb"));
|
||||
});
|
||||
|
||||
it("包含 extraResources(web + api)", () => {
|
||||
assert.ok(yml.includes("extraResources"));
|
||||
assert.ok(yml.includes("../web/out"));
|
||||
assert.ok(yml.includes("../api/dist"));
|
||||
});
|
||||
|
||||
it("包含 publish 配置", () => {
|
||||
assert.ok(yml.includes("publish:"));
|
||||
assert.ok(yml.includes("provider: generic"));
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 6 — tsconfig.json 验证
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("6 — tsconfig.json 验证", () => {
|
||||
let result, tsconfig;
|
||||
before(() => {
|
||||
result = buildElectron();
|
||||
tsconfig = JSON.parse(result.files["tsconfig.json"]);
|
||||
});
|
||||
|
||||
it("target 是 ES2022", () => {
|
||||
assert.equal(tsconfig.compilerOptions.target, "ES2022");
|
||||
});
|
||||
|
||||
it("module 是 commonjs(Electron 主进程需要)", () => {
|
||||
assert.equal(tsconfig.compilerOptions.module, "commonjs");
|
||||
});
|
||||
|
||||
it("strict 模式开启", () => {
|
||||
assert.equal(tsconfig.compilerOptions.strict, true);
|
||||
});
|
||||
|
||||
it("outDir 是 dist", () => {
|
||||
assert.equal(tsconfig.compilerOptions.outDir, "dist");
|
||||
});
|
||||
|
||||
it("include 包含 electron/", () => {
|
||||
assert.ok(tsconfig.include.some(p => p.includes("electron")));
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 7 — 其他文件验证
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("7 — 辅助文件验证", () => {
|
||||
let result;
|
||||
before(() => {
|
||||
result = buildElectron({ projectName: "CRM" });
|
||||
});
|
||||
|
||||
it("README.md 存在且包含关键内容", () => {
|
||||
const readme = result.files["README.md"];
|
||||
assert.ok(readme, "Missing README.md");
|
||||
assert.ok(readme.includes("CRM"), "Missing project name");
|
||||
assert.ok(readme.includes("Development"), "Missing Development section");
|
||||
assert.ok(readme.includes("Build"), "Missing Build section");
|
||||
assert.ok(readme.includes("IPC API"), "Missing IPC API section");
|
||||
});
|
||||
|
||||
it("entitlements.mac.plist 存在", () => {
|
||||
const plist = result.files["entitlements.mac.plist"];
|
||||
assert.ok(plist, "Missing entitlements.mac.plist");
|
||||
assert.ok(plist.includes("com.apple.security.network.client"));
|
||||
});
|
||||
|
||||
it("icon.png 存在且非空", () => {
|
||||
assert.ok(result.files["assets/icon.png"], "Missing icon.png");
|
||||
assert.ok(result.files["assets/icon.png"].length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 8 — 自定义端口配置
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("8 — 自定义端口配置", () => {
|
||||
it("使用自定义 web 端口", () => {
|
||||
const result = buildElectron({ webPort: 4000 });
|
||||
assert.ok(result.files["electron/main.ts"].includes("4000"));
|
||||
});
|
||||
|
||||
it("使用自定义 api 端口", () => {
|
||||
const result = buildElectron({ apiPort: 4001 });
|
||||
assert.ok(result.files["electron/main.ts"].includes("4001"));
|
||||
assert.ok(result.files["electron/utils.ts"].includes("4001"));
|
||||
});
|
||||
|
||||
it("默认端口是 3000/3001", () => {
|
||||
const result = buildElectron();
|
||||
assert.ok(result.files["electron/main.ts"].includes("3000"));
|
||||
assert.ok(result.files["electron/main.ts"].includes("3001"));
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 9 — 写入 I/O
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("9 — 文件写入 I/O", () => {
|
||||
it("writeElectron 写入完整目录树", () => {
|
||||
const result = buildElectron({ projectName: "TestApp" });
|
||||
const tmpDir = resolve(WORKSPACE, ".tmp-electron-test");
|
||||
|
||||
writeElectron(result, tmpDir);
|
||||
|
||||
assert.ok(existsSync(resolve(tmpDir, "package.json")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron-builder.yml")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "tsconfig.json")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron/main.ts")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron/preload.ts")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron/ipc.ts")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron/tray.ts")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron/updater.ts")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "electron/utils.ts")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "assets/icon.png")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "entitlements.mac.plist")));
|
||||
assert.ok(existsSync(resolve(tmpDir, "README.md")));
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("所有生成的文件非空", () => {
|
||||
const result = buildElectron();
|
||||
for (const [path, content] of Object.entries(result.files)) {
|
||||
const len = typeof content === "string" ? content.length : content.byteLength;
|
||||
assert.ok(len > 0, `${path}: empty content`);
|
||||
}
|
||||
});
|
||||
|
||||
it("所有 package.json 可解析", () => {
|
||||
const result = buildElectron();
|
||||
const pkg = JSON.parse(result.files["package.json"]);
|
||||
assert.ok(pkg.name, "Missing package name");
|
||||
assert.ok(pkg.scripts, "Missing scripts");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 10 — 多领域覆盖
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
describe("10 — 多领域覆盖(PetCare / CRM / Inventory)", () => {
|
||||
const domains = [
|
||||
{ name: "PetCare", expected: "petcare" },
|
||||
{ name: "CRM", expected: "crm" },
|
||||
{ name: "Inventory", expected: "inventory" },
|
||||
];
|
||||
|
||||
for (const { name, expected } of domains) {
|
||||
it(`${name} — 生成成功`, () => {
|
||||
const result = buildElectron({ projectName: name });
|
||||
assert.ok(!result.error, `${name}: ${result.message}`);
|
||||
assert.ok(result.files["electron/main.ts"], `${name}: missing main.ts`);
|
||||
assert.ok(result.files["electron/preload.ts"], `${name}: missing preload.ts`);
|
||||
assert.ok(result.files["package.json"], `${name}: missing package.json`);
|
||||
assert.ok(result.files["electron-builder.yml"], `${name}: missing electron-builder.yml`);
|
||||
});
|
||||
|
||||
it(`${name} — package.json 包含正确名称`, () => {
|
||||
const result = buildElectron({ projectName: name });
|
||||
const pkg = JSON.parse(result.files["package.json"]);
|
||||
assert.ok(pkg.name.includes(expected), `${name}: expected '${expected}' in name, got '${pkg.name}'`);
|
||||
});
|
||||
|
||||
it(`${name} — electron-builder.yml 包含正确 appId`, () => {
|
||||
const result = buildElectron({ projectName: name });
|
||||
assert.ok(
|
||||
result.files["electron-builder.yml"].includes(`com.${expected}.desktop`),
|
||||
`${name}: expected 'com.${expected}.desktop' in appId`
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
# Archive: Deprecated
|
||||
@@ -0,0 +1 @@
|
||||
# Archive: Old Log 1
|
||||
@@ -0,0 +1 @@
|
||||
# Archive: Old Log 2
|
||||
@@ -0,0 +1 @@
|
||||
# Context: Core Memory
|
||||
@@ -0,0 +1 @@
|
||||
# Context: Projects
|
||||
@@ -0,0 +1 @@
|
||||
# Context: Settings
|
||||
@@ -0,0 +1 @@
|
||||
# Thread: Discussion 1
|
||||
@@ -0,0 +1 @@
|
||||
# Thread: Discussion 2
|
||||
@@ -0,0 +1 @@
|
||||
# Thread: Discussion 3
|
||||
@@ -0,0 +1 @@
|
||||
# Thread: Discussion 4
|
||||
@@ -0,0 +1 @@
|
||||
# Thread: Discussion 5
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"description": "Degrading agent — health score dropping over 2 snapshots",
|
||||
"snapshots": [
|
||||
{
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"agents": {
|
||||
"mock-staging": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-02T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-01T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"agents": {
|
||||
"mock-staging": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-05T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-04T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"description": "Healthy agent with improving trend — all indicators green, 2 snapshots showing improvement",
|
||||
"snapshots": [
|
||||
{
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"agents": {
|
||||
"claw-prod": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"baselineVersion": "v3",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-02T00:00:00.000Z"},
|
||||
{"result": "failed", "timestamp": "2026-06-01T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"agents": {
|
||||
"claw-prod": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v4",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-05T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-04T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"description": "Improving agent — recovering from degraded state over 3 snapshots",
|
||||
"snapshots": [
|
||||
{
|
||||
"timestamp": "2026-06-01T00:00:00.000Z",
|
||||
"agents": {
|
||||
"recovery-agent": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "not_ready",
|
||||
"baselineVersion": "unknown",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-01T00:00:00.000Z"},
|
||||
{"result": "failed", "timestamp": "2026-05-31T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 3,
|
||||
"compatibilityFailures": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"agents": {
|
||||
"recovery-agent": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"baselineVersion": "v1",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"},
|
||||
{"result": "failed", "timestamp": "2026-06-02T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"agents": {
|
||||
"recovery-agent": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-05T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-04T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"description": "Mixed fleet: healthy, degrading, unstable, and unknown agents",
|
||||
"snapshots": [
|
||||
{
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"agents": {
|
||||
"claw-prod": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v4",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-02T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
},
|
||||
"mock-staging": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
},
|
||||
"old-claw-beta": {
|
||||
"status": "unsupported",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "not_ready",
|
||||
"baselineVersion": "unknown",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1
|
||||
},
|
||||
"unknown-sdk-v1": {
|
||||
"status": "unsupported",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "unknown",
|
||||
"baselineVersion": "unknown",
|
||||
"releaseHistory": [],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"agents": {
|
||||
"claw-prod": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v4",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-05T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-04T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
},
|
||||
"mock-staging": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-05T00:00:00.000Z"},
|
||||
{"result": "success", "timestamp": "2026-06-04T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1
|
||||
},
|
||||
"old-claw-beta": {
|
||||
"status": "unsupported",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "not_ready",
|
||||
"baselineVersion": "unknown",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-05T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 2
|
||||
},
|
||||
"unknown-sdk-v1": {
|
||||
"status": "unsupported",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "unknown",
|
||||
"baselineVersion": "unknown",
|
||||
"releaseHistory": [],
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"description": "Unstable agent — multiple rollbacks, compat failures, certification downgrades",
|
||||
"snapshots": [
|
||||
{
|
||||
"timestamp": "2026-06-01T00:00:00.000Z",
|
||||
"agents": {
|
||||
"unstable-agent": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "success", "timestamp": "2026-06-01T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"agents": {
|
||||
"unstable-agent": {
|
||||
"status": "active",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"baselineVersion": "v2",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-03T00:00:00.000Z"},
|
||||
{"result": "failed", "timestamp": "2026-06-02T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"agents": {
|
||||
"unstable-agent": {
|
||||
"status": "unsupported",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "not_ready",
|
||||
"baselineVersion": "unknown",
|
||||
"releaseHistory": [
|
||||
{"result": "failed", "timestamp": "2026-06-05T00:00:00.000Z"},
|
||||
{"result": "failed", "timestamp": "2026-06-04T00:00:00.000Z"},
|
||||
{"result": "failed", "timestamp": "2026-06-03T00:00:00.000Z"}
|
||||
],
|
||||
"rollbacks": 4,
|
||||
"compatibilityFailures": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"agentType": "claw",
|
||||
"displayName": "Old Claw Beta",
|
||||
"status": "deprecated",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "legacy-memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "deprecated",
|
||||
"lastCertifiedAt": "2025-06-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "mock",
|
||||
"displayName": "Claw Production Duplicate",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "mock",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-demo",
|
||||
"displayName": "Claw Demo",
|
||||
"status": "active",
|
||||
"owner": "team-memory"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "mock-dev",
|
||||
"agentType": "mock",
|
||||
"displayName": "Mock Development Agent",
|
||||
"status": "active",
|
||||
"owner": "dev-team",
|
||||
"workspacePath": "test/fixtures/agent-adapters/mock-agent",
|
||||
"adapter": "mock",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-03T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"agentType": "claw",
|
||||
"displayName": "Old Claw Beta",
|
||||
"status": "deprecated",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "legacy-memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "deprecated",
|
||||
"lastCertifiedAt": "2025-06-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"agentType": "unknown",
|
||||
"displayName": "Unknown SDK Agent v1",
|
||||
"status": "unsupported",
|
||||
"owner": "external-team",
|
||||
"workspacePath": "legacy-data/",
|
||||
"adapter": "none",
|
||||
"baselineVersion": "unknown",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"lastCertifiedAt": "2025-12-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"agentType": "unknown",
|
||||
"displayName": "Unknown SDK Agent v1",
|
||||
"status": "unsupported",
|
||||
"owner": "external-team",
|
||||
"workspacePath": "legacy-data/",
|
||||
"adapter": "none",
|
||||
"baselineVersion": "unknown",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"lastCertifiedAt": "2025-12-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "空 PRD — 用于测试错误处理",
|
||||
"error": "EMPTY_INPUT",
|
||||
"message": "请输入需求描述"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"projectName": "UnknownApp",
|
||||
"chineseName": "通用应用",
|
||||
"domain": "generic",
|
||||
"summary": "一个用户定制的应用。",
|
||||
"features": [
|
||||
{ "name": "核心功能", "description": "产品核心功能模块", "priority": "P0" }
|
||||
],
|
||||
"pages": [
|
||||
{ "name": "首页", "route": "/home", "description": "应用首页" },
|
||||
{ "name": "我的", "route": "/profile", "description": "用户中心" }
|
||||
],
|
||||
"apiRequirements": [
|
||||
{ "method": "GET", "path": "/api/health", "description": "健康检查" },
|
||||
{ "method": "POST", "path": "/api/users", "description": "用户注册" }
|
||||
],
|
||||
"techConstraints": {
|
||||
"platforms": ["web"],
|
||||
"recommendedStack": "任意",
|
||||
"considerations": []
|
||||
},
|
||||
"extraFeatures": [],
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"projectName":"NoteApp","chineseName":"笔记应用","domain":"note","matchConfidence":"high","summary":"一款支持多端同步、Markdown 编辑和标签管理的笔记应用。","personas":[{"name":"知识工作者","description":"需要记录灵感和整理知识的职场人","painPoints":["笔记分散多处","搜索困难","格式化繁琐"]}],"userStories":[{"id":"US-001","as":"知识工作者","want":"富文本/Markdown编辑器、实时保存","soThat":"解决痛点:笔记分散多处","priority":"P0"},{"id":"US-002","as":"知识工作者","want":"分类管理、多级文件夹","soThat":"解决痛点:笔记分散多处","priority":"P0"},{"id":"US-003","as":"知识工作者","want":"标题+内容全文检索","soThat":"解决痛点:笔记分散多处","priority":"P0"}],"mvpScope":{"description":"MVP 聚焦 笔记编辑、文件夹/标签、全文搜索,包含页面:笔记列表、笔记编辑、标签管理","features":["笔记编辑","文件夹/标签","全文搜索"],"pages":["笔记列表","笔记编辑","标签管理"],"estimatedWeeks":3},"features":[{"name":"笔记编辑","description":"富文本/Markdown编辑器、实时保存","priority":"P0"},{"name":"文件夹/标签","description":"分类管理、多级文件夹","priority":"P0"},{"name":"全文搜索","description":"标题+内容全文检索","priority":"P0"},{"name":"多端同步","description":"手机/PC/Web 实时同步","priority":"P1"},{"name":"协作分享","description":"分享链接、协作编辑","priority":"P2"}],"pages":[{"name":"笔记列表","route":"/notes","description":"文件夹导航、笔记列表、搜索"},{"name":"笔记编辑","route":"/note/:id","description":"编辑器页面"},{"name":"标签管理","route":"/tags","description":"标签列表、关联笔记"},{"name":"设置","route":"/settings","description":"主题、同步、账户"}],"apiRequirements":[{"method":"GET","path":"/api/notes","description":"笔记列表"},{"method":"POST","path":"/api/notes","description":"创建笔记"},{"method":"PUT","path":"/api/notes/:id","description":"更新笔记"},{"method":"DELETE","path":"/api/notes/:id","description":"删除笔记"},{"method":"GET","path":"/api/tags","description":"标签列表"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter(跨平台)","considerations":["建议跨平台框架减少开发成本"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"项目脚手架搭建","description":"初始化 NoteApp 项目结构,配置构建工具和 CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"数据库设计与初始化","description":"设计数据模型,创建数据库迁移脚本","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"页面开发:笔记列表","description":"开发 笔记列表 页面(/notes):文件夹导航、笔记列表、搜索","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"页面开发:笔记编辑","description":"开发 笔记编辑 页面(/note/:id):编辑器页面","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"页面开发:标签管理","description":"开发 标签管理 页面(/tags):标签列表、关联笔记","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"页面开发:设置","description":"开发 设置 页面(/settings):主题、同步、账户","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API 开发:GET /api/notes","description":"笔记列表","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API 开发:POST /api/notes","description":"创建笔记","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API 开发:PUT /api/notes/:id","description":"更新笔记","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API 开发:DELETE /api/notes/:id","description":"删除笔记","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"前后端联调","description":"前端页面与后端 API 集成联调","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"测试与修复","description":"功能测试、Bug 修复、性能优化","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-04T22:53:57.117Z","inputLength":4,"domainMatchCount":1}}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"description": "Certification revoked alert — triggers create_incident and mark_high_risk",
|
||||
"alerts": [
|
||||
{
|
||||
"alertId": "monitor-004",
|
||||
"severity": "critical",
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"reason": "certification_downgraded_to_not_ready",
|
||||
"details": "Certification changed from production_ready to not_ready",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "CR-2",
|
||||
"source": "continuous-monitoring"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"eventType": "certification_revoked",
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"metadata": { "previousStatus": "production_ready" }
|
||||
}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"description": "Compatibility failure alert — triggers raise_risk and downgrade_certification",
|
||||
"alerts": [
|
||||
{
|
||||
"alertId": "monitor-001",
|
||||
"severity": "critical",
|
||||
"agentId": "claw-prod",
|
||||
"reason": "compatibility_unsupported",
|
||||
"details": "Compatibility status is unsupported",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "CR-3",
|
||||
"source": "continuous-monitoring"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"eventType": "compatibility_failed",
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"metadata": { "previousStatus": "supported" }
|
||||
}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"description": "Health degradation alert — triggers create_remediation_plan and increase_monitoring",
|
||||
"alerts": [
|
||||
{
|
||||
"alertId": "monitor-002",
|
||||
"severity": "high",
|
||||
"agentId": "claw-staging",
|
||||
"reason": "health_score_drop_high",
|
||||
"details": "Health score dropped 18% (90 → 72)",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "HR-1",
|
||||
"source": "continuous-monitoring"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"eventType": "health_degraded",
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"metadata": { "previousScore": 90, "currentScore": 72 }
|
||||
}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"description": "Mixed severity input — critical, high, and warning alerts triggering different response levels",
|
||||
"alerts": [
|
||||
{
|
||||
"alertId": "monitor-010",
|
||||
"severity": "critical",
|
||||
"agentId": "old-claw-beta",
|
||||
"reason": "compatibility_unsupported",
|
||||
"details": "Compatibility status is unsupported",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "CR-3",
|
||||
"source": "continuous-monitoring"
|
||||
},
|
||||
{
|
||||
"alertId": "monitor-011",
|
||||
"severity": "high",
|
||||
"agentId": "claw-staging",
|
||||
"reason": "health_score_drop_high",
|
||||
"details": "Health score dropped 18%",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "HR-1",
|
||||
"source": "continuous-monitoring"
|
||||
},
|
||||
{
|
||||
"alertId": "monitor-012",
|
||||
"severity": "warning",
|
||||
"agentId": "mock-staging",
|
||||
"reason": "compatibility_warning",
|
||||
"details": "Compatibility has warnings",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "WR-1",
|
||||
"source": "continuous-monitoring"
|
||||
},
|
||||
{
|
||||
"alertId": "monitor-013",
|
||||
"severity": "high",
|
||||
"agentId": "unstable-mock",
|
||||
"reason": "rollback_spike",
|
||||
"details": "Rollback count spiked from 1 to 4",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "HR-3",
|
||||
"source": "continuous-monitoring"
|
||||
}
|
||||
],
|
||||
"events": [],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"description": "Release failure alert — triggers raise_alert and flag_for_review",
|
||||
"alerts": [
|
||||
{
|
||||
"alertId": "monitor-003",
|
||||
"severity": "critical",
|
||||
"agentId": "old-claw-beta",
|
||||
"reason": "consecutive_release_failure",
|
||||
"details": "3 consecutive release failures detected",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "CR-4",
|
||||
"source": "continuous-monitoring"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"eventType": "release_failed",
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"metadata": { "failureCount": 3 }
|
||||
}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"description": "Warning-only input — only warning alerts, should only generate recommendations, not auto-execute",
|
||||
"alerts": [
|
||||
{
|
||||
"alertId": "monitor-020",
|
||||
"severity": "warning",
|
||||
"agentId": "claw-canary",
|
||||
"reason": "certification_conditionally_ready",
|
||||
"details": "Certification is conditionally ready",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "WR-2",
|
||||
"source": "continuous-monitoring"
|
||||
},
|
||||
{
|
||||
"alertId": "monitor-021",
|
||||
"severity": "warning",
|
||||
"agentId": "claw-canary",
|
||||
"reason": "missing_fields",
|
||||
"details": "Missing or unknown fields: releaseResult",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"ruleId": "WR-3",
|
||||
"source": "continuous-monitoring"
|
||||
}
|
||||
],
|
||||
"events": [],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T20:00:31.447Z",
|
||||
"chainLength": 2,
|
||||
"steps": [
|
||||
{
|
||||
"from": "v1",
|
||||
"to": "v2",
|
||||
"growthMode": "normal",
|
||||
"metrics": {
|
||||
"blocCount": {
|
||||
"from": 6,
|
||||
"to": 6,
|
||||
"delta": "0%",
|
||||
"absolute": 0
|
||||
},
|
||||
"cycleCount": {
|
||||
"from": 7,
|
||||
"to": 7,
|
||||
"delta": "0%",
|
||||
"absolute": 0
|
||||
},
|
||||
"candidateCount": {
|
||||
"from": 36,
|
||||
"to": 38,
|
||||
"delta": "+5.56%",
|
||||
"absolute": 2
|
||||
},
|
||||
"activeCandidateCount": {
|
||||
"from": 28,
|
||||
"to": 30,
|
||||
"delta": "+7.14%",
|
||||
"absolute": 2
|
||||
},
|
||||
"archivedCandidateCount": {
|
||||
"from": 8,
|
||||
"to": 8,
|
||||
"delta": "0%",
|
||||
"absolute": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": "v2",
|
||||
"to": "v3",
|
||||
"growthMode": "moderate",
|
||||
"metrics": {
|
||||
"blocCount": {
|
||||
"from": 6,
|
||||
"to": 7,
|
||||
"delta": "+16.67%",
|
||||
"absolute": 1
|
||||
},
|
||||
"cycleCount": {
|
||||
"from": 7,
|
||||
"to": 9,
|
||||
"delta": "+28.57%",
|
||||
"absolute": 2
|
||||
},
|
||||
"candidateCount": {
|
||||
"from": 38,
|
||||
"to": 46,
|
||||
"delta": "+21.05%",
|
||||
"absolute": 8
|
||||
},
|
||||
"activeCandidateCount": {
|
||||
"from": 30,
|
||||
"to": 36,
|
||||
"delta": "+20%",
|
||||
"absolute": 6
|
||||
},
|
||||
"archivedCandidateCount": {
|
||||
"from": 8,
|
||||
"to": 10,
|
||||
"delta": "+25%",
|
||||
"absolute": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"thresholdEvaluation": [
|
||||
{
|
||||
"from": "v1",
|
||||
"to": "v2",
|
||||
"metrics": {
|
||||
"blocCount": "✅ PASS",
|
||||
"cycleCount": "✅ PASS",
|
||||
"candidateCount": "✅ PASS",
|
||||
"activeCandidateCount": "✅ PASS",
|
||||
"archivedCandidateCount": "✅ PASS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": "v2",
|
||||
"to": "v3",
|
||||
"metrics": {
|
||||
"blocCount": "✅ PASS",
|
||||
"cycleCount": "⚠️ WARN",
|
||||
"candidateCount": "⚠️ WARN",
|
||||
"activeCandidateCount": "✅ PASS",
|
||||
"archivedCandidateCount": "⚠️ WARN"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"totalSteps": 2,
|
||||
"passSteps": 1,
|
||||
"warnSteps": 1,
|
||||
"failSteps": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T19:58:33.249Z",
|
||||
"thresholds": {
|
||||
"warn": 20,
|
||||
"fail": 50
|
||||
},
|
||||
"baselineVersions": [
|
||||
"v1",
|
||||
"v2",
|
||||
"v3"
|
||||
],
|
||||
"deltaLevels": [
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
30,
|
||||
50,
|
||||
70
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"baseline": "v1",
|
||||
"deltaPct": 5,
|
||||
"status": "pass",
|
||||
"blockers": 0,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v1",
|
||||
"deltaPct": 10,
|
||||
"status": "pass",
|
||||
"blockers": 0,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v1",
|
||||
"deltaPct": 20,
|
||||
"status": "warn",
|
||||
"blockers": 0,
|
||||
"warnings": 2,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": [
|
||||
"Active Candidate Count: baseline=28 → candidate=34 (增加 21.43%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=10 (增加 25%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v1",
|
||||
"deltaPct": 30,
|
||||
"status": "warn",
|
||||
"blockers": 0,
|
||||
"warnings": 5,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": [
|
||||
"Bloc Count: baseline=6 → candidate=8 (增加 33.33%,超过警示阈值 20%)",
|
||||
"Cycle Count: baseline=7 → candidate=9 (增加 28.57%,超过警示阈值 20%)",
|
||||
"Candidate Count: baseline=36 → candidate=47 (增加 30.56%,超过警示阈值 20%)",
|
||||
"Active Candidate Count: baseline=28 → candidate=36 (增加 28.57%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=10 (增加 25%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v1",
|
||||
"deltaPct": 50,
|
||||
"status": "fail",
|
||||
"blockers": 1,
|
||||
"warnings": 4,
|
||||
"blockerDetails": [
|
||||
"Cycle Count: baseline=7 → candidate=11 (增加 57.14%,超过严重阈值 50%)"
|
||||
],
|
||||
"warningDetails": [
|
||||
"Bloc Count: baseline=6 → candidate=9 (增加 50%,超过警示阈值 20%)",
|
||||
"Candidate Count: baseline=36 → candidate=54 (增加 50%,超过警示阈值 20%)",
|
||||
"Active Candidate Count: baseline=28 → candidate=42 (增加 50%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=12 (增加 50%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v1",
|
||||
"deltaPct": 70,
|
||||
"status": "fail",
|
||||
"blockers": 5,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [
|
||||
"Bloc Count: baseline=6 → candidate=10 (增加 66.67%,超过严重阈值 50%)",
|
||||
"Cycle Count: baseline=7 → candidate=12 (增加 71.43%,超过严重阈值 50%)",
|
||||
"Candidate Count: baseline=36 → candidate=61 (增加 69.44%,超过严重阈值 50%)",
|
||||
"Active Candidate Count: baseline=28 → candidate=48 (增加 71.43%,超过严重阈值 50%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=14 (增加 75%,超过严重阈值 50%)"
|
||||
],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v2",
|
||||
"deltaPct": 5,
|
||||
"status": "pass",
|
||||
"blockers": 0,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v2",
|
||||
"deltaPct": 10,
|
||||
"status": "pass",
|
||||
"blockers": 0,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v2",
|
||||
"deltaPct": 20,
|
||||
"status": "warn",
|
||||
"blockers": 0,
|
||||
"warnings": 2,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": [
|
||||
"Candidate Count: baseline=38 → candidate=46 (增加 21.05%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=10 (增加 25%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v2",
|
||||
"deltaPct": 30,
|
||||
"status": "warn",
|
||||
"blockers": 0,
|
||||
"warnings": 5,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": [
|
||||
"Bloc Count: baseline=6 → candidate=8 (增加 33.33%,超过警示阈值 20%)",
|
||||
"Cycle Count: baseline=7 → candidate=9 (增加 28.57%,超过警示阈值 20%)",
|
||||
"Candidate Count: baseline=38 → candidate=49 (增加 28.95%,超过警示阈值 20%)",
|
||||
"Active Candidate Count: baseline=30 → candidate=39 (增加 30%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=10 (增加 25%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v2",
|
||||
"deltaPct": 50,
|
||||
"status": "fail",
|
||||
"blockers": 1,
|
||||
"warnings": 4,
|
||||
"blockerDetails": [
|
||||
"Cycle Count: baseline=7 → candidate=11 (增加 57.14%,超过严重阈值 50%)"
|
||||
],
|
||||
"warningDetails": [
|
||||
"Bloc Count: baseline=6 → candidate=9 (增加 50%,超过警示阈值 20%)",
|
||||
"Candidate Count: baseline=38 → candidate=57 (增加 50%,超过警示阈值 20%)",
|
||||
"Active Candidate Count: baseline=30 → candidate=45 (增加 50%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=12 (增加 50%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v2",
|
||||
"deltaPct": 70,
|
||||
"status": "fail",
|
||||
"blockers": 5,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [
|
||||
"Bloc Count: baseline=6 → candidate=10 (增加 66.67%,超过严重阈值 50%)",
|
||||
"Cycle Count: baseline=7 → candidate=12 (增加 71.43%,超过严重阈值 50%)",
|
||||
"Candidate Count: baseline=38 → candidate=65 (增加 71.05%,超过严重阈值 50%)",
|
||||
"Active Candidate Count: baseline=30 → candidate=51 (增加 70%,超过严重阈值 50%)",
|
||||
"Archived Candidate Count: baseline=8 → candidate=14 (增加 75%,超过严重阈值 50%)"
|
||||
],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v3",
|
||||
"deltaPct": 5,
|
||||
"status": "pass",
|
||||
"blockers": 0,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v3",
|
||||
"deltaPct": 10,
|
||||
"status": "pass",
|
||||
"blockers": 0,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": []
|
||||
},
|
||||
{
|
||||
"baseline": "v3",
|
||||
"deltaPct": 20,
|
||||
"status": "warn",
|
||||
"blockers": 0,
|
||||
"warnings": 1,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": [
|
||||
"Cycle Count: baseline=9 → candidate=11 (增加 22.22%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v3",
|
||||
"deltaPct": 30,
|
||||
"status": "warn",
|
||||
"blockers": 0,
|
||||
"warnings": 5,
|
||||
"blockerDetails": [],
|
||||
"warningDetails": [
|
||||
"Bloc Count: baseline=7 → candidate=9 (增加 28.57%,超过警示阈值 20%)",
|
||||
"Cycle Count: baseline=9 → candidate=12 (增加 33.33%,超过警示阈值 20%)",
|
||||
"Candidate Count: baseline=46 → candidate=60 (增加 30.43%,超过警示阈值 20%)",
|
||||
"Active Candidate Count: baseline=36 → candidate=47 (增加 30.56%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=10 → candidate=13 (增加 30%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v3",
|
||||
"deltaPct": 50,
|
||||
"status": "fail",
|
||||
"blockers": 2,
|
||||
"warnings": 3,
|
||||
"blockerDetails": [
|
||||
"Bloc Count: baseline=7 → candidate=11 (增加 57.14%,超过严重阈值 50%)",
|
||||
"Cycle Count: baseline=9 → candidate=14 (增加 55.56%,超过严重阈值 50%)"
|
||||
],
|
||||
"warningDetails": [
|
||||
"Candidate Count: baseline=46 → candidate=69 (增加 50%,超过警示阈值 20%)",
|
||||
"Active Candidate Count: baseline=36 → candidate=54 (增加 50%,超过警示阈值 20%)",
|
||||
"Archived Candidate Count: baseline=10 → candidate=15 (增加 50%,超过警示阈值 20%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"baseline": "v3",
|
||||
"deltaPct": 70,
|
||||
"status": "fail",
|
||||
"blockers": 5,
|
||||
"warnings": 0,
|
||||
"blockerDetails": [
|
||||
"Bloc Count: baseline=7 → candidate=12 (增加 71.43%,超过严重阈值 50%)",
|
||||
"Cycle Count: baseline=9 → candidate=15 (增加 66.67%,超过严重阈值 50%)",
|
||||
"Candidate Count: baseline=46 → candidate=78 (增加 69.57%,超过严重阈值 50%)",
|
||||
"Active Candidate Count: baseline=36 → candidate=61 (增加 69.44%,超过严重阈值 50%)",
|
||||
"Archived Candidate Count: baseline=10 → candidate=17 (增加 70%,超过严重阈值 50%)"
|
||||
],
|
||||
"warningDetails": []
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"totalScenarios": 18,
|
||||
"falsePositives": 0,
|
||||
"falseNegatives": 0,
|
||||
"thresholdBoundaryCorrect": {
|
||||
"at20": true,
|
||||
"at50": true
|
||||
},
|
||||
"conclusion": "✅ Thresholds are stable. No false positives or false negatives detected."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T19:45:50.155Z",
|
||||
"version": "v1",
|
||||
"workspace": "/Users/<user>/.openclaw/workspace",
|
||||
"metrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8,
|
||||
"details": {
|
||||
"coreBlocFiles": 6,
|
||||
"projectFiles": 5,
|
||||
"registerFiles": 7,
|
||||
"topLevelFiles": 8,
|
||||
"dailyNoteFiles": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T20:00:31.444Z",
|
||||
"version": "v2",
|
||||
"previousVersion": "v1",
|
||||
"growthMode": "normal",
|
||||
"workspace": "/Users/<user>/.openclaw/workspace",
|
||||
"metrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 38,
|
||||
"activeCandidateCount": 30,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
"details": {
|
||||
"coreBlocFiles": 6,
|
||||
"projectFiles": 5,
|
||||
"registerFiles": 7,
|
||||
"topLevelFiles": 8,
|
||||
"dailyNoteFiles": 8,
|
||||
"evolvedFrom": "v1",
|
||||
"growthApplied": "日常记忆积累 (3-8%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T20:00:31.445Z",
|
||||
"version": "v3",
|
||||
"previousVersion": "v2",
|
||||
"growthMode": "moderate",
|
||||
"workspace": "/Users/<user>/.openclaw/workspace",
|
||||
"metrics": {
|
||||
"blocCount": 7,
|
||||
"cycleCount": 9,
|
||||
"candidateCount": 46,
|
||||
"activeCandidateCount": 36,
|
||||
"archivedCandidateCount": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"currentVersion": "v3",
|
||||
"path": "/Users/a1234/.openclaw/workspace/test/fixtures/baseline-rotation/v3-baseline.json",
|
||||
"updatedAt": "2026-06-04T00:00:00.000Z",
|
||||
"previousVersion": "v2",
|
||||
"rotationId": "rotation-001"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"keepLatest": 3,
|
||||
"archiveOlderThan": 3,
|
||||
"requireGateOpen": true,
|
||||
"requireCompatibilitySupported": true,
|
||||
"allowWarnings": false,
|
||||
"dryRun": true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T19:45:50.155Z",
|
||||
"version": "v1",
|
||||
"workspace": "/Users/<user>/.openclaw/workspace",
|
||||
"metrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8,
|
||||
"details": {
|
||||
"coreBlocFiles": 6,
|
||||
"projectFiles": 5,
|
||||
"registerFiles": 7,
|
||||
"topLevelFiles": 8,
|
||||
"dailyNoteFiles": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T20:00:31.444Z",
|
||||
"version": "v2",
|
||||
"previousVersion": "v1",
|
||||
"growthMode": "normal",
|
||||
"workspace": "/Users/<user>/.openclaw/workspace",
|
||||
"metrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 38,
|
||||
"activeCandidateCount": 30,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
"details": {
|
||||
"coreBlocFiles": 6,
|
||||
"projectFiles": 5,
|
||||
"registerFiles": 7,
|
||||
"topLevelFiles": 8,
|
||||
"dailyNoteFiles": 8,
|
||||
"evolvedFrom": "v1",
|
||||
"growthApplied": "日常记忆积累 (3-8%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"generatedAt": "2026-06-04T20:00:31.445Z",
|
||||
"version": "v3",
|
||||
"previousVersion": "v2",
|
||||
"growthMode": "moderate",
|
||||
"workspace": "/Users/<user>/.openclaw/workspace",
|
||||
"metrics": {
|
||||
"blocCount": 7,
|
||||
"cycleCount": 9,
|
||||
"candidateCount": 46,
|
||||
"activeCandidateCount": 36,
|
||||
"archivedCandidateCount": 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "废弃版本",
|
||||
"scenarioId": 6,
|
||||
|
||||
"clawVersion": "0.1.0",
|
||||
"baselineVersion": "",
|
||||
"schemaStatus": "deprecated",
|
||||
"pipelineStatus": "unknown",
|
||||
"gateDecision": "unknown",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"knownRisks": [
|
||||
"使用旧目录结构 legacy-memory/,不在当前文件扫描路径中",
|
||||
"无法运行 Baseline Compare",
|
||||
"无法运行 Release Pipeline",
|
||||
"遗留数据无迁移工具"
|
||||
],
|
||||
"recommendedAction": "migrate to Claw 1.0.x+ directory structure",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "legacy-memory/blocs/", "count": 4, "fields": ["id", "title", "content"], "note": "旧格式,字段名不同" },
|
||||
"cycles": { "path": "legacy-memory/cycles/", "count": 5, "fields": ["id", "date", "notes"], "note": "旧格式,无结构化 entries" },
|
||||
"candidates": { "path": "legacy-memory/candidates/", "count": 20, "fields": ["id", "text", "score"], "note": "旧格式,无 contentHash/type" }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": null,
|
||||
"cycleCount": null,
|
||||
"candidateCount": null,
|
||||
"activeCandidateCount": null,
|
||||
"archivedCandidateCount": null
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": null,
|
||||
"cycleCount": null,
|
||||
"candidateCount": null,
|
||||
"activeCandidateCount": null,
|
||||
"archivedCandidateCount": null
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "unknown", "blockers": ["旧路径不在扫描范围"], "warnings": [] },
|
||||
"baselineCompare": { "status": "unknown", "blockers": ["无 Baseline 数据"], "warnings": [] },
|
||||
"releaseGate": { "decision": "unknown", "blockers": ["无法运行 Release Pipeline"], "warnings": [] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "当前兼容版本",
|
||||
"scenarioId": 1,
|
||||
|
||||
"clawVersion": "1.0.0",
|
||||
"baselineVersion": "v1",
|
||||
"schemaStatus": "compatible",
|
||||
"pipelineStatus": "pass",
|
||||
"gateDecision": "open",
|
||||
"compatibilityStatus": "supported",
|
||||
"knownRisks": [],
|
||||
"recommendedAction": "safe to release",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["id", "title", "content"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["date", "entries"] },
|
||||
"candidates": { "path": "memory/candidates/*.md", "count": 36, "fields": ["id", "contentHash", "type", "status"] }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "pass", "blockers": 0, "warnings": 0 },
|
||||
"baselineCompare": { "status": "pass", "deltas": {}, "blockers": 0 },
|
||||
"releaseGate": { "decision": "open", "blockers": 0 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "缺失关键目录:memory/candidates 不存在",
|
||||
"scenarioId": 9,
|
||||
|
||||
"clawVersion": "1.0.0",
|
||||
"baselineVersion": "v1",
|
||||
"schemaStatus": "breaking",
|
||||
"pipelineStatus": "fail",
|
||||
"gateDecision": "closed",
|
||||
"compatibilityStatus": "migration_required",
|
||||
"knownRisks": [
|
||||
"memory/candidates 目录缺失,candidateCount = 0",
|
||||
"Baseline Compare candidateCount delta = -36 (-100%),超过 FAIL 阈值",
|
||||
"Release Gate 自动 CLOSED — 无法通过门禁",
|
||||
"无法生成 Release Candidate Report — 关键指标缺失",
|
||||
"可能是误删除或初始化未完成,需人工确认"
|
||||
],
|
||||
"recommendedAction": "restore memory/candidates/ from backup or re-run init-candidates.sh",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["id", "title", "content"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["date", "entries"] },
|
||||
"candidates": { "path": "memory/candidates/*.md", "count": 0, "fields": [], "note": "目录不存在或为空" }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 0,
|
||||
"activeCandidateCount": 0,
|
||||
"archivedCandidateCount": 0
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": -36,
|
||||
"activeCandidateCount": -28,
|
||||
"archivedCandidateCount": -8
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "fail", "blockers": ["关键目录 memory/candidates 缺失,candidateCount = 0"], "warnings": [] },
|
||||
"baselineCompare": { "status": "fail", "blockers": ["candidateCount delta = -100% (>50%), activeCandidateCount delta = -100%, archivedCandidateCount delta = -100%"], "warnings": [] },
|
||||
"releaseGate": { "decision": "closed", "blockers": ["[Schema] memory/candidates 缺失 — 关键目录不可用", "[Checklist] 目录缺失,无法验证关键指标", "[Baseline] candidateCount delta = -100%, Gate CLOSED"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "小版本兼容变化:新增字段",
|
||||
"scenarioId": 2,
|
||||
|
||||
"clawVersion": "1.1.0",
|
||||
"baselineVersion": "v1",
|
||||
"schemaStatus": "compatible",
|
||||
"pipelineStatus": "pass",
|
||||
"gateDecision": "open",
|
||||
"compatibilityStatus": "supported",
|
||||
"knownRisks": [
|
||||
"新字段被上游消费者忽略,无影响",
|
||||
"新增字段不会改变聚合计数"
|
||||
],
|
||||
"recommendedAction": "safe to release",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["id", "title", "content", "metadata.priority"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["date", "entries", "cycle.duration"] },
|
||||
"candidates": { "path": "memory/candidates/*.md", "count": 36, "fields": ["id", "contentHash", "type", "status", "candidate.tags"] }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": 0,
|
||||
"activeCandidateCount": 0,
|
||||
"archivedCandidateCount": 0
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "pass", "blockers": 0, "warnings": 0 },
|
||||
"baselineCompare": { "status": "pass", "blockers": 0, "deltas": { "blocCount": 0, "cycleCount": 0, "candidateCount": 0, "activeCandidateCount": 0, "archivedCandidateCount": 0 } },
|
||||
"releaseGate": { "decision": "open", "blockers": 0 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "字段重命名",
|
||||
"scenarioId": 3,
|
||||
|
||||
"clawVersion": "1.2.0",
|
||||
"baselineVersion": "v1",
|
||||
"schemaStatus": "compatible",
|
||||
"pipelineStatus": "pass",
|
||||
"gateDecision": "open",
|
||||
"compatibilityStatus": "supported",
|
||||
"knownRisks": [
|
||||
"bloc.id → bloc.blocId:上游消费者需要同步字段名",
|
||||
"cycle.id → cycle.cycleId:Baseline 聚合不受影响",
|
||||
"candidate.contentHash → candidate.hash:影响深拷贝一致性比对"
|
||||
],
|
||||
"recommendedAction": "safe to release, notify consumers",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["blocId", "title", "content"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["cycleId", "date", "entries"] },
|
||||
"candidates": { "path": "memory/candidates/*.md", "count": 36, "fields": ["id", "hash", "type", "status"] }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": 0,
|
||||
"activeCandidateCount": 0,
|
||||
"archivedCandidateCount": 0
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "pass", "blockers": 0, "warnings": 0 },
|
||||
"baselineCompare": { "status": "pass", "blockers": 0, "deltas": { "blocCount": 0, "cycleCount": 0, "candidateCount": 0, "activeCandidateCount": 0, "archivedCandidateCount": 0 } },
|
||||
"releaseGate": { "decision": "open", "blockers": 0 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "新增目录",
|
||||
"scenarioId": 4,
|
||||
|
||||
"clawVersion": "1.3.0",
|
||||
"baselineVersion": "v1",
|
||||
"schemaStatus": "compatible",
|
||||
"pipelineStatus": "pass",
|
||||
"gateDecision": "open",
|
||||
"compatibilityStatus": "supported",
|
||||
"knownRisks": [
|
||||
"candidateCount 从 36 → 46 (+28%) 超过 WARN 阈值 (20%),可能触发 Baseline Compare WARN",
|
||||
"activeCandidateCount 从 28 → 36 (+29%) 超过 WARN 阈值",
|
||||
"新增 memory/files/ 目录未被 Baseline 覆盖,需要更新 Baseline 版本到 v2"
|
||||
],
|
||||
"recommendedAction": "safe to release, update baseline to v2",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["id", "title", "content"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["date", "entries"] },
|
||||
"candidates": { "path": "memory/candidates/*.md", "count": 46, "fields": ["id", "contentHash", "type", "status"] },
|
||||
"files": { "path": "memory/files/*.md", "count": 10, "fields": ["id", "filename", "size", "mimeType"] }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 46,
|
||||
"activeCandidateCount": 36,
|
||||
"archivedCandidateCount": 10
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": 10,
|
||||
"activeCandidateCount": 8,
|
||||
"archivedCandidateCount": 2
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "pass", "blockers": 0, "warnings": 0 },
|
||||
"baselineCompare": { "status": "warn", "blockers": 0, "warnings": ["candidateCount +28%, activeCandidateCount +29% exceed WARN threshold"] },
|
||||
"releaseGate": { "decision": "open", "blockers": 0, "warnings": ["[Baseline] candidateCount 增加 28%,超过警示阈值 20%"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "中间件兼容层",
|
||||
"scenarioId": 7,
|
||||
|
||||
"clawVersion": "1.4.0",
|
||||
"baselineVersion": "v2",
|
||||
"schemaStatus": "compatible",
|
||||
"pipelineStatus": "pass",
|
||||
"gateDecision": "open",
|
||||
"compatibilityStatus": "supported",
|
||||
"knownRisks": [
|
||||
"兼容层文件 (memory/adapters/) 可能被 candidateCount 统计到,需更新扫描器排除规则",
|
||||
"etc/claw-migration.config 被 Pipeline 静默忽略",
|
||||
"Baseline v2 尚未生成,需要先运行 build-stable-baseline"
|
||||
],
|
||||
"recommendedAction": "safe to release, update baseline to v2",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["id", "title", "content"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["date", "entries"] },
|
||||
"candidates": { "path": "memory/candidates/*.md", "count": 36, "fields": ["id", "contentHash", "type", "status"] },
|
||||
"adapters": { "path": "memory/adapters/", "count": 3, "fields": ["source", "target", "transform"] },
|
||||
"config": { "path": "etc/claw-migration.config", "count": 1, "format": "TOML" }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": 0,
|
||||
"activeCandidateCount": 0,
|
||||
"archivedCandidateCount": 0
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "pass", "blockers": 0, "warnings": 0 },
|
||||
"baselineCompare": { "status": "pass", "blockers": 0, "deltas": { "blocCount": 0, "cycleCount": 0, "candidateCount": 0, "activeCandidateCount": 0, "archivedCandidateCount": 0 } },
|
||||
"releaseGate": { "decision": "open", "blockers": 0 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "目录结构变化:memory/candidates → memory/items",
|
||||
"scenarioId": 8,
|
||||
|
||||
"clawVersion": "1.5.0",
|
||||
"baselineVersion": "v1",
|
||||
"schemaStatus": "compatible",
|
||||
"pipelineStatus": "warn",
|
||||
"gateDecision": "open",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"knownRisks": [
|
||||
"memory/candidates 重命名为 memory/items,Pipeline 的 candidates 扫描路径需要更新",
|
||||
"Baseline 中 candidateCount 基于 memory/candidates/ 计算,新路径下统计为 0",
|
||||
"如果扫描器未更新,candidateCount 将显示为 0 → Baseline Compare delta = -100%(FAIL)",
|
||||
"迁移后需要对 Baseline 做增量更新(v1 → v2),保留历史 candidates 计数",
|
||||
"上游消费者(RC Checklist、Gate)依赖 candidateCount 做决策,可能触发 CLOSED"
|
||||
],
|
||||
"recommendedAction": "update scanner path rules (candidates → items) + rebuild baseline before release",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.md", "count": 6, "fields": ["id", "title", "content"] },
|
||||
"cycles": { "path": "memory/cycles/*.md", "count": 7, "fields": ["date", "entries"] },
|
||||
"candidates": { "path": "memory/items/*.md", "count": 36, "fields": ["id", "contentHash", "type", "status"] }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 6,
|
||||
"cycleCount": 7,
|
||||
"candidateCount": 36,
|
||||
"activeCandidateCount": 28,
|
||||
"archivedCandidateCount": 8
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": 0,
|
||||
"activeCandidateCount": 0,
|
||||
"archivedCandidateCount": 0
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "warn", "blockers": 0, "warnings": ["memory/candidates 路径变更 → memory/items", "扫描路径需同步更新,否则 count = 0"] },
|
||||
"baselineCompare": { "status": "warn", "blockers": 0, "deltas": { "blocCount": 0, "cycleCount": 0, "candidateCount": 0, "activeCandidateCount": 0, "archivedCandidateCount": 0 }, "warnings": ["candidate 扫描路径已更新,无 delta;如未更新将触发 FAIL"] },
|
||||
"releaseGate": { "decision": "open", "blockers": 0, "warnings": ["[Schema] memory/candidates → memory/items 路径变更,建议更新扫描器并重建 Baseline"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "compatibility-matrix-entry-v1",
|
||||
"generatedAt": "2026-06-05T00:00:00.000Z",
|
||||
"scenario": "不兼容 Schema 变更:Markdown → JSON 格式变化",
|
||||
"scenarioId": 5,
|
||||
|
||||
"clawVersion": "2.0.0",
|
||||
"baselineVersion": "",
|
||||
"schemaStatus": "breaking",
|
||||
"pipelineStatus": "fail",
|
||||
"gateDecision": "closed",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"knownRisks": [
|
||||
"文件格式从 Markdown (.md) 变更为 JSON (.json),文件扫描器无法解析 Markdown 解析器",
|
||||
"Baseline Compare delta > 50%,Gate 自动 CLOSED",
|
||||
"所有既有 Pipeline 检查项均报 FAIL",
|
||||
"需要同步更新 Baseline 版本和文件扫描逻辑",
|
||||
"迁移后需验证 JSON Schema 与数据结构一致性"
|
||||
],
|
||||
"recommendedAction": "update baseline scanner (add JSON parser) + rebuild baseline before release",
|
||||
|
||||
"dataStructure": {
|
||||
"blocs": { "path": "memory/blocs/*.json", "count": 0, "fields": [], "note": "格式变更:.md → .json,当前扫描器无法识别" },
|
||||
"cycles": { "path": "memory/cycles/*.json", "count": 0, "fields": [], "note": "格式变更:.md → .json" },
|
||||
"candidates": { "path": "memory/candidates/*.json", "count": 0, "fields": [], "note": "格式变更:.md → .json" }
|
||||
},
|
||||
|
||||
"baselineMetrics": {
|
||||
"blocCount": 0,
|
||||
"cycleCount": 0,
|
||||
"candidateCount": 0,
|
||||
"activeCandidateCount": 0,
|
||||
"archivedCandidateCount": 0
|
||||
},
|
||||
|
||||
"deltaFromBaseline": {
|
||||
"blocCount": -6,
|
||||
"cycleCount": -7,
|
||||
"candidateCount": -36,
|
||||
"activeCandidateCount": -28,
|
||||
"archivedCandidateCount": -8
|
||||
},
|
||||
|
||||
"pipelineChecks": {
|
||||
"rcChecklist": { "status": "fail", "blockers": ["文件格式变更:.md → .json", "扫描器无法解析 JSON 格式"], "warnings": [] },
|
||||
"baselineCompare": { "status": "fail", "blockers": ["所有指标 delta > 50%", "文件计数归零 → 格式不兼容"], "warnings": [] },
|
||||
"releaseGate": { "decision": "closed", "blockers": ["Schema breaking change — 文件格式变更导致指标严重偏离基线", "[Checklist] 文件格式变更,建议迁移适配器", "[Baseline] Baseline Compare FAIL — 全部指标缺失"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"description": "Critical agent with severe health drop, certification downgrade, and unsupported compatibility — triggers multiple critical alerts",
|
||||
"monitoringSnapshots": [
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-01T00:00:00.000Z",
|
||||
"healthScore": 75,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 80,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-02T00:00:00.000Z",
|
||||
"healthScore": 60,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 65,
|
||||
"status": "degrading",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 35,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 40,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 1
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 15,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "not_ready",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"stabilityScore": 20,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 2
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"healthScore": 10,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "not_ready",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"stabilityScore": 15,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 3,
|
||||
"compatibilityFailures": 3,
|
||||
"releaseFailures": 3
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"criticalHealthDropPercent": 30,
|
||||
"highHealthDropPercent": 15,
|
||||
"trendWindowSize": 3,
|
||||
"consecutiveFailureThreshold": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"description": "Degrading agent with health declining over time — warnings accumulating, 4 snapshots showing steady decline",
|
||||
"monitoringSnapshots": [
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-01T00:00:00.000Z",
|
||||
"healthScore": 88,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 95,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-02T00:00:00.000Z",
|
||||
"healthScore": 78,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 85,
|
||||
"status": "degrading",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 65,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 70,
|
||||
"status": "degrading",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 1
|
||||
},
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 52,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 55,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 2
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"criticalHealthDropPercent": 30,
|
||||
"highHealthDropPercent": 15,
|
||||
"trendWindowSize": 3,
|
||||
"consecutiveFailureThreshold": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"description": "Healthy agent with stable health over time — all indicators green, 4 snapshots showing consistent high scores",
|
||||
"monitoringSnapshots": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-01T00:00:00.000Z",
|
||||
"healthScore": 95,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-02T00:00:00.000Z",
|
||||
"healthScore": 97,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 96,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 98,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"criticalHealthDropPercent": 30,
|
||||
"highHealthDropPercent": 15,
|
||||
"trendWindowSize": 3,
|
||||
"consecutiveFailureThreshold": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Agent with missing/unknown data fields — tests that missing fields are not assumed healthy",
|
||||
"monitoringSnapshots": [
|
||||
{
|
||||
"agentId": "incomplete-sdk",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 85,
|
||||
"riskLevel": "unknown",
|
||||
"certificationStatus": "unknown",
|
||||
"compatibilityStatus": "unknown",
|
||||
"releaseResult": "unknown",
|
||||
"rollbacks": 0,
|
||||
"releaseFailures": 0,
|
||||
"compatibilityFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "incomplete-sdk",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"healthScore": 82,
|
||||
"riskLevel": "unknown",
|
||||
"certificationStatus": "unknown",
|
||||
"compatibilityStatus": "unknown",
|
||||
"releaseResult": "unknown",
|
||||
"rollbacks": 0,
|
||||
"releaseFailures": 0,
|
||||
"compatibilityFailures": 0
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"criticalHealthDropPercent": 30,
|
||||
"highHealthDropPercent": 15,
|
||||
"trendWindowSize": 3,
|
||||
"consecutiveFailureThreshold": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"description": "Mixed fleet with 6 agents across healthy, degrading, critical, and recovery states — 3 snapshots over time",
|
||||
"monitoringSnapshots": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 95,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 72,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 75,
|
||||
"status": "degrading",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 35,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "not_ready",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"stabilityScore": 40,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 2
|
||||
},
|
||||
{
|
||||
"agentId": "claw-canary",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 88,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 92,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "mock-staging",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 60,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 65,
|
||||
"status": "degrading",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 20,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "not_ready",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"stabilityScore": 25,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 3,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 2
|
||||
},
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 96,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 65,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 68,
|
||||
"status": "degrading",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 1
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 30,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "not_ready",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"stabilityScore": 35,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 3,
|
||||
"compatibilityFailures": 3,
|
||||
"releaseFailures": 3
|
||||
},
|
||||
{
|
||||
"agentId": "claw-canary",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 90,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 94,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "mock-staging",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 55,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 58,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 1
|
||||
},
|
||||
{
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 15,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "not_ready",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"stabilityScore": 20,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 4,
|
||||
"compatibilityFailures": 3,
|
||||
"releaseFailures": 3
|
||||
},
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"healthScore": 97,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 100,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "claw-staging",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"healthScore": 58,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 60,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 2,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 2
|
||||
},
|
||||
{
|
||||
"agentId": "claw-canary",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"healthScore": 92,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 96,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"criticalHealthDropPercent": 30,
|
||||
"highHealthDropPercent": 15,
|
||||
"trendWindowSize": 3,
|
||||
"consecutiveFailureThreshold": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"description": "Agent with sudden rollback spike — stable for most history then abrupt increase in rollbacks",
|
||||
"monitoringSnapshots": [
|
||||
{
|
||||
"agentId": "unstable-mock",
|
||||
"timestamp": "2026-06-01T00:00:00.000Z",
|
||||
"healthScore": 85,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 90,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "unstable-mock",
|
||||
"timestamp": "2026-06-02T00:00:00.000Z",
|
||||
"healthScore": 82,
|
||||
"riskLevel": "low",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 88,
|
||||
"status": "healthy",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 0,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "unstable-mock",
|
||||
"timestamp": "2026-06-03T00:00:00.000Z",
|
||||
"healthScore": 75,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "production_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 78,
|
||||
"status": "degrading",
|
||||
"releaseResult": "success",
|
||||
"rollbacks": 1,
|
||||
"compatibilityFailures": 0,
|
||||
"releaseFailures": 0
|
||||
},
|
||||
{
|
||||
"agentId": "unstable-mock",
|
||||
"timestamp": "2026-06-04T00:00:00.000Z",
|
||||
"healthScore": 55,
|
||||
"riskLevel": "medium",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "supported",
|
||||
"stabilityScore": 58,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 3,
|
||||
"compatibilityFailures": 1,
|
||||
"releaseFailures": 1
|
||||
},
|
||||
{
|
||||
"agentId": "unstable-mock",
|
||||
"timestamp": "2026-06-05T00:00:00.000Z",
|
||||
"healthScore": 40,
|
||||
"riskLevel": "high",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"stabilityScore": 42,
|
||||
"status": "at_risk",
|
||||
"releaseResult": "failed",
|
||||
"rollbacks": 5,
|
||||
"compatibilityFailures": 2,
|
||||
"releaseFailures": 2
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"criticalHealthDropPercent": 30,
|
||||
"highHealthDropPercent": 15,
|
||||
"trendWindowSize": 3,
|
||||
"consecutiveFailureThreshold": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"description": "All agents healthy — should certify as production_ready",
|
||||
"healthReport": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"healthScore": 100,
|
||||
"riskLevel": "low",
|
||||
"scoreBreakdown": {"release":30,"compatibility":25,"certification":20,"baseline":15,"operations":10},
|
||||
"stability": 100,
|
||||
"status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "mock-dev",
|
||||
"healthScore": 95,
|
||||
"riskLevel": "low",
|
||||
"scoreBreakdown": {"release":30,"compatibility":25,"certification":20,"baseline":15,"operations":5},
|
||||
"stability": 90,
|
||||
"status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"riskReport": [
|
||||
{"agentId":"claw-prod","riskLevel":"low","healthScore":100,"reasons":["all indicators healthy"]},
|
||||
{"agentId":"mock-dev","riskLevel":"low","healthScore":95,"reasons":["all indicators healthy"]}
|
||||
],
|
||||
"trendReport": [
|
||||
{"agentId":"claw-prod","trend":"stable","delta":0,"currentScore":100,"previousScore":100},
|
||||
{"agentId":"mock-dev","trend":"stable","delta":0,"currentScore":95,"previousScore":95}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"description": "Conditionally ready fleet: meets ≥70% threshold but not ≥90%",
|
||||
"healthReport": [
|
||||
{
|
||||
"agentId": "prod-a", "healthScore": 95, "riskLevel": "low",
|
||||
"scoreBreakdown": {"release":30,"compatibility":25,"certification":20,"baseline":15,"operations":5},
|
||||
"stability": 92, "status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "prod-b", "healthScore": 90, "riskLevel": "low",
|
||||
"scoreBreakdown": {"release":30,"compatibility":25,"certification":20,"baseline":10,"operations":5},
|
||||
"stability": 85, "status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "prod-c", "healthScore": 85, "riskLevel": "low",
|
||||
"scoreBreakdown": {"release":25,"compatibility":25,"certification":20,"baseline":10,"operations":5},
|
||||
"stability": 88, "status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "warn-d", "healthScore": 55, "riskLevel": "medium",
|
||||
"scoreBreakdown": {"release":15,"compatibility":15,"certification":10,"baseline":10,"operations":5},
|
||||
"stability": 70, "status": "degrading",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"riskReport": [
|
||||
{"agentId":"prod-a","riskLevel":"low","healthScore":95,"reasons":["all indicators healthy"]},
|
||||
{"agentId":"prod-b","riskLevel":"low","healthScore":90,"reasons":["all indicators healthy"]},
|
||||
{"agentId":"prod-c","riskLevel":"low","healthScore":85,"reasons":["all indicators healthy"]},
|
||||
{"agentId":"warn-d","riskLevel":"medium","healthScore":55,"reasons":["compatibility has warnings","certification conditionally ready"]}
|
||||
],
|
||||
"trendReport": [
|
||||
{"agentId":"prod-a","trend":"stable","delta":0,"currentScore":95,"previousScore":95},
|
||||
{"agentId":"prod-b","trend":"stable","delta":0,"currentScore":90,"previousScore":90},
|
||||
{"agentId":"prod-c","trend":"stable","delta":0,"currentScore":85,"previousScore":85},
|
||||
{"agentId":"warn-d","trend":"degrading","delta":-15,"currentScore":55,"previousScore":65}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"description": "High risk fleet — most agents at high risk, should be not_ready",
|
||||
"healthReport": [
|
||||
{
|
||||
"agentId": "unsupported-legacy",
|
||||
"healthScore": 5, "riskLevel": "high",
|
||||
"scoreBreakdown": {"release":0,"compatibility":0,"certification":0,"baseline":5,"operations":0},
|
||||
"stability": 30, "status": "at_risk",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "failing-agent",
|
||||
"healthScore": 15, "riskLevel": "high",
|
||||
"scoreBreakdown": {"release":0,"compatibility":5,"certification":0,"baseline":5,"operations":5},
|
||||
"stability": 40, "status": "degrading",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"riskReport": [
|
||||
{"agentId":"unsupported-legacy","riskLevel":"high","healthScore":5,"reasons":["agent unsupported","compatibility unsupported","3 release failures"]},
|
||||
{"agentId":"failing-agent","riskLevel":"high","healthScore":15,"reasons":["certification not_ready","2 release failures","2 rollbacks"]}
|
||||
],
|
||||
"trendReport": [
|
||||
{"agentId":"unsupported-legacy","trend":"degrading","delta":-80,"currentScore":5,"previousScore":25},
|
||||
{"agentId":"failing-agent","trend":"degrading","delta":-70,"currentScore":15,"previousScore":50}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"description": "Mixed fleet: healthy, degrading, high risk agents — conditionally_ready",
|
||||
"healthReport": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"healthScore": 100, "riskLevel": "low",
|
||||
"scoreBreakdown": {"release":30,"compatibility":25,"certification":20,"baseline":15,"operations":10},
|
||||
"stability": 100, "status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "mock-staging",
|
||||
"healthScore": 65, "riskLevel": "medium",
|
||||
"scoreBreakdown": {"release":20,"compatibility":15,"certification":10,"baseline":15,"operations":5},
|
||||
"stability": 78, "status": "degrading",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"healthScore": 10, "riskLevel": "high",
|
||||
"scoreBreakdown": {"release":0,"compatibility":0,"certification":0,"baseline":5,"operations":5},
|
||||
"stability": 50, "status": "at_risk",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"riskReport": [
|
||||
{"agentId":"claw-prod","riskLevel":"low","healthScore":100,"reasons":["all indicators healthy"]},
|
||||
{"agentId":"mock-staging","riskLevel":"medium","healthScore":65,"reasons":["compatibility has warnings","certification is conditionally ready"]},
|
||||
{"agentId":"old-claw-beta","riskLevel":"high","healthScore":10,"reasons":["agent unsupported","compatibility unsupported","certification not_ready"]}
|
||||
],
|
||||
"trendReport": [
|
||||
{"agentId":"claw-prod","trend":"stable","delta":0,"currentScore":100,"previousScore":100},
|
||||
{"agentId":"mock-staging","trend":"degrading","delta":-35,"currentScore":65,"previousScore":100},
|
||||
{"agentId":"old-claw-beta","trend":"degrading","delta":-50,"currentScore":10,"previousScore":20}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"description": "Not ready fleet: too many failing agents",
|
||||
"healthReport": [
|
||||
{
|
||||
"agentId": "fail-1", "healthScore": 10, "riskLevel": "high",
|
||||
"scoreBreakdown": {"release":0,"compatibility":0,"certification":0,"baseline":5,"operations":5},
|
||||
"stability": 25, "status": "at_risk",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "ok-2", "healthScore": 80, "riskLevel": "low",
|
||||
"scoreBreakdown": {"release":25,"compatibility":25,"certification":20,"baseline":5,"operations":5},
|
||||
"stability": 75, "status": "healthy",
|
||||
"lastUpdated": "2026-06-05T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"riskReport": [
|
||||
{"agentId":"fail-1","riskLevel":"high","healthScore":10,"reasons":["agent unsupported","compatibility unsupported","4 release failures"]},
|
||||
{"agentId":"ok-2","riskLevel":"low","healthScore":80,"reasons":["all indicators healthy"]}
|
||||
],
|
||||
"trendReport": [
|
||||
{"agentId":"fail-1","trend":"degrading","delta":-50,"currentScore":10,"previousScore":20},
|
||||
{"agentId":"ok-2","trend":"stable","delta":0,"currentScore":80,"previousScore":80}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"description": "Empty input — no alerts, no existing incidents",
|
||||
"alerts": [],
|
||||
"existingIncidents": [],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"description": "Existing incidents with various lifecycle states for state transition testing",
|
||||
"alerts": [
|
||||
{"alertId":"monitor-010","severity":"critical","agentId":"claw-prod","reason":"consecutive_release_failure","details":"4 failures","timestamp":"2026-06-05T00:00:00.000Z","source":"continuous-monitoring"}
|
||||
],
|
||||
"existingIncidents": [
|
||||
{"incidentId":"inc-001","agentId":"claw-prod","severity":"critical","status":"open","source":"compatibility_unsupported","createdAt":"2026-06-03T00:00:00.000Z","updatedAt":"2026-06-03T00:00:00.000Z","resolvedAt":null},
|
||||
{"incidentId":"inc-002","agentId":"claw-prod","severity":"critical","status":"investigating","source":"health_score_drop_critical","createdAt":"2026-06-03T01:00:00.000Z","updatedAt":"2026-06-03T02:00:00.000Z","resolvedAt":null},
|
||||
{"incidentId":"inc-003","agentId":"claw-prod","severity":"high","status":"recovering","source":"health_score_drop_high","createdAt":"2026-06-02T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":null,"recoveryProgress":60},
|
||||
{"incidentId":"inc-004","agentId":"old-claw-beta","severity":"critical","status":"resolved","source":"compatibility_unsupported","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-03T00:00:00.000Z","resolvedAt":"2026-06-03T00:00:00.000Z"},
|
||||
{"incidentId":"inc-005","agentId":"old-claw-beta","severity":"high","status":"closed","source":"health_score_drop_high","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":"2026-06-04T00:00:00.000Z"}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"description": "Mixed fleet incidents for comprehensive report generation",
|
||||
"alerts": [
|
||||
{"alertId":"monitor-030","severity":"critical","agentId":"claw-prod","reason":"compatibility_unsupported","details":"Compat unsupported on prod","timestamp":"2026-06-05T00:00:00.000Z","source":"continuous-monitoring"},
|
||||
{"alertId":"monitor-031","severity":"high","agentId":"mock-staging","reason":"health_score_drop_high","details":"Score dropped 20%","timestamp":"2026-06-05T01:00:00.000Z","source":"continuous-monitoring"}
|
||||
],
|
||||
"existingIncidents": [
|
||||
{"incidentId":"inc-030","agentId":"claw-prod","severity":"critical","status":"open","source":"health_score_drop_critical","createdAt":"2026-06-04T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":null},
|
||||
{"incidentId":"inc-031","agentId":"claw-staging","severity":"high","status":"recovering","source":"health_score_drop_high","createdAt":"2026-06-03T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":null,"recoveryProgress":60},
|
||||
{"incidentId":"inc-032","agentId":"old-claw-beta","severity":"critical","status":"open","source":"consecutive_release_failure","createdAt":"2026-06-03T00:00:00.000Z","updatedAt":"2026-06-03T00:00:00.000Z","resolvedAt":null},
|
||||
{"incidentId":"inc-033","agentId":"old-claw-beta","severity":"critical","status":"resolved","source":"compatibility_unsupported","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-02T00:00:00.000Z","resolvedAt":"2026-06-02T00:00:00.000Z"},
|
||||
{"incidentId":"inc-034","agentId":"old-claw-beta","severity":"high","status":"resolved","source":"health_score_drop_high","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-03T00:00:00.000Z","resolvedAt":"2026-06-03T00:00:00.000Z"},
|
||||
{"incidentId":"inc-035","agentId":"old-claw-beta","severity":"critical","status":"closed","source":"consecutive_release_failure","createdAt":"2026-05-30T00:00:00.000Z","updatedAt":"2026-06-01T00:00:00.000Z","resolvedAt":"2026-06-01T00:00:00.000Z"},
|
||||
{"incidentId":"inc-036","agentId":"unknown-sdk-v1","severity":"critical","status":"open","source":"compatibility_unsupported","createdAt":"2026-06-02T00:00:00.000Z","updatedAt":"2026-06-02T00:00:00.000Z","resolvedAt":null}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"description": "Multiple critical incidents from monitoring alerts",
|
||||
"alerts": [
|
||||
{"alertId":"monitor-001","severity":"critical","agentId":"claw-prod","reason":"health_score_drop_critical","details":"Score dropped 35%","timestamp":"2026-06-04T00:00:00.000Z","source":"continuous-monitoring"},
|
||||
{"alertId":"monitor-002","severity":"critical","agentId":"claw-prod","reason":"compatibility_unsupported","details":"Compat unsupported","timestamp":"2026-06-04T01:00:00.000Z","source":"continuous-monitoring"},
|
||||
{"alertId":"monitor-003","severity":"critical","agentId":"old-claw-beta","reason":"consecutive_release_failure","details":"3 consecutive failures","timestamp":"2026-06-04T02:00:00.000Z","source":"continuous-monitoring"},
|
||||
{"alertId":"monitor-004","severity":"critical","agentId":"unknown-sdk-v1","reason":"certification_downgraded_to_not_ready","details":"Cert downgraded","timestamp":"2026-06-04T03:00:00.000Z","source":"continuous-monitoring"},
|
||||
{"alertId":"monitor-005","severity":"high","agentId":"claw-staging","reason":"health_score_drop_high","details":"Score dropped 18%","timestamp":"2026-06-04T04:00:00.000Z","source":"continuous-monitoring"}
|
||||
],
|
||||
"existingIncidents": [],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"description": "Recovery tracking test — incidents in recovering state with progress values",
|
||||
"alerts": [],
|
||||
"existingIncidents": [
|
||||
{"incidentId":"inc-010","agentId":"claw-prod","severity":"critical","status":"recovering","source":"compatibility_unsupported","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":null,"recoveryProgress":30},
|
||||
{"incidentId":"inc-011","agentId":"claw-staging","severity":"high","status":"recovering","source":"health_score_drop_high","createdAt":"2026-06-02T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":null,"recoveryProgress":75},
|
||||
{"incidentId":"inc-012","agentId":"old-claw-beta","severity":"critical","status":"recovering","source":"consecutive_release_failure","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-03T00:00:00.000Z","resolvedAt":null,"recoveryProgress":100},
|
||||
{"incidentId":"inc-013","agentId":"unknown-sdk-v1","severity":"critical","status":"resolved","source":"compatibility_unsupported","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":"2026-06-04T00:00:00.000Z"},
|
||||
{"incidentId":"inc-014","agentId":"unknown-sdk-v1","severity":"high","status":"resolved","source":"health_score_drop_high","createdAt":"2026-06-02T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":"2026-06-04T12:00:00.000Z"}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"description": "Agent with many incidents — triggers repeat offender detection (≥3 incidents per agent)",
|
||||
"alerts": [
|
||||
{"alertId":"monitor-020","severity":"critical","agentId":"old-claw-beta","reason":"compatibility_unsupported","details":"test","timestamp":"2026-06-05T00:00:00.000Z","source":"continuous-monitoring"}
|
||||
],
|
||||
"existingIncidents": [
|
||||
{"incidentId":"inc-020","agentId":"old-claw-beta","severity":"critical","status":"open","source":"health_score_drop_critical","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-01T00:00:00.000Z","resolvedAt":null},
|
||||
{"incidentId":"inc-021","agentId":"old-claw-beta","severity":"critical","status":"resolved","source":"consecutive_release_failure","createdAt":"2026-06-02T00:00:00.000Z","updatedAt":"2026-06-03T00:00:00.000Z","resolvedAt":"2026-06-03T00:00:00.000Z"},
|
||||
{"incidentId":"inc-022","agentId":"old-claw-beta","severity":"high","status":"resolved","source":"health_score_drop_high","createdAt":"2026-06-03T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":"2026-06-04T00:00:00.000Z"},
|
||||
{"incidentId":"inc-023","agentId":"claw-prod","severity":"critical","status":"resolved","source":"compatibility_unsupported","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-02T00:00:00.000Z","resolvedAt":"2026-06-02T00:00:00.000Z"},
|
||||
{"incidentId":"inc-024","agentId":"claw-prod","severity":"high","status":"closed","source":"health_score_drop_high","createdAt":"2026-06-01T00:00:00.000Z","updatedAt":"2026-06-04T00:00:00.000Z","resolvedAt":"2026-06-04T00:00:00.000Z"}
|
||||
],
|
||||
"configuration": {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"description": "All agents healthy — every agent is low risk",
|
||||
"registry": {
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "mock-dev",
|
||||
"agentType": "mock",
|
||||
"displayName": "Mock Development",
|
||||
"status": "active",
|
||||
"owner": "dev-team",
|
||||
"workspacePath": "test/fixtures/agent-adapters/mock-agent",
|
||||
"adapter": "mock",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-03T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"releaseHistory": [
|
||||
{"releaseId":"rel-001","agentId":"claw-prod","result":"success","gateDecision":"open","compatibilityStatus":"supported","timestamp":"2026-06-04T00:00:00.000Z"},
|
||||
{"releaseId":"rel-002","agentId":"claw-prod","result":"success","gateDecision":"open","compatibilityStatus":"supported","timestamp":"2026-06-03T00:00:00.000Z"},
|
||||
{"releaseId":"rel-003","agentId":"mock-dev","result":"success","gateDecision":"open","compatibilityStatus":"supported","timestamp":"2026-06-02T00:00:00.000Z"}
|
||||
],
|
||||
"certifications": {
|
||||
"claw-prod": {"status":"production_ready","governanceGrade":"A","governanceScore":100,"riskLevel":"medium","certifiedAt":"2026-06-04T00:00:00.000Z","baselineVersion":"v4"},
|
||||
"mock-dev": {"status":"production_ready","governanceGrade":"A","governanceScore":95,"riskLevel":"low","certifiedAt":"2026-06-03T00:00:00.000Z","baselineVersion":"v1"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"description": "Key data missing — certification and release history absent",
|
||||
"registry": {
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "new-agent",
|
||||
"agentType": "mock",
|
||||
"displayName": "New Uncertified Agent",
|
||||
"status": "active",
|
||||
"owner": "dev-team",
|
||||
"workspacePath": "test/fixtures/agent-adapters/mock-agent",
|
||||
"adapter": "mock",
|
||||
"baselineVersion": "",
|
||||
"compatibilityStatus": "unknown",
|
||||
"lastCertifiedAt": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"releaseHistory": [
|
||||
{"releaseId":"rel-001","agentId":"claw-prod","result":"success","gateDecision":"open","compatibilityStatus":"supported","timestamp":"2026-06-04T00:00:00.000Z"}
|
||||
],
|
||||
"certifications": {
|
||||
"claw-prod": {"status":"production_ready","governanceGrade":"A","governanceScore":100,"riskLevel":"low","certifiedAt":"2026-06-04T00:00:00.000Z","baselineVersion":"v4"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"description": "Mixed risk: low, medium, high, unknown agents",
|
||||
"registry": {
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"lastCertifiedAt": "2026-06-04T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "mock-staging",
|
||||
"agentType": "mock",
|
||||
"displayName": "Mock Staging",
|
||||
"status": "active",
|
||||
"owner": "dev-team",
|
||||
"workspacePath": "test/fixtures/agent-adapters/mock-agent",
|
||||
"adapter": "mock",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"lastCertifiedAt": "2026-06-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"agentType": "claw",
|
||||
"displayName": "Old Claw Beta",
|
||||
"status": "unsupported",
|
||||
"owner": "team-memory",
|
||||
"workspacePath": "legacy-memory/",
|
||||
"adapter": "claw",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"lastCertifiedAt": "2025-06-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"agentType": "unknown",
|
||||
"displayName": "Unknown SDK Agent",
|
||||
"status": "unsupported",
|
||||
"owner": "external-team",
|
||||
"workspacePath": "legacy-data/",
|
||||
"adapter": "none",
|
||||
"baselineVersion": "unknown",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"lastCertifiedAt": "2025-12-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"releaseHistory": [
|
||||
{"releaseId":"rel-001","agentId":"claw-prod","result":"success","gateDecision":"open","compatibilityStatus":"supported","timestamp":"2026-06-04T00:00:00.000Z"},
|
||||
{"releaseId":"rel-002","agentId":"claw-prod","result":"success","gateDecision":"open","compatibilityStatus":"supported","timestamp":"2026-06-03T00:00:00.000Z"},
|
||||
{"releaseId":"rel-003","agentId":"mock-staging","result":"success","gateDecision":"warn","compatibilityStatus":"compatible_with_warnings","timestamp":"2026-06-02T00:00:00.000Z"},
|
||||
{"releaseId":"rel-004","agentId":"old-claw-beta","result":"failed","gateDecision":"closed","compatibilityStatus":"unsupported","timestamp":"2025-06-01T00:00:00.000Z"}
|
||||
],
|
||||
"certifications": {
|
||||
"claw-prod": {"status":"production_ready","governanceGrade":"A","governanceScore":100,"riskLevel":"medium","certifiedAt":"2026-06-04T00:00:00.000Z","baselineVersion":"v4"},
|
||||
"mock-staging": {"status":"conditionally_ready","governanceGrade":"B","governanceScore":75,"riskLevel":"medium","certifiedAt":"2026-06-01T00:00:00.000Z","baselineVersion":"v1"},
|
||||
"old-claw-beta": {"status":"not_ready","governanceGrade":"F","governanceScore":20,"riskLevel":"high","certifiedAt":"2025-06-01T00:00:00.000Z","baselineVersion":"v1"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"description": "Expected output for multi-agent dashboard with mixed risk",
|
||||
"dashboard": [
|
||||
{
|
||||
"agentId": "claw-prod",
|
||||
"agentType": "claw",
|
||||
"displayName": "Claw Production",
|
||||
"status": "active",
|
||||
"baselineVersion": "v4",
|
||||
"compatibilityStatus": "supported",
|
||||
"certificationStatus": "production_ready",
|
||||
"lastReleaseResult": "success",
|
||||
"riskLevel": "low"
|
||||
},
|
||||
{
|
||||
"agentId": "mock-staging",
|
||||
"agentType": "mock",
|
||||
"displayName": "Mock Staging",
|
||||
"status": "active",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "compatible_with_warnings",
|
||||
"certificationStatus": "conditionally_ready",
|
||||
"lastReleaseResult": "success",
|
||||
"riskLevel": "medium"
|
||||
},
|
||||
{
|
||||
"agentId": "old-claw-beta",
|
||||
"agentType": "claw",
|
||||
"displayName": "Old Claw Beta",
|
||||
"status": "unsupported",
|
||||
"baselineVersion": "v1",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "not_ready",
|
||||
"lastReleaseResult": "failed",
|
||||
"riskLevel": "high"
|
||||
},
|
||||
{
|
||||
"agentId": "unknown-sdk-v1",
|
||||
"agentType": "unknown",
|
||||
"displayName": "Unknown SDK Agent",
|
||||
"status": "unsupported",
|
||||
"baselineVersion": "unknown",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"certificationStatus": "unknown",
|
||||
"lastReleaseResult": "unknown",
|
||||
"riskLevel": "high"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"totalAgents": 4,
|
||||
"activeAgents": 2,
|
||||
"productionReadyAgents": 1,
|
||||
"unsupportedAgents": 2,
|
||||
"highRiskAgents": 2,
|
||||
"unknownRiskAgents": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"description": "Single unsupported agent — must be high risk",
|
||||
"registry": {
|
||||
"schemaVersion": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "unsupported-legacy",
|
||||
"agentType": "unknown",
|
||||
"displayName": "Unsupported Legacy Agent",
|
||||
"status": "unsupported",
|
||||
"owner": "external-team",
|
||||
"workspacePath": "legacy-data/",
|
||||
"adapter": "none",
|
||||
"baselineVersion": "unknown",
|
||||
"compatibilityStatus": "unsupported",
|
||||
"lastCertifiedAt": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"releaseHistory": [
|
||||
{"releaseId":"rel-001","agentId":"unsupported-legacy","result":"failed","gateDecision":"closed","compatibilityStatus":"unsupported","timestamp":"2025-01-01T00:00:00.000Z"}
|
||||
],
|
||||
"certifications": {
|
||||
"unsupported-legacy": {"status":"not_ready","governanceGrade":"F","governanceScore":10,"riskLevel":"high","certifiedAt":"2025-01-01T00:00:00.000Z","baselineVersion":"unknown"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "full-pass",
|
||||
"description": "All governance components present and certified",
|
||||
"state": {
|
||||
"baselineRotationDir": "../baseline-rotation",
|
||||
"compatMatrixDir": "../compatibility-matrix",
|
||||
"releaseHistoryDir": "../../../release-history",
|
||||
"baselineEvolutionDir": "../baseline-evolution",
|
||||
"productionValidationDir": "../production-validation"
|
||||
},
|
||||
"expected": {
|
||||
"coverageScore": 100,
|
||||
"checklistPass": 6,
|
||||
"grade": "A"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user