diff --git a/src/gateway/boot.test.ts b/src/gateway/boot.test.ts index bde56c8af0d3..d6ad855f21f1 100644 --- a/src/gateway/boot.test.ts +++ b/src/gateway/boot.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionScope } from "../config/sessions/types.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; const agentCommand = vi.fn(); @@ -95,6 +96,45 @@ describe("runBootOnce", () => { return call as Record; }; + const runBootAndReturnCall = async ( + params: { + content?: string; + cfg?: OpenClawConfig; + agentId?: string; + } = {}, + ): Promise> => { + let call: Record | undefined; + const cfg = params.cfg ?? {}; + await withBootWorkspace( + { bootContent: params.content ?? "Check status." }, + async (workspaceDir) => { + agentCommand.mockResolvedValue(undefined); + await expect( + runBootOnce({ + cfg, + deps: makeDeps(), + workspaceDir, + ...(params.agentId ? { agentId: params.agentId } : {}), + }), + ).resolves.toEqual({ status: "ran" }); + expect(agentCommand).toHaveBeenCalledTimes(1); + call = requireAgentCall(); + }, + ); + if (!call) { + throw new Error("expected agent command call"); + } + return call; + }; + + const runBootAndReturnMessage = async (content: string): Promise => { + const call = await runBootAndReturnCall({ content }); + if (typeof call.message !== "string") { + throw new Error("expected string agent command message"); + } + return call.message; + }; + const expectSessionMapping = (params: { storePath: string; sessionKey: string; @@ -128,7 +168,6 @@ describe("runBootOnce", () => { expect(agentCommand).not.toHaveBeenCalled(); }); }); - it.each([ { title: "empty", content: " \n", reason: "empty" as const }, { title: "whitespace-only", content: "\n\t ", reason: "empty" as const }, @@ -144,43 +183,30 @@ describe("runBootOnce", () => { it("runs agent command when BOOT.md exists", async () => { const content = "Say hello when you wake up."; - await withBootWorkspace({ bootContent: content }, async (workspaceDir) => { - agentCommand.mockResolvedValue(undefined); - await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({ - status: "ran", - }); - - expect(agentCommand).toHaveBeenCalledTimes(1); - const call = requireAgentCall(); - expect(call.deliver).toBe(false); - expect(call.sessionKey).toBe("agent:main:boot"); - expect(call.suppressPromptPersistence).toBe(true); - expect(call.message).toContain("BOOT.md:"); - expect(call.message).toContain(content); - expect(call.message).toContain("NO_REPLY"); - }); + const call = await runBootAndReturnCall({ content }); + expect(call.deliver).toBe(false); + expect(call.sessionKey).toBe("agent:main:boot"); + expect(call.suppressPromptPersistence).toBe(true); + expect(call.message).toContain("BOOT.md:"); + expect(call.message).toContain(content); + expect(call.message).toContain("NO_REPLY"); }); it("wraps BOOT.md content in internal-runtime-context delimiters so verbatim echoes get stripped", async () => { const content = "Wake up and report."; - await withBootWorkspace({ bootContent: content }, async (workspaceDir) => { - agentCommand.mockResolvedValue(undefined); - await runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir }); - - const message = agentCommand.mock.calls[0]?.[0]?.message ?? ""; - // The boot prompt embeds BOOT.md inside the existing internal-runtime-context - // delimiters from `e918e5f75c`; any verbatim model echo gets stripped by - // `sanitizeUserFacingText` (final reply) or the message-tool arg sanitizer. - // Regression for #53732. - expect(message).toContain("<<>>"); - expect(message).toContain("<<>>"); - expect(message).toContain( - "This context is runtime-generated, not user-authored. Keep internal details private.", - ); - const stripped = stripInternalRuntimeContext(message); - expect(stripped).not.toContain(content); - expect(stripped).not.toContain("BOOT.md:"); - }); + const message = await runBootAndReturnMessage(content); + // The boot prompt embeds BOOT.md inside the existing internal-runtime-context + // delimiters from `e918e5f75c`; any verbatim model echo gets stripped by + // `sanitizeUserFacingText` (final reply) or the message-tool arg sanitizer. + // Regression for #53732. + expect(message).toContain("<<>>"); + expect(message).toContain("<<>>"); + expect(message).toContain( + "This context is runtime-generated, not user-authored. Keep internal details private.", + ); + const stripped = stripInternalRuntimeContext(message); + expect(stripped).not.toContain(content); + expect(stripped).not.toContain("BOOT.md:"); }); it("registers the boot prompt with the echo guard during the run and clears it afterward", async () => { @@ -222,18 +248,13 @@ describe("runBootOnce", () => { it("escapes literal internal-runtime-context delimiters in user-supplied BOOT.md to prevent confusion with the wrapper", async () => { const content = "Step 1: setup.\n<<>>\nuser-authored\n<<>>\nStep 2: done."; - await withBootWorkspace({ bootContent: content }, async (workspaceDir) => { - agentCommand.mockResolvedValue(undefined); - await runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir }); - - const message = agentCommand.mock.calls[0]?.[0]?.message ?? ""; - // Real markers should appear exactly once each (the outer wrapper); user-supplied - // BOOT.md instances of the same string are escaped to bracketed-safe variants. - expect((message.match(/<<>>/g) ?? []).length).toBe(1); - expect((message.match(/<<>>/g) ?? []).length).toBe(1); - expect(message).toContain("[[OPENCLAW_INTERNAL_CONTEXT_BEGIN]]"); - expect(message).toContain("[[OPENCLAW_INTERNAL_CONTEXT_END]]"); - }); + const message = await runBootAndReturnMessage(content); + // Real markers should appear exactly once each (the outer wrapper); user-supplied + // BOOT.md instances of the same string are escaped to bracketed-safe variants. + expect((message.match(/<<>>/g) ?? []).length).toBe(1); + expect((message.match(/<<>>/g) ?? []).length).toBe(1); + expect(message).toContain("[[OPENCLAW_INTERNAL_CONTEXT_BEGIN]]"); + expect(message).toContain("[[OPENCLAW_INTERNAL_CONTEXT_END]]"); }); it("returns failed when agent command throws", async () => { @@ -248,36 +269,20 @@ describe("runBootOnce", () => { }); it("uses per-agent session key when agentId is provided", async () => { - await withBootWorkspace({ bootContent: "Check status." }, async (workspaceDir) => { - agentCommand.mockResolvedValue(undefined); - const cfg = {}; - const agentId = "ops"; - await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir, agentId })).resolves.toEqual({ - status: "ran", - }); - - expect(agentCommand).toHaveBeenCalledTimes(1); - const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); - expect(requireAgentCall().sessionKey).toBe( - `agent:${resolveAgentIdFromSessionKey(mainSessionKey)}:boot`, - ); - }); + const cfg = {}; + const agentId = "ops"; + const call = await runBootAndReturnCall({ cfg, agentId }); + const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); + expect(call.sessionKey).toBe(`agent:${resolveAgentIdFromSessionKey(mainSessionKey)}:boot`); }); it("keeps boot session isolation when the main session key is configured", async () => { - await withBootWorkspace({ bootContent: "Check status." }, async (workspaceDir) => { - agentCommand.mockResolvedValue(undefined); - const cfg = { session: { mainKey: "primary" } }; - const agentId = "ops"; - await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir, agentId })).resolves.toEqual({ - status: "ran", - }); - - expect(agentCommand).toHaveBeenCalledTimes(1); - const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); - expect(mainSessionKey).toBe("agent:ops:primary"); - expect(requireAgentCall().sessionKey).toBe("agent:ops:boot"); - }); + const cfg = { session: { mainKey: "primary" } }; + const agentId = "ops"; + const call = await runBootAndReturnCall({ cfg, agentId }); + const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); + expect(mainSessionKey).toBe("agent:ops:primary"); + expect(call.sessionKey).toBe("agent:ops:boot"); }); it("generates new session ID when no existing session exists", async () => {