Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
355 changes: 353 additions & 2 deletions nemoclaw/src/commands/migration-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ vi.mock("node:fs", async (importOriginal) => {
if (!entry) throw new Error(`ENOENT: ${src}`);
store.set(dest, { ...entry });
}),
cpSync: vi.fn((src: string, dest: string) => {
cpSync: vi.fn((src: string, dest: string, opts?: { filter?: (source: string) => boolean }) => {
// Shallow copy: copy all entries whose path starts with src
for (const [k, v] of store) {
if (k === src || k.startsWith(src + "/")) {
if (opts?.filter && !opts.filter(k)) continue;
const relative = k.slice(src.length);
store.set(dest + relative, { ...v });
}
Expand Down Expand Up @@ -455,7 +456,7 @@ describe("commands/migration-state", () => {
expect.unreachable("bundle should not be null");
return;
}
expect(bundle.manifest.version).toBe(2);
expect(bundle.manifest.version).toBe(3);
expect(bundle.manifest.homeDir).toBe("/home/user");
expect(bundle.temporary).toBe(false);
});
Expand Down Expand Up @@ -529,6 +530,156 @@ describe("commands/migration-state", () => {
}
expect(bundle.manifest.externalRoots.length).toBe(1);
});

it("excludes auth-profiles.json from snapshot", () => {
const logger = makeLogger();
addDir("/home/user/.openclaw");
addFile("/home/user/.openclaw/openclaw.json", JSON.stringify({ version: 1 }));
addDir("/home/user/.openclaw/agents/main/agent");
addFile(
"/home/user/.openclaw/agents/main/agent/auth-profiles.json",
JSON.stringify({ "nvidia:manual": { type: "api_key" } }),
);
addFile(
"/home/user/.openclaw/agents/main/agent/config.json",
JSON.stringify({ name: "main" }),
);

const hostState: HostOpenClawState = {
exists: true,
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configDir: "/home/user/.openclaw",
configPath: "/home/user/.openclaw/openclaw.json",
workspaceDir: null,
extensionsDir: null,
skillsDir: null,
hooksDir: null,
externalRoots: [],
warnings: [],
errors: [],
hasExternalConfig: false,
};

const bundle = createSnapshotBundle(hostState, logger, { persist: false });
if (bundle === null) {
expect.unreachable("bundle should not be null");
return;
}

// auth-profiles.json should not exist anywhere in the snapshot
const snapshotKeys = [...store.keys()].filter((k) => k.startsWith(bundle.snapshotDir));
const authProfileKeys = snapshotKeys.filter((k) => k.endsWith("auth-profiles.json"));
expect(authProfileKeys).toHaveLength(0);

// config.json should still be present
const configKeys = snapshotKeys.filter((k) => k.endsWith("agents/main/agent/config.json"));
expect(configKeys.length).toBeGreaterThan(0);
});

it("strips gateway key from sandbox openclaw.json", () => {
const logger = makeLogger();
addDir("/home/user/.openclaw");
addFile(
"/home/user/.openclaw/openclaw.json",
JSON.stringify({ version: 1, gateway: { auth: { token: "secret123" } } }),
);

const hostState: HostOpenClawState = {
exists: true,
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configDir: "/home/user/.openclaw",
configPath: "/home/user/.openclaw/openclaw.json",
workspaceDir: null,
extensionsDir: null,
skillsDir: null,
hooksDir: null,
externalRoots: [],
warnings: [],
errors: [],
hasExternalConfig: false,
};

const bundle = createSnapshotBundle(hostState, logger, { persist: false });
if (bundle === null) {
expect.unreachable("bundle should not be null");
return;
}

// Read the sandbox-bundle openclaw.json
const sandboxConfigEntry = store.get(bundle.preparedStateDir + "/openclaw.json");
if (!sandboxConfigEntry?.content) {
expect.unreachable("sandbox config entry should exist with content");
return;
}
const sandboxConfig = JSON.parse(sandboxConfigEntry.content);
expect(sandboxConfig).not.toHaveProperty("gateway");
expect(sandboxConfig).toHaveProperty("version", 1);
});

it("records blueprintDigest when blueprintPath is provided", () => {
const logger = makeLogger();
addDir("/home/user/.openclaw");
addFile("/home/user/.openclaw/openclaw.json", JSON.stringify({ version: 1 }));
addFile("/test/blueprint.yaml", "version: 0.1.0\ndigest: ''\n");

const hostState: HostOpenClawState = {
exists: true,
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configDir: "/home/user/.openclaw",
configPath: "/home/user/.openclaw/openclaw.json",
workspaceDir: null,
extensionsDir: null,
skillsDir: null,
hooksDir: null,
externalRoots: [],
warnings: [],
errors: [],
hasExternalConfig: false,
};

const bundle = createSnapshotBundle(hostState, logger, {
persist: false,
blueprintPath: "/test/blueprint.yaml",
});
if (bundle === null) {
expect.unreachable("bundle should not be null");
return;
}
expect(typeof bundle.manifest.blueprintDigest).toBe("string");
expect((bundle.manifest.blueprintDigest ?? "").length).toBeGreaterThan(0);
});

it("blueprintDigest is null when no blueprintPath given", () => {
const logger = makeLogger();
addDir("/home/user/.openclaw");
addFile("/home/user/.openclaw/openclaw.json", JSON.stringify({ version: 1 }));

const hostState: HostOpenClawState = {
exists: true,
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configDir: "/home/user/.openclaw",
configPath: "/home/user/.openclaw/openclaw.json",
workspaceDir: null,
extensionsDir: null,
skillsDir: null,
hooksDir: null,
externalRoots: [],
warnings: [],
errors: [],
hasExternalConfig: false,
};

const bundle = createSnapshotBundle(hostState, logger, { persist: false });
if (bundle === null) {
expect.unreachable("bundle should not be null");
return;
}
expect(bundle.manifest.blueprintDigest).toBeNull();
});
});

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -896,5 +1047,205 @@ describe("commands/migration-state", () => {
}
}
});

