diff --git a/extensions/qqbot/src/engine/utils/data-paths.test.ts b/extensions/qqbot/src/engine/utils/data-paths.test.ts new file mode 100644 index 000000000000..34cc007b3f5f --- /dev/null +++ b/extensions/qqbot/src/engine/utils/data-paths.test.ts @@ -0,0 +1,74 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getCredentialBackupFile, getLegacyCredentialBackupFile } from "./data-paths.js"; + +const createdStateDirs: string[] = []; + +function createTempDir(prefix: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + createdStateDirs.push(dir); + return dir; +} + +describe("qqbot credential backup paths", () => { + afterEach(() => { + vi.unstubAllEnvs(); + for (const stateDir of createdStateDirs.splice(0)) { + fs.rmSync(stateDir, { recursive: true, force: true }); + } + }); + + it("scopes credential backups to the active OPENCLAW_STATE_DIR", () => { + const stateDir = createTempDir("qqbot-state-"); + vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); + + expect(getCredentialBackupFile("default")).toBe( + path.join(stateDir, "qqbot", "data", "credential-backup-default.json"), + ); + expect(getLegacyCredentialBackupFile()).toBe( + path.join(stateDir, "qqbot", "data", "credential-backup.json"), + ); + }); + + it("keeps same account IDs isolated across different state directories", () => { + const stateDirA = createTempDir("qqbot-state-a-"); + const stateDirB = createTempDir("qqbot-state-b-"); + + vi.stubEnv("OPENCLAW_STATE_DIR", stateDirA); + const gatewayAPath = getCredentialBackupFile("default"); + + vi.stubEnv("OPENCLAW_STATE_DIR", stateDirB); + const gatewayBPath = getCredentialBackupFile("default"); + + expect(gatewayAPath).toBe( + path.join(stateDirA, "qqbot", "data", "credential-backup-default.json"), + ); + expect(gatewayBPath).toBe( + path.join(stateDirB, "qqbot", "data", "credential-backup-default.json"), + ); + expect(gatewayBPath).not.toBe(gatewayAPath); + }); + + it("uses OPENCLAW_HOME for default credential backup state", () => { + const homeDir = createTempDir("qqbot-openclaw-home-"); + vi.stubEnv("OPENCLAW_STATE_DIR", ""); + vi.stubEnv("OPENCLAW_HOME", homeDir); + + expect(getCredentialBackupFile("default")).toBe( + path.join(homeDir, ".openclaw", "qqbot", "data", "credential-backup-default.json"), + ); + }); + + it("expands tilde state-dir overrides through the canonical state resolver", () => { + const homeDir = createTempDir("qqbot-home-"); + vi.stubEnv("HOME", homeDir); + vi.stubEnv("OPENCLAW_HOME", ""); + vi.stubEnv("OPENCLAW_STATE_DIR", "~/gateway-a"); + + expect(getCredentialBackupFile("default")).toBe( + path.join(homeDir, "gateway-a", "qqbot", "data", "credential-backup-default.json"), + ); + }); +}); diff --git a/extensions/qqbot/src/engine/utils/data-paths.ts b/extensions/qqbot/src/engine/utils/data-paths.ts index 91c7d6951011..7bf189ff8389 100644 --- a/extensions/qqbot/src/engine/utils/data-paths.ts +++ b/extensions/qqbot/src/engine/utils/data-paths.ts @@ -11,7 +11,7 @@ */ import path from "node:path"; -import { getQQBotDataPath } from "./platform.js"; +import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; /** * Normalise an identifier so it is safe to embed in a filename. @@ -21,6 +21,10 @@ function safeName(id: string): string { return id.replace(/[^a-zA-Z0-9._-]/g, "_"); } +function getCredentialBackupRoot(): string { + return path.join(resolveStateDir(process.env), "qqbot", "data"); +} + // ---- credential backup ---- /** @@ -29,10 +33,10 @@ function safeName(id: string): string { * missing from the live config. */ export function getCredentialBackupFile(accountId: string): string { - return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`); + return path.join(getCredentialBackupRoot(), `credential-backup-${safeName(accountId)}.json`); } /** Legacy single-file credential backup (pre-multi-account-isolation). */ export function getLegacyCredentialBackupFile(): string { - return path.join(getQQBotDataPath("data"), "credential-backup.json"); + return path.join(getCredentialBackupRoot(), "credential-backup.json"); } diff --git a/extensions/qqbot/src/engine/utils/platform-storage-laziness.test.ts b/extensions/qqbot/src/engine/utils/platform-storage-laziness.test.ts index 3969a830561a..906fcc80599a 100644 --- a/extensions/qqbot/src/engine/utils/platform-storage-laziness.test.ts +++ b/extensions/qqbot/src/engine/utils/platform-storage-laziness.test.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const createdHomes: string[] = []; async function useMockHome(homeDir: string): Promise { + vi.stubEnv("HOME", homeDir); vi.resetModules(); vi.doMock("node:os", async (importOriginal) => { const actual = await importOriginal(); @@ -26,6 +27,7 @@ function makeHome(): string { describe("qqbot storage laziness", () => { afterEach(() => { vi.doUnmock("node:os"); + vi.unstubAllEnvs(); vi.resetModules(); for (const home of createdHomes.splice(0)) { fs.rmSync(home, { recursive: true, force: true });