From bbd6dfbe925cb6db84ce7070b7322d47e7b01cf5 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 25 Apr 2026 15:51:10 +0530 Subject: [PATCH] fix: cover CLI session prompt hash reuse (#69236) (thanks @stainlu) --- src/agents/cli-runner/prepare.test.ts | 68 +++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts index 96d68a2b7f0..e9685e63c42 100644 --- a/src/agents/cli-runner/prepare.test.ts +++ b/src/agents/cli-runner/prepare.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; import { __testing as cliBackendsTesting } from "../cli-backends.js"; +import { hashCliSessionText } from "../cli-session.js"; import { buildActiveMusicGenerationTaskPromptContextForSession } from "../music-generation-task-status.js"; import { buildActiveVideoGenerationTaskPromptContextForSession } from "../video-generation-task-status.js"; import { @@ -52,11 +53,15 @@ const mockBuildActiveMusicGenerationTaskPromptContextForSession = vi.mocked( buildActiveMusicGenerationTaskPromptContextForSession, ); -function createCliBackendConfig(params: { systemPromptOverride?: string } = {}): OpenClawConfig { +function createCliBackendConfig( + params: { systemPromptOverride?: string | null } = {}, +): OpenClawConfig { return { agents: { defaults: { - systemPromptOverride: params.systemPromptOverride ?? "test system prompt", + ...(params.systemPromptOverride !== null + ? { systemPromptOverride: params.systemPromptOverride ?? "test system prompt" } + : {}), cliBackends: { "test-cli": { command: "test-cli", @@ -308,7 +313,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => { model: "test-model", timeoutMs: 1_000, runId: "run-test-legacy-merge", - config: createCliBackendConfig(), + config: createCliBackendConfig({ systemPromptOverride: null }), }); expect(context.params.prompt).toBe("prompt prepend\n\nlegacy prepend\n\nlatest ask"); @@ -355,6 +360,63 @@ describe("shouldSkipLocalCliCredentialEpoch", () => { } }); + it("uses explicit static prompt text for CLI session reuse hashing", async () => { + const { dir, sessionFile } = createSessionFile(); + try { + const context = await prepareCliRunContext({ + sessionId: "session-test", + sessionFile, + workspaceDir: dir, + prompt: "latest ask", + provider: "test-cli", + model: "test-model", + timeoutMs: 1_000, + runId: "run-test-static-prompt", + extraSystemPrompt: "## Inbound Context\nchannel=telegram", + extraSystemPromptStatic: "", + cliSessionBinding: { + sessionId: "cli-session", + }, + config: createCliBackendConfig({ systemPromptOverride: null }), + }); + + expect(context.systemPrompt).toContain("## Inbound Context\nchannel=telegram"); + expect(context.extraSystemPromptHash).toBeUndefined(); + expect(context.reusableCliSession).toEqual({ sessionId: "cli-session" }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("ignores volatile prompt text when static prompt text matches", async () => { + const { dir, sessionFile } = createSessionFile(); + try { + const staticPrompt = "## Direct Context\nYou are in a Telegram direct conversation."; + const context = await prepareCliRunContext({ + sessionId: "session-test", + sessionFile, + workspaceDir: dir, + prompt: "latest ask", + provider: "test-cli", + model: "test-model", + timeoutMs: 1_000, + runId: "run-test-volatile-prompt", + extraSystemPrompt: `## Inbound Context\nchannel=heartbeat\n\n${staticPrompt}`, + extraSystemPromptStatic: staticPrompt, + cliSessionBinding: { + sessionId: "cli-session", + extraSystemPromptHash: hashCliSessionText(staticPrompt), + }, + config: createCliBackendConfig(), + }); + + expect(context.extraSystemPromptHash).toBe(hashCliSessionText(staticPrompt)); + expect(context.reusableCliSession).toEqual({ sessionId: "cli-session" }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it("applies direct-run prepend system context helpers on the CLI path", async () => { const { dir, sessionFile } = createSessionFile(); try {