it("restore succeeds when blueprint digest matches", () => {
const logger = makeLogger();
const origHome = process.env.HOME;
process.env.HOME = "/home/user";
try {
// Create a blueprint file and compute its expected digest
const blueprintContent = "version: 0.1.0\ndigest: ''\n";
addFile("/test/blueprint.yaml", blueprintContent);

// First create a snapshot with blueprintPath to get the real digest
addDir("/home/user/.openclaw");
addFile("/home/user/.openclaw/openclaw.json", JSON.stringify({ version: 1 }));
const hostState: HostOpenClawState = {
exists: true,
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configDir: "/home/user/.openclaw",
configPath: "/home/user/.openclaw/openclaw.json",
workspaceDir: null,
extensionsDir: null,
skillsDir: null,
hooksDir: null,
externalRoots: [],
warnings: [],
errors: [],
hasExternalConfig: false,
};
const bundle = createSnapshotBundle(hostState, logger, {
persist: false,
blueprintPath: "/test/blueprint.yaml",
});
if (bundle === null) {
expect.unreachable("bundle should not be null");
return;
}
const digest = bundle.manifest.blueprintDigest;
expect(digest).toBeTruthy();

// Now set up for restore with matching digest
store.clear();
const manifest: SnapshotManifest = {
version: 3,
createdAt: "2026-03-01T00:00:00.000Z",
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configPath: null,
hasExternalConfig: false,
externalRoots: [],
warnings: [],
blueprintDigest: digest,
};
addFile("/snapshots/snap1/snapshot.json", JSON.stringify(manifest));
addDir("/snapshots/snap1/openclaw");
addFile("/snapshots/snap1/openclaw/openclaw.json", JSON.stringify({ restored: true }));
addFile("/test/blueprint.yaml", blueprintContent);

const result = restoreSnapshotToHost("/snapshots/snap1", logger, {
blueprintPath: "/test/blueprint.yaml",
});
expect(result).toBe(true);
} finally {
if (origHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = origHome;
}
}
});

it("restore fails when blueprint digest mismatches", () => {
const logger = makeLogger();
const origHome = process.env.HOME;
process.env.HOME = "/home/user";
try {
const manifest: SnapshotManifest = {
version: 3,
createdAt: "2026-03-01T00:00:00.000Z",
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configPath: null,
hasExternalConfig: false,
externalRoots: [],
warnings: [],
blueprintDigest: "wrong-hash-value",
};
addFile("/snapshots/snap1/snapshot.json", JSON.stringify(manifest));
addDir("/snapshots/snap1/openclaw");
addFile("/test/blueprint.yaml", "version: 0.1.0\n");

const result = restoreSnapshotToHost("/snapshots/snap1", logger, {
blueprintPath: "/test/blueprint.yaml",
});
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("digest mismatch"));
} finally {
if (origHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = origHome;
}
}
});

it("restore fails when manifest has empty string blueprintDigest", () => {
const logger = makeLogger();
const origHome = process.env.HOME;
process.env.HOME = "/home/user";
try {
const manifest: SnapshotManifest = {
version: 3,
createdAt: "2026-03-01T00:00:00.000Z",
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configPath: null,
hasExternalConfig: false,
externalRoots: [],
warnings: [],
blueprintDigest: "",
};
addFile("/snapshots/snap1/snapshot.json", JSON.stringify(manifest));
addDir("/snapshots/snap1/openclaw");

const result = restoreSnapshotToHost("/snapshots/snap1", logger);
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining("invalid blueprintDigest"),
);
} finally {
if (origHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = origHome;
}
}
});

it("restore fails when manifest has digest but no blueprintPath provided", () => {
const logger = makeLogger();
const origHome = process.env.HOME;
process.env.HOME = "/home/user";
try {
const manifest: SnapshotManifest = {
version: 3,
createdAt: "2026-03-01T00:00:00.000Z",
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configPath: null,
hasExternalConfig: false,
externalRoots: [],
warnings: [],
blueprintDigest: "abc123",
};
addFile("/snapshots/snap1/snapshot.json", JSON.stringify(manifest));
addDir("/snapshots/snap1/openclaw");

const result = restoreSnapshotToHost("/snapshots/snap1", logger);
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining("no blueprint is available"),
);
} finally {
if (origHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = origHome;
}
}
});

it("restore succeeds when manifest has no blueprintDigest (backward compat)", () => {
const logger = makeLogger();
const origHome = process.env.HOME;
process.env.HOME = "/home/user";
try {
const manifest: SnapshotManifest = {
version: 2,
createdAt: "2026-03-01T00:00:00.000Z",
homeDir: "/home/user",
stateDir: "/home/user/.openclaw",
configPath: null,
hasExternalConfig: false,
externalRoots: [],
warnings: [],
// no blueprintDigest field
};
addFile("/snapshots/snap1/snapshot.json", JSON.stringify(manifest));
addDir("/snapshots/snap1/openclaw");
addFile("/snapshots/snap1/openclaw/openclaw.json", JSON.stringify({ restored: true }));

const result = restoreSnapshotToHost("/snapshots/snap1", logger);
expect(result).toBe(true);
} finally {
if (origHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = origHome;
}
}
});
});
});
Loading
Loading