
























@@ -16,7 +16,12 @@ import {
1616moveHeartbeatMainSessionEntry,
1717resolveHeartbeatMainSessionRepairCandidate,
1818} from "./doctor-heartbeat-main-session-repair.js";
19-import { noteStateIntegrity } from "./doctor-state-integrity.js";
19+import {
20+detectStateIntegrityHealthIssues,
21+noteStateIntegrity,
22+stateIntegrityIssueToHealthFinding,
23+stateIntegrityIssueToRepairEffect,
24+} from "./doctor-state-integrity.js";
20252126vi.mock("../channels/plugins/bundled-ids.js", () => ({
2227listBundledChannelIds: () => ["matrix", "whatsapp"],
@@ -103,6 +108,123 @@ async function runStateIntegrityText(cfg: OpenClawConfig): Promise<string> {
103108return stateIntegrityText();
104109}
105110111+describe("structured state integrity findings", () => {
112+let envSnapshot: ReturnType<typeof captureEnv>;
113+let tempHome = "";
114+115+beforeEach(() => {
116+envSnapshot = captureEnv(["HOME", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]);
117+tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-state-integrity-"));
118+setTestEnvValue("HOME", tempHome);
119+setTestEnvValue("OPENCLAW_HOME", tempHome);
120+setTestEnvValue("OPENCLAW_STATE_DIR", path.join(tempHome, ".openclaw"));
121+});
122+123+afterEach(() => {
124+envSnapshot.restore();
125+fs.rmSync(tempHome, { recursive: true, force: true });
126+});
127+128+it("maps a missing state directory to a structured finding and dry-run effect", () => {
129+const issue = detectStateIntegrityHealthIssues({}).find(
130+(candidate) => candidate.kind === "missing-state-dir",
131+);
132+if (!issue) {
133+throw new Error("expected missing state directory issue");
134+}
135+136+expect(issue).toEqual({
137+kind: "missing-state-dir",
138+path: path.join(tempHome, ".openclaw"),
139+});
140+expect(stateIntegrityIssueToHealthFinding(issue)).toMatchObject({
141+checkId: "core/doctor/state-integrity",
142+severity: "error",
143+path: path.join(tempHome, ".openclaw"),
144+fixHint: "Run `openclaw doctor --fix` to create the state directory.",
145+});
146+expect(stateIntegrityIssueToRepairEffect(issue)).toEqual({
147+kind: "state",
148+action: "would-create-state-dir",
149+target: path.join(tempHome, ".openclaw"),
150+dryRunSafe: false,
151+});
152+});
153+154+it("reports permissive state and config file permissions as structured findings", () => {
155+if (process.platform === "win32") {
156+return;
157+}
158+const stateDir = path.join(tempHome, ".openclaw");
159+const configPath = path.join(tempHome, "openclaw.json");
160+fs.mkdirSync(stateDir, { recursive: true, mode: 0o755 });
161+fs.chmodSync(stateDir, 0o755);
162+fs.writeFileSync(configPath, "{}\n", { mode: 0o644 });
163+fs.chmodSync(configPath, 0o644);
164+165+const findings = detectStateIntegrityHealthIssues({}, { configPath }).map(
166+stateIntegrityIssueToHealthFinding,
167+);
168+169+expect(findings).toEqual(
170+expect.arrayContaining([
171+expect.objectContaining({
172+checkId: "core/doctor/state-integrity",
173+severity: "warning",
174+path: stateDir,
175+message: "State directory permissions are too open. Recommend chmod 700.",
176+}),
177+expect.objectContaining({
178+checkId: "core/doctor/state-integrity",
179+severity: "warning",
180+path: configPath,
181+message: "Config file is group/world readable. Recommend chmod 600.",
182+}),
183+]),
184+);
185+});
186+187+it("keeps checking config permissions when the state directory is missing", () => {
188+if (process.platform === "win32") {
189+return;
190+}
191+const stateDir = path.join(tempHome, ".openclaw");
192+const configPath = path.join(tempHome, "openclaw.json");
193+fs.writeFileSync(configPath, "{}\n", { mode: 0o644 });
194+fs.chmodSync(configPath, 0o644);
195+196+const findings = detectStateIntegrityHealthIssues({}, { configPath }).map(
197+stateIntegrityIssueToHealthFinding,
198+);
199+200+expect(findings).toEqual(
201+expect.arrayContaining([
202+expect.objectContaining({
203+checkId: "core/doctor/state-integrity",
204+severity: "error",
205+path: stateDir,
206+message:
207+"State directory is missing. Sessions, credentials, logs, and config are stored there.",
208+}),
209+expect.objectContaining({
210+checkId: "core/doctor/state-integrity",
211+severity: "warning",
212+path: configPath,
213+message: "Config file is group/world readable. Recommend chmod 600.",
214+}),
215+]),
216+);
217+expect(findings).not.toEqual(
218+expect.arrayContaining([
219+expect.objectContaining({
220+checkId: "core/doctor/state-integrity",
221+message: expect.stringContaining("runtime directory is missing"),
222+}),
223+]),
224+);
225+});
226+});
227+106228async function runOrphanTranscriptCheckWithQmdSessions(enabled: boolean, homeDir: string) {
107229const cfg: OpenClawConfig = {
108230memory: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。