import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { renderDocsHeadingMap } from "../../scripts/docs-list.js"; import { restorePrepackArtifacts } from "../../scripts/openclaw-postpack.mjs"; import { preparePackageChangelog } from "../../scripts/package-changelog.mjs"; import { preparePackageDocsMap, restorePackageDocsMap } from "../../scripts/package-docs-map.mjs"; import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js"; const tempDirs: string[] = []; const sourceChangelog = `# Changelog ## 2026.8.1 - Current release notes with enough detail for package validation. ## 2026.7.1 - Previous release notes with enough detail for package validation. `; function makePackageRoot(): string { const root = makeTempDir(tempDirs, "openclaw-package-docs-map-"); mkdirSync(path.join(root, "docs"), { recursive: true }); writeFileSync(path.join(root, "docs", "page.md"), "# Package docs\n", "utf8"); writeFileSync(path.join(root, "package.json"), '{"name":"openclaw","version":"2026.8.1"}\n'); writeFileSync(path.join(root, "CHANGELOG.md"), sourceChangelog); return root; } afterEach(() => { cleanupTempDirs(tempDirs); }); describe("package docs map", () => { it("materializes exact generated bytes and removes only its transient copy", async () => { const root = makePackageRoot(); const mapPath = path.join(root, "docs", "docs_map.md"); await expect(preparePackageDocsMap(root)).resolves.toBe(true); expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs"))); await expect(restorePackageDocsMap(root)).resolves.toBe(true); expect(existsSync(mapPath)).toBe(false); }); it("preserves an identical map that existed before packaging", async () => { const root = makePackageRoot(); const mapPath = path.join(root, "docs", "docs_map.md"); const content = renderDocsHeadingMap(path.join(root, "docs")); writeFileSync(mapPath, content, "utf8"); await expect(preparePackageDocsMap(root)).resolves.toBe(false); await expect(restorePackageDocsMap(root)).resolves.toBe(false); expect(readFileSync(mapPath, "utf8")).toBe(content); }); it("restores a tracked source stub after packaging", async () => { const root = makePackageRoot(); const mapPath = path.join(root, "docs", "docs_map.md"); const stub = "# Docs map source\n"; writeFileSync(mapPath, stub, "utf8"); await expect(preparePackageDocsMap(root)).resolves.toBe(true); expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs"))); await expect(restorePackageDocsMap(root)).resolves.toBe(true); expect(readFileSync(mapPath, "utf8")).toBe(stub); }); it("serializes concurrent package preparations with the receipt", async () => { const root = makePackageRoot(); const mapPath = path.join(root, "docs", "docs_map.md"); const stub = "# Docs map source\n"; writeFileSync(mapPath, stub, "utf8"); const results = await Promise.allSettled([ preparePackageDocsMap(root), preparePackageDocsMap(root), ]); expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); const rejected = results.find((result) => result.status === "rejected"); expect(rejected).toMatchObject({ reason: expect.objectContaining({ code: "PACKAGE_DOCS_MAP_ACTIVE", message: expect.stringContaining("node scripts/openclaw-postpack.mjs"), }), }); expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs"))); await expect(restorePackageDocsMap(root)).resolves.toBe(true); expect(readFileSync(mapPath, "utf8")).toBe(stub); }); it("refuses to remove a transient map changed after prepack", async () => { const root = makePackageRoot(); const mapPath = path.join(root, "docs", "docs_map.md"); await preparePackageDocsMap(root); writeFileSync(mapPath, "operator change\n", "utf8"); await expect(restorePackageDocsMap(root)).rejects.toThrow("changed after prepack"); expect(readFileSync(mapPath, "utf8")).toBe("operator change\n"); }); it("keeps the lifecycle lock until interrupted changelog state is restored", async () => { const root = makePackageRoot(); const mapPath = path.join(root, "docs", "docs_map.md"); const receiptPath = path.join(root, ".artifacts", "package-docs-map", "receipt.json"); const changelogBackupPath = path.join( root, ".artifacts", "package-changelog", "CHANGELOG.md.prepack-backup", ); const stub = "# Docs map source\n"; writeFileSync(mapPath, stub); await preparePackageDocsMap(root); await preparePackageChangelog(root); const packagedChangelog = readFileSync(path.join(root, "CHANGELOG.md"), "utf8"); writeFileSync(path.join(root, "CHANGELOG.md"), "operator change\n"); await expect(restorePrepackArtifacts(root)).rejects.toThrow("changed since the backup"); expect(existsSync(receiptPath)).toBe(true); expect(existsSync(changelogBackupPath)).toBe(true); expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs"))); writeFileSync(path.join(root, "CHANGELOG.md"), packagedChangelog); await expect(restorePrepackArtifacts(root)).resolves.toBeUndefined(); expect(readFileSync(path.join(root, "CHANGELOG.md"), "utf8")).toBe(sourceChangelog); expect(readFileSync(mapPath, "utf8")).toBe(stub); expect(existsSync(changelogBackupPath)).toBe(false); expect(existsSync(receiptPath)).toBe(false); }); });