Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 7533b85156186863609fee9379cd9aedf74435af Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com> Reviewed-by: @shakkernerd
175 lines
6.3 KiB
TypeScript
175 lines
6.3 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { resolveStorePath, resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
|
|
import { note } from "../terminal/note.js";
|
|
import { noteStateIntegrity } from "./doctor-state-integrity.js";
|
|
|
|
vi.mock("../terminal/note.js", () => ({
|
|
note: vi.fn(),
|
|
}));
|
|
|
|
type EnvSnapshot = {
|
|
HOME?: string;
|
|
OPENCLAW_HOME?: string;
|
|
OPENCLAW_STATE_DIR?: string;
|
|
OPENCLAW_OAUTH_DIR?: string;
|
|
};
|
|
|
|
function captureEnv(): EnvSnapshot {
|
|
return {
|
|
HOME: process.env.HOME,
|
|
OPENCLAW_HOME: process.env.OPENCLAW_HOME,
|
|
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
|
|
OPENCLAW_OAUTH_DIR: process.env.OPENCLAW_OAUTH_DIR,
|
|
};
|
|
}
|
|
|
|
function restoreEnv(snapshot: EnvSnapshot) {
|
|
for (const key of Object.keys(snapshot) as Array<keyof EnvSnapshot>) {
|
|
const value = snapshot[key];
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setupSessionState(cfg: OpenClawConfig, env: NodeJS.ProcessEnv, homeDir: string) {
|
|
const agentId = "main";
|
|
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId, env, () => homeDir);
|
|
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
}
|
|
|
|
function stateIntegrityText(): string {
|
|
return vi
|
|
.mocked(note)
|
|
.mock.calls.filter((call) => call[1] === "State integrity")
|
|
.map((call) => String(call[0]))
|
|
.join("\n");
|
|
}
|
|
|
|
const OAUTH_PROMPT_MATCHER = expect.objectContaining({
|
|
message: expect.stringContaining("Create OAuth dir at"),
|
|
});
|
|
|
|
async function runStateIntegrity(cfg: OpenClawConfig) {
|
|
setupSessionState(cfg, process.env, process.env.HOME ?? "");
|
|
const confirmSkipInNonInteractive = vi.fn(async () => false);
|
|
await noteStateIntegrity(cfg, { confirmSkipInNonInteractive });
|
|
return confirmSkipInNonInteractive;
|
|
}
|
|
|
|
describe("doctor state integrity oauth dir checks", () => {
|
|
let envSnapshot: EnvSnapshot;
|
|
let tempHome = "";
|
|
|
|
beforeEach(() => {
|
|
envSnapshot = captureEnv();
|
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-integrity-"));
|
|
process.env.HOME = tempHome;
|
|
process.env.OPENCLAW_HOME = tempHome;
|
|
process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw");
|
|
delete process.env.OPENCLAW_OAUTH_DIR;
|
|
fs.mkdirSync(process.env.OPENCLAW_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
vi.mocked(note).mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreEnv(envSnapshot);
|
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not prompt for oauth dir when no whatsapp/pairing config is active", async () => {
|
|
const cfg: OpenClawConfig = {};
|
|
const confirmSkipInNonInteractive = await runStateIntegrity(cfg);
|
|
expect(confirmSkipInNonInteractive).not.toHaveBeenCalledWith(OAUTH_PROMPT_MATCHER);
|
|
const text = stateIntegrityText();
|
|
expect(text).toContain("OAuth dir not present");
|
|
expect(text).not.toContain("CRITICAL: OAuth dir missing");
|
|
});
|
|
|
|
it("prompts for oauth dir when whatsapp is configured", async () => {
|
|
const cfg: OpenClawConfig = {
|
|
channels: {
|
|
whatsapp: {},
|
|
},
|
|
};
|
|
const confirmSkipInNonInteractive = await runStateIntegrity(cfg);
|
|
expect(confirmSkipInNonInteractive).toHaveBeenCalledWith(OAUTH_PROMPT_MATCHER);
|
|
expect(stateIntegrityText()).toContain("CRITICAL: OAuth dir missing");
|
|
});
|
|
|
|
it("prompts for oauth dir when a channel dmPolicy is pairing", async () => {
|
|
const cfg: OpenClawConfig = {
|
|
channels: {
|
|
telegram: {
|
|
dmPolicy: "pairing",
|
|
},
|
|
},
|
|
};
|
|
const confirmSkipInNonInteractive = await runStateIntegrity(cfg);
|
|
expect(confirmSkipInNonInteractive).toHaveBeenCalledWith(OAUTH_PROMPT_MATCHER);
|
|
});
|
|
|
|
it("prompts for oauth dir when OPENCLAW_OAUTH_DIR is explicitly configured", async () => {
|
|
process.env.OPENCLAW_OAUTH_DIR = path.join(tempHome, ".oauth");
|
|
const cfg: OpenClawConfig = {};
|
|
const confirmSkipInNonInteractive = await runStateIntegrity(cfg);
|
|
expect(confirmSkipInNonInteractive).toHaveBeenCalledWith(OAUTH_PROMPT_MATCHER);
|
|
expect(stateIntegrityText()).toContain("CRITICAL: OAuth dir missing");
|
|
});
|
|
|
|
it("detects orphan transcripts and offers archival remediation", async () => {
|
|
const cfg: OpenClawConfig = {};
|
|
setupSessionState(cfg, process.env, process.env.HOME ?? "");
|
|
const sessionsDir = resolveSessionTranscriptsDirForAgent("main", process.env, () => tempHome);
|
|
fs.writeFileSync(path.join(sessionsDir, "orphan-session.jsonl"), '{"type":"session"}\n');
|
|
const confirmSkipInNonInteractive = vi.fn(async (params: { message: string }) =>
|
|
params.message.includes("orphan transcript file"),
|
|
);
|
|
await noteStateIntegrity(cfg, { confirmSkipInNonInteractive });
|
|
expect(stateIntegrityText()).toContain("orphan transcript file");
|
|
expect(confirmSkipInNonInteractive).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
message: expect.stringContaining("orphan transcript file"),
|
|
}),
|
|
);
|
|
const files = fs.readdirSync(sessionsDir);
|
|
expect(files.some((name) => name.startsWith("orphan-session.jsonl.deleted."))).toBe(true);
|
|
});
|
|
|
|
it("prints openclaw-only verification hints when recent sessions are missing transcripts", async () => {
|
|
const cfg: OpenClawConfig = {};
|
|
setupSessionState(cfg, process.env, process.env.HOME ?? "");
|
|
const storePath = resolveStorePath(cfg.session?.store, { agentId: "main" });
|
|
fs.writeFileSync(
|
|
storePath,
|
|
JSON.stringify(
|
|
{
|
|
"agent:main:main": {
|
|
sessionId: "missing-transcript",
|
|
updatedAt: Date.now(),
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
await noteStateIntegrity(cfg, { confirmSkipInNonInteractive: vi.fn(async () => false) });
|
|
|
|
const text = stateIntegrityText();
|
|
expect(text).toContain("recent sessions are missing transcripts");
|
|
expect(text).toMatch(/openclaw sessions --store ".*sessions\.json"/);
|
|
expect(text).toMatch(/openclaw sessions cleanup --store ".*sessions\.json" --dry-run/);
|
|
expect(text).not.toContain("--active");
|
|
expect(text).not.toContain(" ls ");
|
|
});
|
|
});
|