mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
* fix(packaging): preserve markers across package lifecycle handoffs Create the distribution inventory only after the existing package receipt is acquired, and remove lifecycle markers before releasing that receipt. Rejected contenders and finishing owners must not mutate the next package's marker. Keep smoke validation before acquisition and retain its existing failure behavior. Cover contention, cleanup faults, successful and failed handoffs, selected plugin restoration, and the frozen source writer. Production delta is zero. * fix(packaging): clean up bundled channel smoke layouts Return the child status from the temporary-layout owner so its finally cleanup runs before the CLI exits. Cover setup in the same lifetime and keep installed-layout selection and child failure behavior unchanged. Extend the maintained fixture across source and installed layouts, checking exit behavior, unchanged package bytes and caller-owned temporary siblings.
151 lines
6.3 KiB
TypeScript
151 lines
6.3 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, rmSync, 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("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.each(["source stub", "generated map"])(
|
|
"serializes preparations starting from a %s",
|
|
async (initialMap) => {
|
|
const root = makePackageRoot();
|
|
const mapPath = path.join(root, "docs", "docs_map.md");
|
|
const original =
|
|
initialMap === "generated map"
|
|
? renderDocsHeadingMap(path.join(root, "docs"))
|
|
: "# Docs map source\n";
|
|
writeFileSync(mapPath, original, "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(original);
|
|
},
|
|
);
|
|
|
|
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);
|
|
});
|
|
|
|
it.each([".openclaw-lifecycle-pending", "dist/openclaw-install-guard"])(
|
|
"retains the lifecycle lock when %s cannot be removed",
|
|
async (relativePath) => {
|
|
const root = makePackageRoot();
|
|
const markerPath = path.join(root, relativePath);
|
|
const receiptPath = path.join(root, ".artifacts/package-docs-map/receipt.json");
|
|
await preparePackageDocsMap(root);
|
|
await preparePackageChangelog(root);
|
|
mkdirSync(markerPath, { recursive: true });
|
|
writeFileSync(path.join(markerPath, "retain"), "filesystem fault fixture\n");
|
|
|
|
await expect(restorePrepackArtifacts(root)).rejects.toMatchObject({ code: "ERR_FS_EISDIR" });
|
|
expect(existsSync(receiptPath)).toBe(true);
|
|
await expect(preparePackageDocsMap(root)).rejects.toMatchObject({
|
|
code: "PACKAGE_DOCS_MAP_ACTIVE",
|
|
});
|
|
|
|
rmSync(markerPath, { recursive: true });
|
|
await restorePrepackArtifacts(root);
|
|
expect(existsSync(receiptPath)).toBe(false);
|
|
expect(readFileSync(path.join(root, "CHANGELOG.md"), "utf8")).toBe(sourceChangelog);
|
|
},
|
|
);
|
|
});
|