mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 19:12:22 +00:00
fix(qqbot): isolate credential backups by state root
QQBot credential backups now resolve under the active OpenClaw state directory instead of the old home-global QQBot data path. This keeps isolated gateway profiles from restoring each other's QQBot appId/clientSecret backups while preserving per-state-root recovery. Proof: focused QQBot path/storage-laziness Vitest suite passed on Node 24.15.0, focused oxlint passed, source-runtime two-root backup proof passed, exact-head CI run 26814565282 passed, and ClawSweeper re-review run 26815054980 marked proof sufficient. Closes #84313. Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
This commit is contained in:
74
extensions/qqbot/src/engine/utils/data-paths.test.ts
Normal file
74
extensions/qqbot/src/engine/utils/data-paths.test.ts
Normal file
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
const createdHomes: string[] = [];
|
||||
|
||||
async function useMockHome(homeDir: string): Promise<void> {
|
||||
vi.stubEnv("HOME", homeDir);
|
||||
vi.resetModules();
|
||||
vi.doMock("node:os", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:os")>();
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user