fix(backup): exclude state temp files (#122250)

This commit is contained in:
Peter Steinberger
2026-08-11 13:48:27 -07:00
committed by GitHub
parent a432e2a586
commit d699662083
3 changed files with 42 additions and 7 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ SQLite databases under the state directory are captured with SQLite's online bac
Installed plugin source and manifest files under the state directory's `extensions/` tree are included, but their nested `node_modules/` dependency trees are skipped as rebuildable install artifacts. After restoring an archive, use `openclaw plugins update <id>` or reinstall with `openclaw plugins install <spec> --force` if a restored plugin reports missing dependencies.
Installer-managed and rebuildable runtime roots under the state directory are also skipped: `dev/`, `git/`, `npm/`, legacy `npm-runtime/`, and `tools/`. These contain managed checkouts, package trees, and downloaded runtimes rather than authoritative user state; reinstall or update the corresponding runtime or plugin after restore. An explicitly configured config file, credentials directory, or workspace inside one of these roots remains included.
Installer-managed and rebuildable runtime roots under the state directory are also skipped: `dev/`, `git/`, `npm/`, legacy `npm-runtime/`, `tmp/`, and `tools/`. These contain managed checkouts, package trees, compiler caches, temporary files, and downloaded runtimes rather than authoritative user state; reinstall or update the corresponding runtime or plugin after restore. An explicitly configured config file, credentials directory, or workspace inside one of these roots remains included.
Local edits inside a managed `dev/` checkout are developer source, not OpenClaw product state, and are not included. Commit and push those edits or copy the checkout separately before relying on a state backup.
+39 -4
View File
@@ -726,7 +726,7 @@ describe("createBackupArchive", () => {
suffix,
).toBe(false);
}
expect(result.skippedVolatileCount).toBe(10);
expect(result.skippedVolatileCount).toBe(9);
},
);
});
@@ -2524,7 +2524,9 @@ describe("createBackupArchive", () => {
await fs.mkdir(path.join(stateDir, "dev", "openclaw", "dist"), { recursive: true });
await fs.mkdir(path.join(stateDir, "developer"), { recursive: true });
await fs.mkdir(path.join(stateDir, "dev-backup"), { recursive: true });
for (const managedRoot of ["dev", "git", "npm-runtime", "tools"]) {
await fs.mkdir(path.join(stateDir, "temporary"), { recursive: true });
await fs.mkdir(path.join(stateDir, "tmp-data"), { recursive: true });
for (const managedRoot of ["dev", "git", "npm-runtime", "tmp", "tools"]) {
await fs.mkdir(path.join(stateDir, managedRoot, "runtime"), { recursive: true });
await fs.writeFile(
path.join(stateDir, managedRoot, "runtime", "fixture.sqlite"),
@@ -2589,6 +2591,8 @@ describe("createBackupArchive", () => {
);
await fs.writeFile(path.join(stateDir, "developer", "keep.txt"), "keep\n", "utf8");
await fs.writeFile(path.join(stateDir, "dev-backup", "keep.txt"), "keep\n", "utf8");
await fs.writeFile(path.join(stateDir, "temporary", "keep.txt"), "keep\n", "utf8");
await fs.writeFile(path.join(stateDir, "tmp-data", "keep.txt"), "keep\n", "utf8");
await fs.mkdir(outputDir, { recursive: true });
const result = await createBackupArchive({
@@ -2603,7 +2607,7 @@ describe("createBackupArchive", () => {
expect(entrySuffixes).toContain("/state/extensions/demo/src/index.js");
expect(entrySuffixes).toContain("/state/node_modules/root-dep/index.js");
expect(entrySuffixes).toContain("/state/node_modules/root-dep/fixture.sqlite");
for (const managedRoot of ["dev", "git", "npm", "npm-runtime", "tools"]) {
for (const managedRoot of ["dev", "git", "npm", "npm-runtime", "tmp", "tools"]) {
expect(
entrySuffixes.some(
(entry) =>
@@ -2614,6 +2618,8 @@ describe("createBackupArchive", () => {
}
expect(entrySuffixes).toContain("/state/developer/keep.txt");
expect(entrySuffixes).toContain("/state/dev-backup/keep.txt");
expect(entrySuffixes).toContain("/state/temporary/keep.txt");
expect(entrySuffixes).toContain("/state/tmp-data/keep.txt");
const pluginNodeModuleEntries = entries.filter((entry) =>
entry.includes("/state/extensions/demo/node_modules/"),
);
@@ -2637,6 +2643,8 @@ describe("createBackupArchive", () => {
async (state) => {
const stateDir = state.stateDir;
const workspaceDir = path.join(stateDir, "dev", "workspace");
const tmpWorkspaceDir = path.join(stateDir, "tmp", "workspace");
const externalTmpWorkspaceDir = state.path("tmp");
const runtimeDir = path.join(stateDir, "dev", "openclaw");
const configPath = path.join(stateDir, "git", "config", "openclaw.json");
const oauthDir = path.join(stateDir, "tools", "oauth");
@@ -2647,6 +2655,9 @@ describe("createBackupArchive", () => {
state.envVars.OPENCLAW_OAUTH_DIR = oauthDir;
state.applyEnv();
await fs.mkdir(workspaceDir, { recursive: true });
await fs.mkdir(tmpWorkspaceDir, { recursive: true });
await fs.mkdir(externalTmpWorkspaceDir, { recursive: true });
await fs.mkdir(path.join(stateDir, "tmp", "tsx-501"), { recursive: true });
await fs.mkdir(runtimeDir, { recursive: true });
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.mkdir(oauthDir, { recursive: true });
@@ -2655,13 +2666,32 @@ describe("createBackupArchive", () => {
configPath,
`${JSON.stringify({
agents: {
entries: { main: { default: true, workspace: workspaceDir } },
entries: {
main: { default: true, workspace: workspaceDir },
external: { workspace: externalTmpWorkspaceDir },
worker: { workspace: tmpWorkspaceDir },
},
},
})}\n`,
"utf8",
);
await fs.writeFile(path.join(oauthDir, "credentials.json"), "{}\n", "utf8");
await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "durable workspace\n", "utf8");
await fs.writeFile(
path.join(tmpWorkspaceDir, "AGENTS.md"),
"durable tmp workspace\n",
"utf8",
);
await fs.writeFile(
path.join(externalTmpWorkspaceDir, "AGENTS.md"),
"durable external tmp workspace\n",
"utf8",
);
await fs.writeFile(
path.join(stateDir, "tmp", "tsx-501", "cache-entry"),
"rebuildable compiler cache\n",
"utf8",
);
await fs.writeFile(path.join(runtimeDir, "package.json"), "{}\n", "utf8");
await fs.writeFile(path.join(toolRuntimeDir, "tool.bin"), "runtime\n", "utf8");
const sqlite = requireNodeSqlite();
@@ -2688,6 +2718,10 @@ describe("createBackupArchive", () => {
expect(
entries.some((entry) => entry.endsWith("/state/dev/workspace/workspace.sqlite")),
).toBe(true);
expect(entries.some((entry) => entry.endsWith("/state/tmp/workspace/AGENTS.md"))).toBe(
true,
);
expect(entries.some((entry) => entry.endsWith("/tmp/AGENTS.md"))).toBe(true);
expect(entries.some((entry) => entry.endsWith("/state/git/config/openclaw.json"))).toBe(
true,
);
@@ -2695,6 +2729,7 @@ describe("createBackupArchive", () => {
true,
);
expect(entries.some((entry) => entry.includes("/state/dev/openclaw/"))).toBe(false);
expect(entries.some((entry) => entry.includes("/state/tmp/tsx-501/"))).toBe(false);
expect(entries.some((entry) => entry.includes("/state/tools/runtime/"))).toBe(false);
const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
+2 -2
View File
@@ -331,7 +331,7 @@ function normalizeBackupFilterPath(value: string): string {
return value.replaceAll("\\", "/").replace(/\/+$/u, "");
}
const REINSTALLABLE_STATE_ROOTS = new Set(["dev", "git", "npm", "npm-runtime", "tools"]);
const NON_AUTHORITATIVE_STATE_ROOTS = new Set(["dev", "git", "npm", "npm-runtime", "tmp", "tools"]);
function buildStateBackupFilter(
stateDir: string,
@@ -348,7 +348,7 @@ function buildStateBackupFilter(
}
const segments = normalizedFilePath.slice(statePrefix.length).split("/");
if (REINSTALLABLE_STATE_ROOTS.has(segments[0] ?? "")) {
if (NON_AUTHORITATIVE_STATE_ROOTS.has(segments[0] ?? "")) {
const resolvedFilePath = path.resolve(filePath);
// Configured workspaces nested under a managed root remain authoritative
// user state. Keep their ancestors traversable without admitting siblings.