/** * 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"); }); });