From 6ae66a8cbcf2c9bc1a80bd37ea0c075affe8de74 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Mar 2026 00:17:20 +0000 Subject: [PATCH] test: add state migration coverage --- src/infra/state-migrations.test.ts | 201 +++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/infra/state-migrations.test.ts diff --git a/src/infra/state-migrations.test.ts b/src/infra/state-migrations.test.ts new file mode 100644 index 00000000000..f8437f85529 --- /dev/null +++ b/src/infra/state-migrations.test.ts @@ -0,0 +1,201 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveChannelAllowFromPath } from "../pairing/pairing-store.js"; +import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js"; +import { detectLegacyStateMigrations, runLegacyStateMigrations } from "./state-migrations.js"; + +const tempDirs = createTrackedTempDirs(); +const createTempDir = () => tempDirs.make("openclaw-state-migrations-test-"); + +function createConfig(): OpenClawConfig { + return { + agents: { + list: [{ id: "worker-1", default: true }], + }, + session: { + mainKey: "desk", + }, + channels: { + telegram: { + accounts: { + beta: {}, + alpha: {}, + }, + }, + }, + } as OpenClawConfig; +} + +function createEnv(stateDir: string): NodeJS.ProcessEnv { + return { + ...process.env, + OPENCLAW_STATE_DIR: stateDir, + }; +} + +afterEach(async () => { + await tempDirs.cleanup(); +}); + +describe("state migrations", () => { + it("detects legacy sessions, agent files, whatsapp auth, and telegram allowFrom copies", async () => { + const root = await createTempDir(); + const stateDir = path.join(root, ".openclaw"); + const env = createEnv(stateDir); + const cfg = createConfig(); + + await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "agent"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true }); + + await fs.writeFile( + path.join(stateDir, "sessions", "sessions.json"), + `${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`, + "utf8", + ); + await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8"); + await fs.writeFile( + path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"), + `${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`, + "utf8", + ); + await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8"); + await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8"); + await fs.writeFile( + path.join(stateDir, "credentials", "oauth.json"), + '{"oauth":true}\n', + "utf8", + ); + await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8"); + + const detected = await detectLegacyStateMigrations({ + cfg, + env, + homedir: () => root, + }); + + expect(detected.targetAgentId).toBe("worker-1"); + expect(detected.targetMainKey).toBe("desk"); + expect(detected.sessions.hasLegacy).toBe(true); + expect(detected.sessions.legacyKeys).toEqual(["group:123@g.us"]); + expect(detected.agentDir.hasLegacy).toBe(true); + expect(detected.whatsappAuth.hasLegacy).toBe(true); + expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true); + expect(detected.pairingAllowFrom.copyPlans.map((plan) => plan.targetPath)).toEqual([ + resolveChannelAllowFromPath("telegram", env, "alpha"), + resolveChannelAllowFromPath("telegram", env, "beta"), + ]); + expect(detected.preview).toEqual([ + `- Sessions: ${path.join(stateDir, "sessions")} → ${path.join(stateDir, "agents", "worker-1", "sessions")}`, + `- Sessions: canonicalize legacy keys in ${path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json")}`, + `- Agent dir: ${path.join(stateDir, "agent")} → ${path.join(stateDir, "agents", "worker-1", "agent")}`, + `- WhatsApp auth: ${path.join(stateDir, "credentials")} → ${path.join(stateDir, "credentials", "whatsapp", "default")} (keep oauth.json)`, + `- Telegram pairing allowFrom: ${resolveChannelAllowFromPath("telegram", env)} → ${resolveChannelAllowFromPath("telegram", env, "alpha")}`, + `- Telegram pairing allowFrom: ${resolveChannelAllowFromPath("telegram", env)} → ${resolveChannelAllowFromPath("telegram", env, "beta")}`, + ]); + }); + + it("runs legacy state migrations and canonicalizes the merged session store", async () => { + const root = await createTempDir(); + const stateDir = path.join(root, ".openclaw"); + const env = createEnv(stateDir); + const cfg = createConfig(); + + await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "agent"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true }); + + await fs.writeFile( + path.join(stateDir, "sessions", "sessions.json"), + `${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`, + "utf8", + ); + await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8"); + await fs.writeFile( + path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"), + `${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`, + "utf8", + ); + await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8"); + await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8"); + await fs.writeFile( + path.join(stateDir, "credentials", "pre-key-1.json"), + '{"preKey":true}\n', + "utf8", + ); + await fs.writeFile( + path.join(stateDir, "credentials", "oauth.json"), + '{"oauth":true}\n', + "utf8", + ); + await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8"); + + const detected = await detectLegacyStateMigrations({ + cfg, + env, + homedir: () => root, + }); + const result = await runLegacyStateMigrations({ + detected, + now: () => 1234, + }); + + expect(result.warnings).toEqual([]); + expect(result.changes).toEqual([ + `Migrated latest direct-chat session → agent:worker-1:desk`, + `Merged sessions store → ${path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json")}`, + "Canonicalized 1 legacy session key(s)", + "Moved trace.jsonl → agents/worker-1/sessions", + "Moved agent file settings.json → agents/worker-1/agent", + "Moved WhatsApp auth creds.json → whatsapp/default", + "Moved WhatsApp auth pre-key-1.json → whatsapp/default", + `Copied Telegram pairing allowFrom → ${resolveChannelAllowFromPath("telegram", env, "alpha")}`, + `Copied Telegram pairing allowFrom → ${resolveChannelAllowFromPath("telegram", env, "beta")}`, + ]); + + const mergedStore = JSON.parse( + await fs.readFile( + path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"), + "utf8", + ), + ) as Record; + expect(mergedStore["agent:worker-1:desk"]?.sessionId).toBe("legacy-direct"); + expect(mergedStore["agent:worker-1:whatsapp:group:123@g.us"]?.sessionId).toBe("group-session"); + + await expect( + fs.readFile(path.join(stateDir, "agents", "worker-1", "sessions", "trace.jsonl"), "utf8"), + ).resolves.toBe("{}\n"); + await expect(fs.stat(path.join(stateDir, "sessions", "sessions.json"))).rejects.toMatchObject({ + code: "ENOENT", + }); + await expect(fs.stat(path.join(stateDir, "sessions", "trace.jsonl"))).rejects.toMatchObject({ + code: "ENOENT", + }); + + await expect( + fs.readFile(path.join(stateDir, "agents", "worker-1", "agent", "settings.json"), "utf8"), + ).resolves.toContain('"ok":true'); + await expect( + fs.readFile(path.join(stateDir, "credentials", "whatsapp", "default", "creds.json"), "utf8"), + ).resolves.toContain('"auth":true'); + await expect( + fs.readFile( + path.join(stateDir, "credentials", "whatsapp", "default", "pre-key-1.json"), + "utf8", + ), + ).resolves.toContain('"preKey":true'); + await expect( + fs.readFile(path.join(stateDir, "credentials", "oauth.json"), "utf8"), + ).resolves.toContain('"oauth":true'); + await expect( + fs.readFile(resolveChannelAllowFromPath("telegram", env, "alpha"), "utf8"), + ).resolves.toBe('["123","456"]\n'); + await expect( + fs.readFile(resolveChannelAllowFromPath("telegram", env, "beta"), "utf8"), + ).resolves.toBe('["123","456"]\n'); + }); +});