SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
78d0c37 verified
Raw History Blame Contribute Delete
21.6 kB
import fs from "node:fs/promises";
import path from "node:path";
import * as tar from "tar";
import { describe, expect, it, vi } from "vitest";
import {
closeOpenClawStateDatabaseForTest,
openOpenClawStateDatabase,
} from "../../state/openclaw-state-db.js";
import { withTestDir } from "../../test-helpers/temp-dir.js";
import * as bundleStaging from "./bundle-staging.js";
import {
createWorkerBundleProducer,
resolveWorkerNpmInstallationArtifact,
type WorkerInstallationArtifact,
} from "./bundle.js";
import { createWorkerProjectPreparationIdentity } from "./preparation-identity.js";
import { createWorkerEnvironmentStore } from "./store.js";
import { listRetainedWorkerBundleHashes } from "./worker-bundle-retention.js";
import { workerWorkspaceRsyncReceiverEntryPath } from "./workspace-sync-helpers.js";
type WorkerBundleArtifact = Extract<WorkerInstallationArtifact, { install: "bundle" }>;
async function writeFixture(
packageRoot: string,
workerSource = "export const worker = true;\n",
): Promise<void> {
await fs.mkdir(path.join(packageRoot, "dist", "worker"), { recursive: true });
await fs.writeFile(
path.join(packageRoot, "package.json"),
`${JSON.stringify({
name: "openclaw",
version: "1.2.3",
type: "module",
dependencies: { json5: "2.2.3" },
scripts: { postinstall: "node scripts/postinstall.mjs" },
})}\n`,
"utf8",
);
await fs.writeFile(path.join(packageRoot, "dist", "worker", "worker.mjs"), workerSource, {
encoding: "utf8",
mode: 0o755,
});
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "github-exec-launcher.mjs"),
"export const launcher = true;\n",
{ encoding: "utf8", mode: 0o755 },
);
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "workspace-rsync-receiver.mjs"),
"export const receiver = true;\n",
{ encoding: "utf8", mode: 0o755 },
);
}
async function listTarball(tarballPath: string): Promise<string[]> {
const entries: string[] = [];
await tar.list({
file: tarballPath,
onReadEntry(entry) {
entries.push(entry.path);
},
});
return entries;
}
function bundleArtifact(overrides: Partial<WorkerBundleArtifact> = {}): WorkerBundleArtifact {
return {
install: "bundle",
bundleHash: "a".repeat(64),
openclawVersion: "1.2.3",
protocolFeatures: [],
tarballBytes: 1,
tarballSha256: "b".repeat(64),
tarballPath: "/tmp/openclaw-worker.tgz",
...overrides,
};
}
describe("worker bundle producer", () => {
it("stages the workspace rsync receiver at the path used by transfers", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-receiver-" }, async (root) => {
const packageRoot = path.join(root, "package");
await writeFixture(packageRoot);
const artifact = await createWorkerBundleProducer({
packageRoot,
cacheDir: path.join(root, "cache"),
}).prepare();
const installPrefix = `.openclaw-worker/${artifact.bundleHash}/`;
const receiverPath = workerWorkspaceRsyncReceiverEntryPath(artifact.bundleHash);
expect(receiverPath.startsWith(installPrefix)).toBe(true);
await expect(listTarball(artifact.tarballPath)).resolves.toContain(
receiverPath.slice(installPrefix.length),
);
});
});
it("hashes and archives only the dedicated deploy artifacts", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-" }, async (root) => {
const packageA = path.join(root, "package-a");
const packageB = path.join(root, "package-b");
await writeFixture(packageA);
await writeFixture(packageB);
await fs.mkdir(path.join(packageA, "dist", "control-ui"), { recursive: true });
await fs.writeFile(path.join(packageA, "dist", "entry.js"), 'import "json5";\n');
await fs.writeFile(
path.join(packageA, "dist", "control-ui", "index.html"),
"<main>UI</main>",
);
await fs.utimes(
path.join(packageA, "dist", "worker", "worker.mjs"),
new Date(1_000),
new Date(1_000),
);
await fs.utimes(
path.join(packageB, "dist", "worker", "worker.mjs"),
new Date(9_000),
new Date(9_000),
);
const first = await createWorkerBundleProducer({
packageRoot: packageA,
cacheDir: path.join(root, "cache-a"),
openclawVersion: "1.2.3",
}).prepare();
const second = await createWorkerBundleProducer({
packageRoot: packageB,
cacheDir: path.join(root, "cache-b"),
openclawVersion: "1.2.3",
}).prepare();
expect(first.bundleHash).toMatch(/^[a-f0-9]{64}$/u);
expect(second.bundleHash).toBe(first.bundleHash);
await expect(listTarball(first.tarballPath)).resolves.toEqual([
"github-exec-launcher.mjs",
"worker.mjs",
"workspace-rsync-receiver.mjs",
]);
const extractRoot = path.join(root, "extract");
await fs.mkdir(extractRoot);
await tar.extract({ file: first.tarballPath, cwd: extractRoot });
await expect(fs.readFile(path.join(extractRoot, "worker.mjs"), "utf8")).resolves.toContain(
"worker = true",
);
await expect(
fs.readFile(path.join(extractRoot, "workspace-rsync-receiver.mjs"), "utf8"),
).resolves.toContain("receiver = true");
await expect(fs.access(path.join(extractRoot, "package.json"))).rejects.toThrow();
await expect(fs.access(path.join(extractRoot, "node_modules"))).rejects.toThrow();
});
});
it("changes identity only when the deploy artifact changes", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-change-" }, async (root) => {
const packageRoot = path.join(root, "package");
const cacheDir = path.join(root, "cache");
await writeFixture(packageRoot, "export const value = 1;\n");
const first = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
await fs.writeFile(
path.join(packageRoot, "dist", "entry.js"),
"export const unrelated = 2;\n",
);
const unrelated = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
expect(unrelated.bundleHash).toBe(first.bundleHash);
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "worker.mjs"),
"export const value = 2;\n",
);
const changed = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
expect(changed.bundleHash).not.toBe(first.bundleHash);
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "workspace-rsync-receiver.mjs"),
"export const receiver = false;\n",
);
const receiverChanged = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
expect(receiverChanged.bundleHash).not.toBe(changed.bundleHash);
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "github-exec-launcher.mjs"),
"export const launcher = false;\n",
);
const launcherChanged = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
expect(launcherChanged.bundleHash).not.toBe(receiverChanged.bundleHash);
});
});
it("packages hardlinked artifacts larger than the default file-read limit", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-large-" }, async (root) => {
const packageRoot = path.join(root, "package");
const padding = "x".repeat(128);
const contents = Array.from(
{ length: 128 * 1024 },
(_, index) => `//${index}:${padding}\n`,
).join("");
await writeFixture(packageRoot, contents);
await fs.link(
path.join(packageRoot, "dist", "worker", "worker.mjs"),
path.join(root, "source-alias.mjs"),
);
const artifact = await createWorkerBundleProducer({
packageRoot,
cacheDir: path.join(root, "cache"),
}).prepare();
const extractDir = path.join(root, "extract");
await fs.mkdir(extractDir);
await tar.extract({ file: artifact.tarballPath, cwd: extractDir });
await expect(fs.readFile(path.join(extractDir, "worker.mjs"), "utf8")).resolves.toBe(
contents,
);
if (process.platform !== "win32") {
expect((await fs.stat(path.join(extractDir, "worker.mjs"))).mode & 0o777).toBe(0o700);
}
});
});
it("prunes only unretained bundles for an exclusive cache owner", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-prune-" }, async (root) => {
const packageRoot = path.join(root, "package");
const cacheDir = path.join(root, "cache");
await writeFixture(packageRoot, "export const value = 1;\n");
const retained = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "worker.mjs"),
"export const value = 2;\n",
);
const owner = createWorkerBundleProducer({
packageRoot,
cacheDir,
cacheOwnership: "exclusive",
});
const current = await owner.prepare();
const removedPath = path.join(cacheDir, `${"c".repeat(64)}.tgz`);
await fs.writeFile(removedPath, "historical");
await owner.prune([retained.bundleHash]);
await expect(fs.stat(retained.tarballPath)).resolves.toBeDefined();
await expect(fs.stat(current.tarballPath)).resolves.toBeDefined();
await expect(fs.stat(removedPath)).rejects.toMatchObject({ code: "ENOENT" });
});
});
it("retains a persisted provisioning bundle until its environment terminates", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-provisioning-" }, async (root) => {
const packageRoot = path.join(root, "package");
const cacheDir = path.join(root, "cache");
const databasePath = path.join(root, "state", "openclaw.sqlite");
await writeFixture(packageRoot, "export const value = 1;\n");
const admitted = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
const admittedBytes = await fs.readFile(admitted.tarballPath);
const project = { key: "project", root, baseCommit: "a".repeat(40) };
const preparation = createWorkerProjectPreparationIdentity({
namespace: "bundle-retention-test",
providerId: "fixture",
profileId: "test",
profileSnapshot: { settings: {} },
project,
target: { machineClass: "small", platform: "linux" },
artifacts: {
nodeBootstrapSha256: "b".repeat(64),
enabledPluginIds: [],
workerBundleHash: admitted.bundleHash,
workerArchiveSha256: admitted.tarballSha256,
openclawVersion: admitted.openclawVersion,
protocolFeatures: [...admitted.protocolFeatures],
},
});
try {
const store = createWorkerEnvironmentStore({
database: openOpenClawStateDatabase({ path: databasePath }),
});
store.createIntent({
environmentId: "preparing",
providerId: "fixture",
profileId: "test",
provisionOperationId: "prepare-project",
profileSnapshot: { settings: {}, project: { ...project, preparation } },
});
store.transition({ environmentId: "preparing", from: "requested", to: "provisioning" });
closeOpenClawStateDatabaseForTest();
await writeFixture(packageRoot, "export const value = 2;\n");
const successor = createWorkerBundleProducer({
packageRoot,
cacheDir,
cacheOwnership: "exclusive",
});
const current = await successor.prepare();
expect(current.bundleHash).not.toBe(admitted.bundleHash);
const reopened = createWorkerEnvironmentStore({
database: openOpenClawStateDatabase({ path: databasePath }),
});
expect(reopened.get("preparing")).toMatchObject({
state: "provisioning",
bootstrapReceipt: null,
});
const retained = () =>
listRetainedWorkerBundleHashes({ environments: reopened.list(), placements: [] });
await successor.prune(retained());
await expect(fs.readFile(admitted.tarballPath)).resolves.toEqual(admittedBytes);
reopened.transition({ environmentId: "preparing", from: "provisioning", to: "failed" });
await successor.prune(retained());
await expect(fs.stat(admitted.tarballPath)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.stat(current.tarballPath)).resolves.toBeDefined();
} finally {
closeOpenClawStateDatabaseForTest();
}
});
});
it("reclaims recognized crash artifacts but preserves unknown cache entries", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-crash-cleanup-" }, async (root) => {
const packageRoot = path.join(root, "package");
const cacheDir = path.join(root, "cache");
await writeFixture(packageRoot);
const owner = createWorkerBundleProducer({
packageRoot,
cacheDir,
cacheOwnership: "exclusive",
});
const current = await owner.prepare();
const staging = path.join(cacheDir, ".staging-stale");
const temporary = path.join(
cacheDir,
`${"b".repeat(64)}.tgz.123.123e4567-e89b-12d3-a456-426614174000.tmp`,
);
const unknown = path.join(cacheDir, "keep-me.txt");
await fs.mkdir(staging);
await fs.writeFile(temporary, "partial");
await fs.writeFile(unknown, "operator-owned");
await owner.prune([]);
await expect(fs.stat(staging)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.stat(temporary)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.readFile(unknown, "utf8")).resolves.toBe("operator-owned");
await expect(fs.stat(current.tarballPath)).resolves.toBeDefined();
});
});
it("keeps custom caches non-destructive after failed preparation", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-shared-cache-" }, async (root) => {
const cacheDir = path.join(root, "cache");
await fs.mkdir(cacheDir);
const historical = path.join(cacheDir, `${"c".repeat(64)}.tgz`);
await fs.writeFile(historical, "historical");
const shared = createWorkerBundleProducer({
packageRoot: path.join(root, "missing-package"),
cacheDir,
});
await expect(shared.prepare()).rejects.toThrow("worker deploy artifact is missing");
await shared.prune([]);
await expect(fs.readFile(historical, "utf8")).resolves.toBe("historical");
});
});
it("archives staged bytes when the source changes during packaging", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-mutation-" }, async (root) => {
const baselineRoot = path.join(root, "baseline");
const packageRoot = path.join(root, "package");
const originalContents = "export const value = 'before';\n";
const changedContents = "export const value = 'after';\n";
await writeFixture(baselineRoot, originalContents);
await writeFixture(packageRoot, originalContents);
const baseline = await createWorkerBundleProducer({
packageRoot: baselineRoot,
cacheDir: path.join(root, "baseline-cache"),
}).prepare();
const originalCollect = bundleStaging.collectWorkerBundleManifest;
let sourceMutated = false;
const stagingSpy = vi
.spyOn(bundleStaging, "collectWorkerBundleManifest")
.mockImplementation(async (...args) => {
const manifest = await originalCollect(...args);
sourceMutated = true;
await fs.writeFile(
path.join(packageRoot, "dist", "worker", "worker.mjs"),
changedContents,
);
return manifest;
});
try {
const artifact = await createWorkerBundleProducer({
packageRoot,
cacheDir: path.join(root, "cache"),
}).prepare();
const extractDir = path.join(root, "extract");
await fs.mkdir(extractDir);
await tar.extract({ file: artifact.tarballPath, cwd: extractDir });
expect(sourceMutated).toBe(true);
expect(artifact.bundleHash).toBe(baseline.bundleHash);
await expect(fs.readFile(path.join(extractDir, "worker.mjs"), "utf8")).resolves.toBe(
originalContents,
);
} finally {
stagingSpy.mockRestore();
}
});
});
it("owns one immutable build snapshot and retries failed preparation", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-cache-" }, async (root) => {
const packageRoot = path.join(root, "package");
const producer = createWorkerBundleProducer({
packageRoot,
cacheDir: path.join(root, "cache"),
protocolFeatures: ["resume", "admission", "resume"],
});
const failed = producer.prepare();
await expect(failed).rejects.toThrow("worker deploy artifact is missing");
await writeFixture(packageRoot);
const retried = producer.prepare();
expect(retried).not.toBe(failed);
const first = await retried;
await fs.writeFile(path.join(packageRoot, "dist", "worker", "worker.mjs"), "changed\n");
await expect(producer.prepare()).resolves.toBe(first);
expect(first.protocolFeatures).toEqual(["admission", "resume"]);
});
});
it("replaces a corrupt content-addressed cache entry", async () => {
await withTestDir({ prefix: "openclaw-worker-bundle-corrupt-" }, async (root) => {
const packageRoot = path.join(root, "package");
const cacheDir = path.join(root, "cache");
await writeFixture(packageRoot);
const first = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
await fs.writeFile(first.tarballPath, "not a tarball");
const repaired = await createWorkerBundleProducer({ packageRoot, cacheDir }).prepare();
expect(repaired.bundleHash).toBe(first.bundleHash);
await expect(listTarball(repaired.tarballPath)).resolves.toEqual([
"github-exec-launcher.mjs",
"worker.mjs",
"workspace-rsync-receiver.mjs",
]);
});
});
it.skipIf(process.platform === "win32")("rejects symlinked deploy artifacts", async () => {
for (const artifactName of [
"github-exec-launcher.mjs",
"worker.mjs",
"workspace-rsync-receiver.mjs",
]) {
await withTestDir({ prefix: "openclaw-worker-bundle-symlink-" }, async (root) => {
const packageRoot = path.join(root, "package");
await writeFixture(packageRoot);
const artifactPath = path.join(packageRoot, "dist", "worker", artifactName);
await fs.rename(artifactPath, `${artifactPath}.target`);
await fs.symlink(`${artifactName}.target`, artifactPath);
await expect(
createWorkerBundleProducer({
packageRoot,
cacheDir: path.join(root, "cache"),
}).prepare(),
).rejects.toThrow("Unsafe worker deploy artifact");
});
}
});
});
describe("worker npm installation artifact", () => {
it("uses an exact registry-proven gateway package", async () => {
await withTestDir({ prefix: "openclaw-worker-npm-release-" }, async (packageRoot) => {
await writeFixture(packageRoot);
const packageIntegrity = `sha512-${Buffer.alloc(64).toString("base64")}`;
const verifyRelease = vi.fn(async () => packageIntegrity);
const artifact = await resolveWorkerNpmInstallationArtifact({
bundle: bundleArtifact({ protocolFeatures: ["admission"] }),
packageRoot,
verifyRelease,
});
expect(verifyRelease).toHaveBeenCalledWith({
bundleHash: "a".repeat(64),
version: "1.2.3",
});
expect(artifact).toEqual({
install: "npm",
bundleHash: "a".repeat(64),
openclawVersion: "1.2.3",
packageIntegrity,
protocolFeatures: ["admission"],
packageSpec: "openclaw@1.2.3",
});
});
});
it("rejects dev and packages that fail release verification", async () => {
const verifyRelease = vi.fn(async (): Promise<string> => {
throw new Error("OpenClaw 1.2.3 is not published; use the worker bundle install");
});
await expect(
resolveWorkerNpmInstallationArtifact({
bundle: bundleArtifact({ openclawVersion: "dev" }),
isPackageInstall: async () => true,
verifyRelease,
}),
).rejects.toThrow("exact published gateway version");
expect(verifyRelease).not.toHaveBeenCalled();
await expect(
resolveWorkerNpmInstallationArtifact({
bundle: bundleArtifact(),
isPackageInstall: async () => true,
verifyRelease,
}),
).rejects.toThrow("use the worker bundle install");
});
it("rejects a source checkout even when its version is published", async () => {
await withTestDir({ prefix: "openclaw-worker-npm-source-" }, async (packageRoot) => {
await writeFixture(packageRoot);
await fs.mkdir(path.join(packageRoot, ".git"));
const verifyRelease = vi.fn(async () => `sha512-${Buffer.alloc(64).toString("base64")}`);
await expect(
resolveWorkerNpmInstallationArtifact({
bundle: bundleArtifact(),
packageRoot,
verifyRelease,
}),
).rejects.toThrow("packaged release install");
expect(verifyRelease).not.toHaveBeenCalled();
});
});
});