





















@@ -13,7 +13,12 @@ vi.mock("../../packages/terminal-core/src/note.js", () => ({
1313 note,
1414}));
151516-import { noteSessionLockHealth } from "./doctor-session-locks.js";
16+import {
17+detectStaleSessionLocks,
18+noteSessionLockHealth,
19+sessionLockToHealthFinding,
20+sessionLockToRepairEffect,
21+} from "./doctor-session-locks.js";
17221823async function expectPathMissing(targetPath: string): Promise<void> {
1924try {
@@ -105,6 +110,154 @@ describe("noteSessionLockHealth", () => {
105110await expect(fs.access(freshLock)).resolves.toBeUndefined();
106111});
107112113+it("detects stale locks without removing them for structured lint", async () => {
114+const sessionsDir = state.sessionsDir();
115+await fs.mkdir(sessionsDir, { recursive: true });
116+117+const staleLock = path.join(sessionsDir, "stale.jsonl.lock");
118+const freshLock = path.join(sessionsDir, "fresh.jsonl.lock");
119+120+await fs.writeFile(
121+staleLock,
122+JSON.stringify({ pid: -1, createdAt: new Date(Date.now() - 120_000).toISOString() }),
123+"utf8",
124+);
125+await fs.writeFile(
126+freshLock,
127+JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }),
128+"utf8",
129+);
130+131+const locks = await detectStaleSessionLocks({
132+staleMs: 30_000,
133+readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"],
134+});
135+136+expect(locks).toHaveLength(1);
137+expect(locks[0]?.lockPath).toBe(staleLock);
138+await expect(fs.access(staleLock)).resolves.toBeUndefined();
139+await expect(fs.access(freshLock)).resolves.toBeUndefined();
140+});
141+142+it("maps stale locks to structured findings and dry-run effects", async () => {
143+const sessionsDir = state.sessionsDir();
144+await fs.mkdir(sessionsDir, { recursive: true });
145+const lockPath = path.join(sessionsDir, "stale.jsonl.lock");
146+await fs.writeFile(
147+lockPath,
148+JSON.stringify({ pid: -1, createdAt: new Date(Date.now() - 120_000).toISOString() }),
149+"utf8",
150+);
151+152+const [lock] = await detectStaleSessionLocks({
153+staleMs: 30_000,
154+readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"],
155+});
156+if (!lock) {
157+throw new Error("expected stale session lock");
158+}
159+160+expect(sessionLockToHealthFinding(lock)).toEqual(
161+expect.objectContaining({
162+checkId: "core/doctor/session-locks",
163+severity: "warning",
164+path: lockPath,
165+}),
166+);
167+expect(sessionLockToRepairEffect(lock)).toEqual({
168+kind: "state",
169+action: "would-remove-stale-session-lock",
170+target: lockPath,
171+dryRunSafe: false,
172+});
173+});
174+175+it("preserves fresh malformed stale locks in dry-run repair effects", async () => {
176+const sessionsDir = state.sessionsDir();
177+await fs.mkdir(sessionsDir, { recursive: true });
178+179+const malformedLock = path.join(sessionsDir, "malformed.jsonl.lock");
180+await fs.writeFile(malformedLock, "{}", "utf8");
181+182+const [lock] = await detectStaleSessionLocks({
183+staleMs: 30_000,
184+readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"],
185+});
186+if (!lock) {
187+throw new Error("expected stale session lock");
188+}
189+190+expect(lock.staleReasons).toEqual(["missing-pid", "invalid-createdAt"]);
191+expect(lock.removable).toBe(false);
192+expect(sessionLockToHealthFinding(lock).fixHint).toContain("after the cleanup grace period");
193+expect(sessionLockToRepairEffect(lock)).toEqual({
194+kind: "state",
195+action: "would-preserve-mtime-gated-stale-session-lock",
196+target: malformedLock,
197+dryRunSafe: false,
198+});
199+await expect(fs.access(malformedLock)).resolves.toBeUndefined();
200+});
201+202+it("uses the supplied env to choose the structured lint state dir", async () => {
203+const other = await createOpenClawTestState({
204+layout: "state-only",
205+prefix: "openclaw-doctor-locks-other-",
206+applyEnv: false,
207+});
208+try {
209+await fs.mkdir(other.sessionsDir(), { recursive: true });
210+const lockPath = path.join(other.sessionsDir(), "other-stale.jsonl.lock");
211+await fs.writeFile(
212+lockPath,
213+JSON.stringify({ pid: -1, createdAt: new Date(Date.now() - 120_000).toISOString() }),
214+"utf8",
215+);
216+217+const locks = await detectStaleSessionLocks({
218+env: other.env,
219+staleMs: 30_000,
220+readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"],
221+});
222+223+expect(locks.map((lock) => lock.lockPath)).toEqual([lockPath]);
224+} finally {
225+await other.cleanup();
226+}
227+});
228+229+it("preserves report-only live OpenClaw locks in dry-run repair effects", async () => {
230+const sessionsDir = state.sessionsDir();
231+await fs.mkdir(sessionsDir, { recursive: true });
232+233+const reportOnlyLock = path.join(sessionsDir, "report-only.jsonl.lock");
234+await fs.writeFile(
235+reportOnlyLock,
236+JSON.stringify({ pid: process.pid, createdAt: new Date(Date.now() - 45_000).toISOString() }),
237+"utf8",
238+);
239+240+const [lock] = await detectStaleSessionLocks({
241+staleMs: 30_000,
242+readOwnerProcessArgs: () => ["node", "/opt/openclaw/openclaw.mjs", "doctor"],
243+});
244+if (!lock) {
245+throw new Error("expected stale session lock");
246+}
247+248+expect(lock.staleReasons).toEqual(["too-old"]);
249+expect(sessionLockToHealthFinding(lock).fixHint).toBe(
250+"OpenClaw is preserving this live owned lock; inspect the owning process if it appears stuck.",
251+);
252+expect(sessionLockToRepairEffect(lock)).toEqual({
253+kind: "state",
254+action: "would-preserve-report-only-stale-session-lock",
255+target: reportOnlyLock,
256+dryRunSafe: false,
257+});
258+await expect(fs.access(reportOnlyLock)).resolves.toBeUndefined();
259+});
260+108261it("uses configured stale threshold without removing live OpenClaw lock files", async () => {
109262const sessionsDir = state.sessionsDir();
110263await fs.mkdir(sessionsDir, { recursive: true });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。