|
| 1 | +import { spawn } from "node:child_process"; |
1 | 2 | import fsSync from "node:fs"; |
2 | 3 | import fs from "node:fs/promises"; |
3 | 4 | import os from "node:os"; |
4 | 5 | import path from "node:path"; |
5 | 6 | import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; |
6 | 7 | import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; |
| 8 | +import { SessionWriteLockStaleError } from "./session-write-lock-error.js"; |
7 | 9 |
|
8 | 10 | const FAKE_STARTTIME = 12345; |
9 | 11 | let testing: typeof import("./session-write-lock.js").testing; |
@@ -119,12 +121,13 @@ async function withSymlinkedSessionPaths( |
119 | 121 |
|
120 | 122 | async function expectActiveInProcessLockIsNotReclaimed(params?: { |
121 | 123 | legacyStarttime?: unknown; |
| 124 | + createdAt?: string; |
122 | 125 | }): Promise<void> { |
123 | 126 | await withTempSessionLockFile(async ({ sessionFile, lockPath }) => { |
124 | 127 | const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 }); |
125 | 128 | const lockPayload = { |
126 | 129 | pid: process.pid, |
127 | | - createdAt: new Date().toISOString(), |
| 130 | + createdAt: params?.createdAt ?? new Date().toISOString(), |
128 | 131 | ...(params && "legacyStarttime" in params ? { starttime: params.legacyStarttime } : {}), |
129 | 132 | }; |
130 | 133 | await fs.writeFile(lockPath, JSON.stringify(lockPayload), "utf8"); |
@@ -410,6 +413,46 @@ describe("acquireSessionWriteLock", () => { |
410 | 413 | }); |
411 | 414 | }); |
412 | 415 |
|
| 416 | + it("does not report or remove active in-process locks that pass staleMs", async () => { |
| 417 | + await expectActiveInProcessLockIsNotReclaimed({ |
| 418 | + createdAt: new Date(Date.now() - 120_000).toISOString(), |
| 419 | + }); |
| 420 | + }); |
| 421 | + |
| 422 | + it("reports live OpenClaw-owned stale locks without removing them", async () => { |
| 423 | + await withTempSessionLockFile(async ({ sessionFile, lockPath }) => { |
| 424 | + const owner = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)", "openclaw"], { |
| 425 | + stdio: "ignore", |
| 426 | + }); |
| 427 | + if (!owner.pid) { |
| 428 | + throw new Error("missing lock owner pid"); |
| 429 | + } |
| 430 | + await fs.writeFile( |
| 431 | + lockPath, |
| 432 | + JSON.stringify({ |
| 433 | + pid: owner.pid, |
| 434 | + createdAt: new Date(Date.now() - 120_000).toISOString(), |
| 435 | + }), |
| 436 | + "utf8", |
| 437 | + ); |
| 438 | + |
| 439 | + try { |
| 440 | + await expect( |
| 441 | + acquireSessionWriteLock({ sessionFile, timeoutMs: 500, staleMs: 10 }), |
| 442 | + ).rejects.toMatchObject({ |
| 443 | + name: "SessionWriteLockStaleError", |
| 444 | + staleReasons: ["too-old"], |
| 445 | + }); |
| 446 | + await expect( |
| 447 | + acquireSessionWriteLock({ sessionFile, timeoutMs: 500, staleMs: 10 }), |
| 448 | + ).rejects.toBeInstanceOf(SessionWriteLockStaleError); |
| 449 | + await expect(fs.access(lockPath)).resolves.toBeUndefined(); |
| 450 | + } finally { |
| 451 | + owner.kill("SIGTERM"); |
| 452 | + } |
| 453 | + }); |
| 454 | + }); |
| 455 | + |
413 | 456 | it("watchdog releases stale in-process locks", async () => { |
414 | 457 | const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lock-")); |
415 | 458 | const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); |
|
0 commit comments