From c643651199aa87d865680e1d23ce0fa25b7b5946 Mon Sep 17 00:00:00 2001 From: zeroaltitude Date: Sun, 8 Mar 2026 23:19:42 -0700 Subject: [PATCH] test: exercise slug-collision restoration path with deterministic Math.random MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test couldn't trigger a slug collision because the random suffix made same-filename generation virtually impossible. Pin Math.random to a fixed value so both handler calls produce the same fallback slug, exercising the preExistingContent !== null restoration branch. Verifies: second handler overwrites first file → late blockSessionSave retracts second write → first session's content is restored (not deleted). --- .../bundled/session-memory/handler.test.ts | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index c192ff5f624..8b8326ed300 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -647,59 +647,65 @@ describe("session-memory hook", () => { expect(memoryFiles).toHaveLength(0); }); - it("late-block retraction restores pre-existing file instead of deleting", async () => { + it("late-block retraction restores pre-existing file instead of deleting (slug collision)", async () => { const tempDir = await createCaseWorkspace("block-save-restore"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); const sessionFile = await writeWorkspaceFile({ dir: sessionsDir, name: "test-session.jsonl", - content: createMockSessionContent([{ role: "user", content: "new session" }]), + content: createMockSessionContent([{ role: "user", content: "first session" }]), }); - // Run the handler to create the memory file (captures the generated filename). - const event = createHookEvent("command", "new", "agent:main:main", { - cfg: { agents: { defaults: { workspace: tempDir } } } satisfies OpenClawConfig, - previousSessionEntry: { sessionId: "s1", sessionFile }, - }); - await handler(event); - await drainPostHookActions(event); + // Pin Math.random to force deterministic slug — both handler calls + // produce the same fallback filename, exercising the slug-collision + // restoration path (preExistingContent !== null). + const origRandom = Math.random; + Math.random = () => 0.5; - const memoryDir = path.join(tempDir, "memory"); - const files1 = (await fs.readdir(memoryDir)).filter((f) => f.endsWith(".md")); - expect(files1).toHaveLength(1); - const existingFile = files1[0]; - const existingPath = path.join(memoryDir, existingFile); + try { + // First handler: creates memory file with deterministic slug. + const event1 = createHookEvent("command", "new", "agent:main:main", { + cfg: { agents: { defaults: { workspace: tempDir } } } satisfies OpenClawConfig, + previousSessionEntry: { sessionId: "s1", sessionFile }, + }); + await handler(event1); + await drainPostHookActions(event1); - // Replace the file content with known "prior" content to simulate a - // pre-existing memory file that would be at risk during slug collision. - await fs.writeFile(existingPath, "prior session content"); + const memoryDir = path.join(tempDir, "memory"); + const files1 = (await fs.readdir(memoryDir)).filter((f) => f.endsWith(".md")); + expect(files1).toHaveLength(1); + const collidingFile = files1[0]; + const collidingPath = path.join(memoryDir, collidingFile); + const originalContent = await fs.readFile(collidingPath, "utf-8"); + expect(originalContent).toContain("first session"); - // Now run a second handler that writes to the SAME filename. - // We simulate this by using the same session (same slug output). - const event2 = createHookEvent("command", "new", "agent:main:main", { - cfg: { agents: { defaults: { workspace: tempDir } } } satisfies OpenClawConfig, - previousSessionEntry: { sessionId: "s2", sessionFile }, - }); - await handler(event2); + // Second handler: same deterministic slug → overwrites the file (collision). + const sessionFile2 = await writeWorkspaceFile({ + dir: sessionsDir, + name: "test-session2.jsonl", + content: createMockSessionContent([{ role: "user", content: "second session" }]), + }); + const event2 = createHookEvent("command", "new", "agent:main:main", { + cfg: { agents: { defaults: { workspace: tempDir } } } satisfies OpenClawConfig, + previousSessionEntry: { sessionId: "s2", sessionFile: sessionFile2 }, + }); + await handler(event2); - // Verify inline write happened (file content changed from "prior session content"). - // Note: if slugs differ, this test just validates the non-collision retraction path. - // The fix ensures correctness in both cases — collision restores, no-collision deletes. + // Verify the file was overwritten by second handler. + const overwrittenContent = await fs.readFile(collidingPath, "utf-8"); + expect(overwrittenContent).toContain("second session"); - // Late-block: a later hook sets blockSessionSave - event2.context.blockSessionSave = true; - await drainPostHookActions(event2); + // Late-block: retraction should restore the FIRST session's content. + event2.context.blockSessionSave = true; + await drainPostHookActions(event2); - // The SECOND handler's file should be retracted. - // If it was the same filename (collision), the prior content should be restored. - // If different filename, that file should be deleted and the original file untouched. - const files2 = (await fs.readdir(memoryDir)).filter((f) => f.endsWith(".md")); - - // The original file must survive regardless of collision - expect(files2).toContain(existingFile); - const content = await fs.readFile(existingPath, "utf-8"); - expect(content).toBe("prior session content"); + const restoredContent = await fs.readFile(collidingPath, "utf-8"); + expect(restoredContent).toContain("first session"); + expect(restoredContent).not.toContain("second session"); + } finally { + Math.random = origRandom; + } }); it("sessionSaveContent (pre-set) overrides saved content", async () => {