Skip to content

Commit 378a36e

Browse files
committed
test(agents): cover memo no-poison contract for throwing readOwnerProcessArgs (#86509)
`readOwnerProcessArgs` helper one layer above the memo already catches resolver throws and returns null, so `cleanStaleLockFiles` does not propagate the error. The memo's addition is write-after-success ordering: a throwing resolver does not occupy the cache slot, so later locks for the same pid retry the resolver fresh instead of cache-hitting on a stale failure. Pin that with three same-pid locks against a throwing resolver and assert the resolver runs three times.
1 parent 71cd8ef commit 378a36e

1 file changed

Lines changed: 38 additions & 0 deletions

File tree

src/agents/session-write-lock.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,44 @@ describe("acquireSessionWriteLock", () => {
776776
}
777777
});
778778

779+
it("does not poison the per-pid memo when readOwnerProcessArgs throws (#86509)", async () => {
780+
// A helper one layer up (`readOwnerProcessArgs`) already catches thrown resolvers and
781+
// returns null, so `cleanStaleLockFiles` never propagates the throw — but a naive memo
782+
// could still cache that null-equivalent failure and short-circuit later locks for the
783+
// same pid. The fix writes the cache only after the resolver returns, so each lock
784+
// retries the resolver fresh after a throw.
785+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lock-"));
786+
const sessionsDir = path.join(root, "sessions");
787+
await fs.mkdir(sessionsDir, { recursive: true });
788+
const nowMs = Date.now();
789+
const lockCount = 3;
790+
try {
791+
for (let i = 0; i < lockCount; i++) {
792+
await fs.writeFile(
793+
path.join(sessionsDir, `throwing-${i}.jsonl.lock`),
794+
JSON.stringify({ pid: process.pid, createdAt: new Date(nowMs).toISOString() }),
795+
"utf8",
796+
);
797+
}
798+
let throwCalls = 0;
799+
const result = await cleanStaleLockFiles({
800+
sessionsDir,
801+
staleMs: 30_000,
802+
nowMs,
803+
removeStale: true,
804+
readOwnerProcessArgs: () => {
805+
throwCalls++;
806+
throw new Error("transient resolver failure");
807+
},
808+
});
809+
// Resolver is invoked once per lock — the throw is not cached as a no-args entry.
810+
expect(throwCalls).toBe(lockCount);
811+
expect(result.cleaned).toHaveLength(0);
812+
} finally {
813+
await fs.rm(root, { recursive: true, force: true });
814+
}
815+
});
816+
779817
it("keeps fresh live .jsonl lock files with OpenClaw or unknown owners", async () => {
780818
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lock-"));
781819
const sessionsDir = path.join(root, "sessions");

0 commit comments

Comments
 (0)