























@@ -3,6 +3,7 @@ import fsSync from "node:fs";
33import fs from "node:fs/promises";
44import os from "node:os";
55import path from "node:path";
6+import { fileURLToPath } from "node:url";
67import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
78import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
89import { SessionWriteLockStaleError } from "./session-write-lock-error.js";
@@ -87,6 +88,19 @@ async function writeCurrentProcessLock(lockPath: string, extra?: Record<string,
8788);
8889}
899091+function readFilePathToString(filePath: Parameters<typeof fs.readFile>[0]): string | undefined {
92+if (typeof filePath === "string") {
93+return filePath;
94+}
95+if (Buffer.isBuffer(filePath)) {
96+return filePath.toString("utf8");
97+}
98+if (filePath instanceof URL) {
99+return fileURLToPath(filePath);
100+}
101+return undefined;
102+}
103+90104async function withSymlinkedSessionPaths(
91105run: (params: {
92106sessionReal: string;
@@ -453,6 +467,157 @@ describe("acquireSessionWriteLock", () => {
453467});
454468});
455469470+it("retries when a stale lock report disappears before diagnostics", async () => {
471+await withTempSessionLockFile(async ({ sessionFile, lockPath }) => {
472+const owner = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)", "openclaw"], {
473+stdio: "ignore",
474+});
475+if (!owner.pid) {
476+throw new Error("missing lock owner pid");
477+}
478+await fs.writeFile(
479+lockPath,
480+JSON.stringify({
481+pid: owner.pid,
482+createdAt: new Date(Date.now() - 120_000).toISOString(),
483+}),
484+"utf8",
485+);
486+487+const originalReadFile = fs.readFile.bind(fs);
488+let lockReads = 0;
489+const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation((async (
490+filePath,
491+options,
492+) => {
493+const lockFilePath = readFilePathToString(filePath);
494+if (lockFilePath && path.basename(lockFilePath) === path.basename(lockPath)) {
495+lockReads += 1;
496+if (lockReads === 3) {
497+await fs.rm(lockFilePath, { force: true });
498+await fs.rm(lockPath, { force: true });
499+throw Object.assign(new Error("lock disappeared"), { code: "ENOENT" });
500+}
501+}
502+return await originalReadFile(filePath, options as never);
503+}) as typeof fs.readFile);
504+505+try {
506+const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500, staleMs: 10 });
507+await lock.release();
508+expect(lockReads).toBeGreaterThanOrEqual(3);
509+await expectPathMissing(lockPath);
510+} finally {
511+readFileSpy.mockRestore();
512+owner.kill("SIGTERM");
513+}
514+});
515+});
516+517+it("retries when a stale lock report is replaced before diagnostics", async () => {
518+await withTempSessionLockFile(async ({ sessionFile, lockPath }) => {
519+const owner = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)", "openclaw"], {
520+stdio: "ignore",
521+});
522+if (!owner.pid) {
523+throw new Error("missing lock owner pid");
524+}
525+await fs.writeFile(
526+lockPath,
527+JSON.stringify({
528+pid: owner.pid,
529+createdAt: new Date(Date.now() - 120_000).toISOString(),
530+}),
531+"utf8",
532+);
533+534+const originalReadFile = fs.readFile.bind(fs);
535+let lockReads = 0;
536+const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation((async (
537+filePath,
538+options,
539+) => {
540+const lockFilePath = readFilePathToString(filePath);
541+if (lockFilePath && path.basename(lockFilePath) === path.basename(lockPath)) {
542+lockReads += 1;
543+if (lockReads === 3) {
544+await fs.rm(lockFilePath, { force: true });
545+await fs.rm(lockPath, { force: true });
546+await fs.writeFile(
547+lockFilePath,
548+JSON.stringify({ pid: owner.pid, createdAt: new Date().toISOString() }),
549+"utf8",
550+);
551+setTimeout(() => {
552+void fs.rm(lockFilePath, { force: true });
553+}, 10);
554+throw Object.assign(new Error("lock disappeared"), { code: "ENOENT" });
555+}
556+}
557+return await originalReadFile(filePath, options as never);
558+}) as typeof fs.readFile);
559+560+try {
561+const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500, staleMs: 10 });
562+await lock.release();
563+expect(lockReads).toBeGreaterThanOrEqual(3);
564+await expectPathMissing(lockPath);
565+} finally {
566+readFileSpy.mockRestore();
567+owner.kill("SIGTERM");
568+}
569+});
570+});
571+572+it("retries when a stale lock report is replaced by a fresh payload-less lock", async () => {
573+await withTempSessionLockFile(async ({ sessionFile, lockPath }) => {
574+const owner = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)", "openclaw"], {
575+stdio: "ignore",
576+});
577+if (!owner.pid) {
578+throw new Error("missing lock owner pid");
579+}
580+await fs.writeFile(
581+lockPath,
582+JSON.stringify({
583+pid: owner.pid,
584+createdAt: new Date(Date.now() - 120_000).toISOString(),
585+}),
586+"utf8",
587+);
588+589+const originalReadFile = fs.readFile.bind(fs);
590+let lockReads = 0;
591+const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation((async (
592+filePath,
593+options,
594+) => {
595+const lockFilePath = readFilePathToString(filePath);
596+if (lockFilePath && path.basename(lockFilePath) === path.basename(lockPath)) {
597+lockReads += 1;
598+if (lockReads === 3) {
599+await fs.rm(lockFilePath, { force: true });
600+await fs.writeFile(lockFilePath, "", "utf8");
601+setTimeout(() => {
602+void fs.rm(lockFilePath, { force: true });
603+}, 10);
604+}
605+}
606+return await originalReadFile(filePath, options as never);
607+}) as typeof fs.readFile);
608+609+try {
610+const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 800, staleMs: 10 });
611+await lock.release();
612+expect(lockReads).toBeGreaterThanOrEqual(3);
613+await expectPathMissing(lockPath);
614+} finally {
615+readFileSpy.mockRestore();
616+owner.kill("SIGTERM");
617+}
618+});
619+});
620+456621it("watchdog releases stale in-process locks", async () => {
457622const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lock-"));
458623const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
@@ -600,6 +765,14 @@ describe("acquireSessionWriteLock", () => {
600765});
601766});
602767768+it("preserves one acquire timeout budget across retries", () => {
769+expect(testing.resolveRemainingAcquireTimeoutMs(500, 1_000, 1_125)).toBe(375);
770+expect(testing.resolveRemainingAcquireTimeoutMs(500, 1_000, 1_500)).toBe(0);
771+expect(testing.resolveRemainingAcquireTimeoutMs(Number.POSITIVE_INFINITY, 1_000, 9_000)).toBe(
772+Number.POSITIVE_INFINITY,
773+);
774+});
775+603776it("uses resolved stale policy when cleaning stale lock files", async () => {
604777const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lock-policy-"));
605778const sessionsDir = path.join(root, "sessions");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。