diff --git a/src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts b/src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts index 572788b4ad7..884ba9aea57 100644 --- a/src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts @@ -5,6 +5,34 @@ import type { Api, Model } from "@mariozechner/pi-ai"; import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; import { describe, expect, it, vi } from "vitest"; +const MEMORY_RELATIVE_PATH = "memory/2026-03-24.md"; + +function createAttemptParams(workspaceDir: string) { + return { + sessionId: "session-memory-flush", + sessionKey: "agent:main", + sessionFile: path.join(workspaceDir, "session.json"), + workspaceDir, + prompt: "flush durable notes", + timeoutMs: 30_000, + runId: "run-memory-flush", + provider: "openai", + modelId: "gpt-5.4", + model: { + api: "responses", + provider: "openai", + id: "gpt-5.4", + input: ["text"], + contextWindow: 128_000, + } as Model, + authStorage: {} as AuthStorage, + modelRegistry: {} as ModelRegistry, + thinkLevel: "off" as const, + trigger: "memory" as const, + memoryFlushWritePath: MEMORY_RELATIVE_PATH, + }; +} + describe("runEmbeddedAttempt memory flush tool forwarding", () => { it("forwards memory trigger metadata into tool creation so append-only guards activate", async () => { vi.resetModules(); @@ -28,40 +56,61 @@ describe("runEmbeddedAttempt memory flush tool forwarding", () => { const { runEmbeddedAttempt } = await import("./attempt.js"); - await expect( - runEmbeddedAttempt({ - sessionId: "session-memory-flush", - sessionKey: "agent:main", - sessionFile: path.join(workspaceDir, "session.json"), - workspaceDir, - prompt: "flush durable notes", - timeoutMs: 30_000, - runId: "run-memory-flush", - provider: "openai", - modelId: "gpt-5.4", - model: { - api: "responses", - provider: "openai", - id: "gpt-5.4", - input: ["text"], - contextWindow: 128_000, - } as Model, - authStorage: {} as AuthStorage, - modelRegistry: {} as ModelRegistry, - thinkLevel: "off", - trigger: "memory", - memoryFlushWritePath: "memory/2026-03-24.md", - }), - ).rejects.toBe(stop); + await expect(runEmbeddedAttempt(createAttemptParams(workspaceDir))).rejects.toBe(stop); expect(capturedOptions).toHaveLength(1); expect(capturedOptions[0]).toMatchObject({ trigger: "memory", - memoryFlushWritePath: "memory/2026-03-24.md", + memoryFlushWritePath: MEMORY_RELATIVE_PATH, }); } finally { vi.doUnmock("../../pi-tools.js"); await fs.rm(workspaceDir, { recursive: true, force: true }); } }); + + it("keeps the forwarded memory flush write tool append-only", async () => { + vi.resetModules(); + + const workspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), "openclaw-attempt-memory-flush-append-"), + ); + const memoryFile = path.join(workspaceDir, MEMORY_RELATIVE_PATH); + const stop = new Error("stop after append-only write check"); + let appendOnlyWrite: Promise | undefined; + + try { + await fs.mkdir(path.dirname(memoryFile), { recursive: true }); + await fs.writeFile(memoryFile, "seed", "utf-8"); + + vi.doMock("../../pi-tools.js", async () => { + const actual = + await vi.importActual("../../pi-tools.js"); + return { + ...actual, + createOpenClawCodingTools: vi.fn((options) => { + const tools = actual.createOpenClawCodingTools(options); + const writeTool = tools.find((tool) => tool.name === "write"); + expect(writeTool).toBeDefined(); + + appendOnlyWrite = writeTool!.execute("call-memory-flush", { + path: MEMORY_RELATIVE_PATH, + content: "new durable note", + }); + + throw stop; + }), + }; + }); + + const { runEmbeddedAttempt } = await import("./attempt.js"); + + await expect(runEmbeddedAttempt(createAttemptParams(workspaceDir))).rejects.toBe(stop); + await expect(appendOnlyWrite).resolves.toBeDefined(); + await expect(fs.readFile(memoryFile, "utf-8")).resolves.toBe("seed\nnew durable note"); + } finally { + vi.doUnmock("../../pi-tools.js"); + await fs.rm(workspaceDir, { recursive: true, force: true }); + } + }); });