openclaw / src /cli /program /register.backup.product-path.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
8.73 kB
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { Worker } from "node:worker_threads";
import * as tar from "tar";
import { describe, expect, it } from "vitest";
import {
closeOpenClawStateDatabase,
openOpenClawStateDatabase,
} from "../../state/openclaw-state-db.js";
import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js";
function runBackupCli(params: {
env: NodeJS.ProcessEnv;
outputPath: string;
preloadPath?: string;
includeWorkspace?: boolean;
}): Promise<{ code: number | null; stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
...(params.preloadPath ? ["--import", params.preloadPath] : []),
"--import",
"tsx",
path.resolve("src/entry.ts"),
"backup",
"create",
"--output",
params.outputPath,
...(params.includeWorkspace ? [] : ["--no-include-workspace"]),
"--verify",
"--json",
],
{
env: { ...params.env, OPENCLAW_TEST_RUNTIME_LOG: "1" },
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.once("error", reject);
child.once("close", (code) => resolve({ code, stdout, stderr }));
});
}
describe("backup create CLI", () => {
it.each([true, false])(
"backup create retains workspace-discovered owners under state write load (includeWorkspace=%s)",
async (includeWorkspace) => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const pluginRoot = path.join(
state.workspaceDir,
".openclaw",
"extensions",
"backup-fixture",
);
await fs.mkdir(pluginRoot, { recursive: true });
await fs.writeFile(path.join(state.workspaceDir, "workspace-only-marker.txt"), "workspace");
await fs.writeFile(
path.join(pluginRoot, "package.json"),
JSON.stringify({
name: "backup-fixture",
version: "1.0.0",
openclaw: { extensions: ["./index.cjs"] },
}),
);
await fs.writeFile(
path.join(pluginRoot, "index.cjs"),
"module.exports = { register() {} };",
);
await fs.writeFile(
path.join(pluginRoot, "openclaw.plugin.json"),
JSON.stringify({
id: "backup-fixture",
configSchema: { type: "object", properties: {} },
backupResources: [
{ disposition: "include", scope: "state", relativePath: "plugin-data" },
],
}),
);
await state.writeConfig({
agents: { entries: { main: { workspace: state.workspaceDir } } },
plugins: { allow: ["backup-fixture"], entries: { "backup-fixture": { enabled: true } } },
});
const root = openOpenClawStateDatabase();
const rootPath = root.path;
closeOpenClawStateDatabase();
await fs.mkdir(state.statePath("plugin-data"));
const writer = new Worker(
`
const { parentPort, workerData } = require('node:worker_threads');
const { DatabaseSync } = require('node:sqlite');
const plugin = new DatabaseSync(workerData.plugin);
plugin.exec("CREATE TABLE records(value TEXT); INSERT INTO records VALUES ('owned');");
plugin.close();
const db = new DatabaseSync(workerData.root, { timeout: 5000 });
db.exec('PRAGMA journal_mode=WAL; CREATE TABLE contention_fixture(value BLOB); INSERT INTO contention_fixture VALUES(zeroblob(1048576));');
const write = db.prepare('UPDATE contention_fixture SET value=randomblob(1048576)');
let stopped = false;
parentPort.on('message', () => { stopped = true; });
function tick() {
if (stopped) { db.close(); parentPort.close(); return; }
write.run(); setImmediate(tick);
}
tick(); parentPort.postMessage('ready');
`,
{
eval: true,
workerData: { root: rootPath, plugin: state.statePath("plugin-data/records.sqlite") },
},
);
const finished = new Promise<number | Error>((resolve) => {
writer.once("error", resolve);
writer.once("exit", resolve);
});
try {
await new Promise<void>((resolve, reject) => {
writer.once("message", () => resolve());
writer.once("error", reject);
});
const outputPath = state.path("backup.tar.gz");
const result = await runBackupCli({
env: { ...process.env, ...state.env },
outputPath,
includeWorkspace,
});
expect(result.code, result.stderr).toBe(0);
const archive = JSON.parse(result.stdout);
expect(archive.agentRoots).toEqual([expect.objectContaining({ agentId: "main" })]);
expect(archive.verified).toBe(true);
expect(archive.warnings ?? []).toEqual([]);
const entries: string[] = [];
await tar.t({
file: outputPath,
onReadEntry: (entry) => {
entries.push(entry.path);
},
});
expect(entries.some((entry) => entry.endsWith("/plugin-data/records.sqlite"))).toBe(true);
expect(entries.some((entry) => entry.endsWith("/workspace-only-marker.txt"))).toBe(
includeWorkspace,
);
} finally {
writer.postMessage("stop", []);
expect(await finished).toBe(0);
}
});
},
);
it("backup create refuses unreadable discovery state even without workspaces", async () => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
await state.writeConfig({ agents: { entries: { main: {} } } });
await fs.mkdir(state.statePath("state"));
await fs.writeFile(state.statePath("state/openclaw.sqlite"), "unreadable database");
const outputPath = state.path("backup.tar.gz");
const result = await runBackupCli({ env: { ...process.env, ...state.env }, outputPath });
expect(result.code).toBe(1);
expect(result.stderr).not.toContain("Config invalid");
expect(result.stderr).toContain("Cannot read shared state for discovery");
await expect(fs.stat(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
});
});
it("completes when the SQLite snapshot outlives the audit lease", async () => {
await withOpenClawTestState(
{ layout: "state-only", prefix: "backup-cli-audit-lease-", scenario: "minimal" },
async (state) => {
await state.writeConfig({ gateway: { mode: "local" } });
await state.writeText(
"logs/config-audit.jsonl",
`${JSON.stringify({
ts: "2026-09-03T00:00:00.000Z",
source: "config-io",
event: "config.write",
argv: ["openclaw", "config", "set", "proof", "lease"],
execArgv: [],
})}\n`,
);
const markerPath = state.path("sqlite-backup-entered");
const preloadPath = await state.writeText(
"shift-clock-at-sqlite-backup.mjs",
`
import fs from "node:fs";
import { syncBuiltinESMExports } from "node:module";
const sqlite = process.getBuiltinModule("node:sqlite");
const originalBackup = sqlite.backup.bind(sqlite);
let shifted = false;
sqlite.backup = async (...args) => {
if (!shifted) {
shifted = true;
fs.writeFileSync(process.env.PROOF_SNAPSHOT_MARKER, "entered\\n", { mode: 0o600 });
const realNow = Date.now.bind(Date);
Date.now = () => realNow() + 61_000;
}
return await originalBackup(...args);
};
syncBuiltinESMExports();
`,
);
const outputPath = state.path("backup.tar.gz");
const result = await runBackupCli({
env: {
...process.env,
...state.env,
OPENCLAW_TEST_CONSOLE: "1",
PROOF_SNAPSHOT_MARKER: markerPath,
},
outputPath,
preloadPath,
});
expect(result.code, result.stderr).toBe(0);
expect(await fs.readFile(markerPath, "utf8")).toBe("entered\n");
expect((await fs.stat(outputPath)).size).toBeGreaterThan(0);
},
);
});
});