Files
16gagent/test/release-builder-agent.test.mjs
2026-06-06 10:40:48 +08:00

499 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Release Builder Agent Test — SF-07
*
* 验证 Release Package 生成器的所有关键功能。
*/
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, readdirSync, statSync } from "node:fs";
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
let buildRelease, writeRelease;
before(async () => {
const mod = await import("../scripts/release-builder-agent.mjs");
buildRelease = mod.buildRelease;
writeRelease = mod.writeRelease;
});
// ═══════════════════════════════════════════════════════
// 1 — Release 目录结构生成
// ═══════════════════════════════════════════════════════
describe("1 — Release 目录结构生成", () => {
let result;
before(() => {
result = buildRelease({ 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("包含 release/version.json", () => {
assert.ok(result.files["release/version.json"], "Missing version.json");
});
it("包含 release/manifests/manifest.json", () => {
assert.ok(result.files["release/manifests/manifest.json"], "Missing manifest.json");
});
it("包含 release/checksums/checksums.txt", () => {
assert.ok(result.files["release/checksums/checksums.txt"], "Missing checksums.txt");
});
it("包含 release/release-notes/release-notes.md", () => {
assert.ok(result.files["release/release-notes/release-notes.md"], "Missing release-notes.md");
});
it("包含 release/build-info.json", () => {
assert.ok(result.files["release/build-info.json"], "Missing build-info.json");
});
it("包含 release/README.md", () => {
assert.ok(result.files["release/README.md"], "Missing README.md");
});
});
// ═══════════════════════════════════════════════════════
// 2 — version.json 验证
// ═══════════════════════════════════════════════════════
describe("2 — version.json 验证", () => {
let result, version;
before(() => {
result = buildRelease({ projectName: "PetCare", version: "2.0.0", buildNumber: 5 });
version = JSON.parse(result.files["release/version.json"]);
});
it("包含正确名称", () => {
assert.equal(version.name, "petcare");
});
it("包含正确版本", () => {
assert.equal(version.version, "2.0.0");
});
it("包含正确构建号", () => {
assert.equal(version.buildNumber, 5);
});
it("包含所有平台", () => {
assert.deepEqual(version.platforms, ["windows", "macos", "linux"]);
});
it("默认版本是 1.0.0", () => {
const r = buildRelease({ projectName: "Test" });
const v = JSON.parse(r.files["release/version.json"]);
assert.equal(v.version, "1.0.0");
});
it("默认构建号是 1", () => {
const r = buildRelease({ projectName: "Test" });
const v = JSON.parse(r.files["release/version.json"]);
assert.equal(v.buildNumber, 1);
});
});
// ═══════════════════════════════════════════════════════
// 3 — manifest.json 验证
// ═══════════════════════════════════════════════════════
describe("3 — manifest.json 验证", () => {
let result, manifest;
before(() => {
result = buildRelease({ projectName: "CRM" });
manifest = JSON.parse(result.files["release/manifests/manifest.json"]);
});
it("包含项目名称", () => {
assert.equal(manifest.project, "CRM");
});
it("包含版本", () => {
assert.equal(manifest.version, "1.0.0");
});
it("包含生成时间", () => {
assert.ok(manifest.generatedAt, "Missing generatedAt");
assert.ok(!isNaN(new Date(manifest.generatedAt).getTime()), "Invalid date");
});
it("包含文件列表", () => {
assert.ok(Array.isArray(manifest.files), "files should be array");
assert.ok(manifest.files.length > 0, "files should not be empty");
});
it("每个文件包含 path, size, sha256", () => {
for (const f of manifest.files) {
assert.ok(f.path, "Missing path");
assert.ok(typeof f.size === "number", "Missing size");
assert.ok(f.sha256, "Missing sha256");
assert.ok(f.sha256.length === 64, "SHA256 should be 64 hex chars");
}
});
it("包含平台列表", () => {
assert.deepEqual(manifest.platforms, ["windows", "macos", "linux"]);
});
it("包含生成器版本", () => {
assert.ok(manifest.generatorVersion, "Missing generatorVersion");
});
it("totalFiles 与 files 数组长度一致", () => {
assert.equal(manifest.totalFiles, manifest.files.length);
});
it("totalSize 是所有文件大小之和", () => {
const expectedSize = manifest.files.reduce((s, f) => s + f.size, 0);
assert.equal(manifest.totalSize, expectedSize);
});
});
// ═══════════════════════════════════════════════════════
// 4 — checksums.txt 验证
// ═══════════════════════════════════════════════════════
describe("4 — checksums.txt 验证", () => {
let result, checksums;
before(() => {
result = buildRelease({ projectName: "Inventory" });
checksums = result.files["release/checksums/checksums.txt"];
});
it("非空", () => {
assert.ok(checksums.length > 0, "Empty checksums");
});
it("每行格式为 SHA256 filename", () => {
const lines = checksums.trim().split("\n");
for (const line of lines) {
const parts = line.split(/\s+/);
assert.ok(parts.length >= 2, `Invalid line: ${line}`);
assert.ok(parts[0].length === 64, `Invalid SHA256: ${parts[0]}`);
assert.ok(parts[1].length > 0, `Missing filename`);
}
});
it("行数与 manifest 文件数一致", () => {
const manifest = JSON.parse(result.files["release/manifests/manifest.json"]);
const lines = checksums.trim().split("\n");
assert.equal(lines.length, manifest.files.length);
});
it("SHA256 与 manifest 一致", () => {
const manifest = JSON.parse(result.files["release/manifests/manifest.json"]);
const lines = checksums.trim().split("\n");
for (let i = 0; i < lines.length; i++) {
const [sha, path] = lines[i].split(/\s+/);
assert.equal(sha, manifest.files[i].sha256, `SHA mismatch for ${path}`);
assert.equal(path, manifest.files[i].path, `Path mismatch at line ${i}`);
}
});
});
// ═══════════════════════════════════════════════════════
// 5 — release-notes.md 验证
// ═══════════════════════════════════════════════════════
describe("5 — release-notes.md 验证", () => {
let result, notes;
before(() => {
result = buildRelease({ projectName: "PetCare" });
notes = result.files["release/release-notes/release-notes.md"];
});
it("非空", () => {
assert.ok(notes.length > 0, "Empty release notes");
});
it("包含项目名称", () => {
assert.ok(notes.includes("PetCare"), "Missing project name");
});
it("包含版本信息", () => {
assert.ok(notes.includes("1.0.0"), "Missing version");
});
it("包含功能列表", () => {
assert.ok(notes.includes("Features"), "Missing features section");
});
it("包含构建时间", () => {
assert.ok(notes.includes("Build Time"), "Missing build time");
});
it("包含平台信息", () => {
assert.ok(notes.includes("windows"), "Missing windows");
assert.ok(notes.includes("macos"), "Missing macos");
assert.ok(notes.includes("linux"), "Missing linux");
});
it("包含安装方式", () => {
assert.ok(notes.includes("Installation"), "Missing installation section");
assert.ok(notes.includes(".exe"), "Missing Windows installer");
assert.ok(notes.includes(".dmg"), "Missing macOS DMG");
assert.ok(notes.includes(".AppImage"), "Missing Linux AppImage");
});
it("包含校验和引用", () => {
assert.ok(notes.includes("checksums"), "Missing checksums reference");
});
});
// ═══════════════════════════════════════════════════════
// 6 — build-info.json 验证
// ═══════════════════════════════════════════════════════
describe("6 — build-info.json 验证", () => {
let result, buildInfo;
before(() => {
result = buildRelease({ projectName: "CRM" });
buildInfo = JSON.parse(result.files["release/build-info.json"]);
});
it("包含 Node Version", () => {
assert.ok(buildInfo.nodeVersion, "Missing nodeVersion");
assert.ok(buildInfo.nodeVersion.startsWith("v"), "nodeVersion should start with v");
});
it("包含 Generator Version", () => {
assert.ok(buildInfo.generatorVersion, "Missing generatorVersion");
});
it("包含 Build Time", () => {
assert.ok(buildInfo.buildTime, "Missing buildTime");
assert.ok(!isNaN(new Date(buildInfo.buildTime).getTime()), "Invalid buildTime");
});
it("包含 Domain", () => {
assert.ok("domain" in buildInfo, "Missing domain");
});
it("包含 Entity Count", () => {
assert.ok("entityCount" in buildInfo, "Missing entityCount");
assert.ok(typeof buildInfo.entityCount === "number", "entityCount should be number");
});
it("包含 Route Count", () => {
assert.ok("routeCount" in buildInfo, "Missing routeCount");
assert.ok(typeof buildInfo.routeCount === "number", "routeCount should be number");
});
it("包含 Screen Count", () => {
assert.ok("screenCount" in buildInfo, "Missing screenCount");
assert.ok(typeof buildInfo.screenCount === "number", "screenCount should be number");
});
});
// ═══════════════════════════════════════════════════════
// 7 — Platform Artifact Layout
// ═══════════════════════════════════════════════════════
describe("7 — Platform Artifact Layout", () => {
let result;
before(() => {
result = buildRelease({ projectName: "PetCare" });
});
it("包含 Windows 目录文件", () => {
const winFiles = Object.keys(result.files).filter(p => p.startsWith("release/windows/"));
assert.ok(winFiles.length >= 2, `Expected >= 2 Windows files, got ${winFiles.length}`);
});
it("包含 macOS 目录文件", () => {
const macFiles = Object.keys(result.files).filter(p => p.startsWith("release/macos/"));
assert.ok(macFiles.length >= 2, `Expected >= 2 macOS files, got ${macFiles.length}`);
});
it("包含 Linux 目录文件", () => {
const linuxFiles = Object.keys(result.files).filter(p => p.startsWith("release/linux/"));
assert.ok(linuxFiles.length >= 2, `Expected >= 2 Linux files, got ${linuxFiles.length}`);
});
it("Windows 文件包含安装器占位", () => {
const winExe = Object.entries(result.files).find(([p]) => p.includes("windows") && p.includes("setup"));
assert.ok(winExe, "Missing Windows setup placeholder");
assert.ok(winExe[1].includes("PLACEHOLDER"), "Should be placeholder");
});
it("macOS 文件包含 DMG 占位", () => {
const macDmg = Object.entries(result.files).find(([p]) => p.includes("macos") && p.endsWith(".dmg"));
assert.ok(macDmg, "Missing macOS DMG placeholder");
assert.ok(macDmg[1].includes("PLACEHOLDER"), "Should be placeholder");
});
it("Linux 文件包含 AppImage 占位", () => {
const linuxApp = Object.entries(result.files).find(([p]) => p.includes("linux") && p.includes("AppImage"));
assert.ok(linuxApp, "Missing Linux AppImage placeholder");
assert.ok(linuxApp[1].includes("PLACEHOLDER"), "Should be placeholder");
});
it("所有平台文件非空", () => {
const platformFiles = Object.entries(result.files).filter(([p]) =>
p.startsWith("release/windows/") || p.startsWith("release/macos/") || p.startsWith("release/linux/")
);
for (const [path, content] of platformFiles) {
assert.ok(content.length > 0, `${path}: empty content`);
}
});
});
// ═══════════════════════════════════════════════════════
// 8 — 文件写入 I/O
// ═══════════════════════════════════════════════════════
describe("8 — 文件写入 I/O", () => {
it("writeRelease 写入完整目录树", () => {
const result = buildRelease({ projectName: "TestApp" });
const tmpDir = resolve(WORKSPACE, ".tmp-release-test");
writeRelease(result, tmpDir);
assert.ok(existsSync(resolve(tmpDir, "release", "version.json")));
assert.ok(existsSync(resolve(tmpDir, "release", "manifests", "manifest.json")));
assert.ok(existsSync(resolve(tmpDir, "release", "checksums", "checksums.txt")));
assert.ok(existsSync(resolve(tmpDir, "release", "release-notes", "release-notes.md")));
assert.ok(existsSync(resolve(tmpDir, "release", "build-info.json")));
assert.ok(existsSync(resolve(tmpDir, "release", "README.md")));
// Platform dirs
const winDir = resolve(tmpDir, "release", "windows");
const macDir = resolve(tmpDir, "release", "macos");
const linuxDir = resolve(tmpDir, "release", "linux");
assert.ok(existsSync(winDir), "Missing windows dir");
assert.ok(existsSync(macDir), "Missing macos dir");
assert.ok(existsSync(linuxDir), "Missing linux dir");
// Platform files
const winFiles = readdirSync(winDir);
assert.ok(winFiles.length >= 2, "Windows should have >= 2 files");
const macFiles = readdirSync(macDir);
assert.ok(macFiles.length >= 2, "macOS should have >= 2 files");
const linuxFiles = readdirSync(linuxDir);
assert.ok(linuxFiles.length >= 2, "Linux should have >= 2 files");
rmSync(tmpDir, { recursive: true, force: true });
});
it("所有生成的文件非空", () => {
const result = buildRelease();
for (const [path, content] of Object.entries(result.files)) {
assert.ok(content.length > 0, `${path}: empty content`);
}
});
});
// ═══════════════════════════════════════════════════════
// 9 — Stats 验证
// ═══════════════════════════════════════════════════════
describe("9 — Stats 验证", () => {
it("统计正确", () => {
const result = buildRelease({ projectName: "PetCare" });
const stats = result.stats;
assert.ok(stats.totalFiles > 0, "totalFiles should be > 0");
assert.ok(stats.platformFiles >= 6, "platformFiles should be >= 6 (2 per platform)");
assert.ok(stats.manifestFiles >= 1, "manifestFiles should be >= 1");
assert.ok(stats.checksumFiles >= 1, "checksumFiles should be >= 1");
assert.ok(stats.releaseNotesFiles >= 1, "releaseNotesFiles should be >= 1");
assert.ok(stats.metadataFiles >= 3, "metadataFiles should be >= 3 (version, manifest, build-info)");
assert.ok(stats.totalSize > 0, "totalSize should be > 0");
});
});
// ═══════════════════════════════════════════════════════
// 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 = buildRelease({ projectName: name });
assert.ok(!result.error, `${name}: ${result.message}`);
assert.ok(result.files["release/version.json"], `${name}: missing version.json`);
assert.ok(result.files["release/manifests/manifest.json"], `${name}: missing manifest.json`);
assert.ok(result.files["release/checksums/checksums.txt"], `${name}: missing checksums.txt`);
assert.ok(result.files["release/release-notes/release-notes.md"], `${name}: missing release-notes.md`);
assert.ok(result.files["release/build-info.json"], `${name}: missing build-info.json`);
});
it(`${name} — version.json 包含正确名称`, () => {
const result = buildRelease({ projectName: name });
const version = JSON.parse(result.files["release/version.json"]);
assert.equal(version.name, expected, `${name}: expected '${expected}', got '${version.name}'`);
});
it(`${name} — manifest 包含正确项目名`, () => {
const result = buildRelease({ projectName: name });
const manifest = JSON.parse(result.files["release/manifests/manifest.json"]);
assert.equal(manifest.project, name, `${name}: expected '${name}', got '${manifest.project}'`);
});
it(`${name} — 包含三个平台目录`, () => {
const result = buildRelease({ projectName: name });
const paths = Object.keys(result.files);
assert.ok(paths.some(p => p.startsWith("release/windows/")), `${name}: missing windows/`);
assert.ok(paths.some(p => p.startsWith("release/macos/")), `${name}: missing macos/`);
assert.ok(paths.some(p => p.startsWith("release/linux/")), `${name}: missing linux/`);
});
}
});
// ═══════════════════════════════════════════════════════
// 11 — PRD 集成(从项目目录读取元数据)
// ═══════════════════════════════════════════════════════
describe("11 — PRD 集成", () => {
it("从 prd.json 读取项目信息", () => {
const petcareDir = resolve(WORKSPACE, ".benchmark", "petcare");
if (!existsSync(resolve(petcareDir, "prd.json"))) return; // skip if no benchmark data
const result = buildRelease({ projectDir: petcareDir });
assert.ok(!result.error, `Error: ${result.error}`);
const version = JSON.parse(result.files["release/version.json"]);
assert.equal(version.name, "petcare", "Should read name from PRD");
const buildInfo = JSON.parse(result.files["release/build-info.json"]);
assert.ok(buildInfo.entityCount > 0, "Should have entities from PRD");
assert.ok(buildInfo.screenCount > 0, "Should have screens from PRD");
});
it("从 crm benchmark 读取元数据", () => {
const crmDir = resolve(WORKSPACE, ".benchmark", "crm");
if (!existsSync(resolve(crmDir, "prd.json"))) return;
const result = buildRelease({ projectDir: crmDir });
assert.ok(!result.error, `Error: ${result.error}`);
const buildInfo = JSON.parse(result.files["release/build-info.json"]);
assert.ok(buildInfo.entityCount > 0, "Should have entities");
});
it("从 inventory benchmark 读取元数据", () => {
const invDir = resolve(WORKSPACE, ".benchmark", "inventory");
if (!existsSync(resolve(invDir, "prd.json"))) return;
const result = buildRelease({ projectDir: invDir });
assert.ok(!result.error, `Error: ${result.error}`);
const buildInfo = JSON.parse(result.files["release/build-info.json"]);
assert.ok(buildInfo.entityCount > 0, "Should have entities");
});
});