mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 12:41:33 +00:00
* docs: generate docs map at publish time * fix: canonicalize Docker pack entry path * fix: preserve package lifecycle ownership
132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|