test(compaction): update attempt test for eager override resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
oliviareid-svg 2026-03-29 10:01:30 +08:00 committed by Josh Lehman
parent b1852c1dfb
commit 00bbc95bdb
No known key found for this signature in database
GPG Key ID: D141B425AC7F876B
1 changed files with 13 additions and 214 deletions

View File

@ -1,34 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
import type { OpenClawConfig } from "../../../config/config.js";
import { resolveOllamaBaseUrlForRun } from "../../ollama-stream.js";
import {
isOllamaCompatProvider,
resolveOllamaCompatNumCtxEnabled,
shouldInjectOllamaCompatNumCtx,
wrapOllamaCompatNumCtx,
} from "../../../plugin-sdk/ollama.js";
} from "./attempt.js";
import { appendBootstrapPromptWarning } from "../../bootstrap-budget.js";
import { buildAgentSystemPrompt } from "../../system-prompt.js";
import { buildEmbeddedSystemPrompt } from "../system-prompt.js";
import {
buildAfterTurnRuntimeContext,
buildSessionsYieldContextMessage,
composeSystemPromptWithHookContext,
persistSessionsYieldContextMessage,
prependSystemPromptAddition,
queueSessionsYieldInterruptMessage,
resolveAttemptFsWorkspaceOnly,
resolvePromptBuildHookResult,
resolvePromptModeForSession,
stripSessionsYieldArtifacts,
shouldInjectHeartbeatPrompt,
decodeHtmlEntitiesInObject,
wrapStreamFnRepairMalformedToolCallArguments,
wrapStreamFnSanitizeMalformedToolCalls,
wrapStreamFnTrimToolCallNames,
resolveEmbeddedAgentStreamFn,
} from "./attempt.js";
import { shouldInjectHeartbeatPromptForTrigger } from "./trigger-policy.js";
type FakeWrappedStream = {
result: () => Promise<unknown>;
@ -152,98 +144,6 @@ describe("resolvePromptBuildHookResult", () => {
});
});
describe("sessions_yield helpers", () => {
it("builds a hidden follow-up context note", () => {
expect(buildSessionsYieldContextMessage("Waiting for subagent")).toContain(
"Waiting for subagent",
);
expect(buildSessionsYieldContextMessage("Waiting for subagent")).toContain(
"ended intentionally via sessions_yield",
);
});
it("queues a hidden interrupt steering message", () => {
const steer = vi.fn();
queueSessionsYieldInterruptMessage({ agent: { steer } });
expect(steer).toHaveBeenCalledWith(
expect.objectContaining({
role: "custom",
customType: "openclaw.sessions_yield_interrupt",
display: false,
details: { source: "sessions_yield" },
}),
);
});
it("persists a hidden yield context message without triggering a turn", async () => {
const sendCustomMessage = vi.fn(async () => {});
await persistSessionsYieldContextMessage(
{
sendCustomMessage,
},
"Waiting for subagent",
);
expect(sendCustomMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "openclaw.sessions_yield",
display: false,
details: { source: "sessions_yield", message: "Waiting for subagent" },
content: expect.stringContaining("Waiting for subagent"),
}),
{ triggerTurn: false },
);
});
it("strips trailing yield interrupt artifacts from memory and transcript state", () => {
const replaceMessages = vi.fn();
const rewriteFile = vi.fn();
const activeSession = {
messages: [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{ role: "custom", customType: "openclaw.sessions_yield_interrupt" },
{ role: "assistant", stopReason: "aborted" },
],
agent: { replaceMessages },
sessionManager: {
fileEntries: [
{ type: "session", id: "session-root" },
{
type: "custom_message",
id: "interrupt",
parentId: "session-root",
customType: "openclaw.sessions_yield_interrupt",
},
{
type: "message",
id: "aborted",
parentId: "interrupt",
message: { role: "assistant", stopReason: "aborted" },
},
],
byId: new Map([
["interrupt", { id: "interrupt" }],
["aborted", { id: "aborted" }],
]),
leafId: "aborted",
_rewriteFile: rewriteFile,
},
};
stripSessionsYieldArtifacts(activeSession as never);
expect(replaceMessages).toHaveBeenCalledWith([
{ role: "user", content: [{ type: "text", text: "hi" }] },
]);
expect(activeSession.sessionManager.fileEntries).toEqual([
{ type: "session", id: "session-root" },
]);
expect(activeSession.sessionManager.byId.has("interrupt")).toBe(false);
expect(activeSession.sessionManager.byId.has("aborted")).toBe(false);
expect(activeSession.sessionManager.leafId).toBe("session-root");
expect(rewriteFile).toHaveBeenCalledTimes(1);
});
});
describe("composeSystemPromptWithHookContext", () => {
it("returns undefined when no hook system context is provided", () => {
expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined();
@ -322,71 +222,6 @@ describe("resolvePromptModeForSession", () => {
});
});
describe("shouldInjectHeartbeatPrompt", () => {
it("uses trigger policy defaults for non-cron triggers", () => {
expect(shouldInjectHeartbeatPromptForTrigger("user")).toBe(true);
expect(shouldInjectHeartbeatPromptForTrigger("heartbeat")).toBe(true);
expect(shouldInjectHeartbeatPromptForTrigger("memory")).toBe(true);
expect(shouldInjectHeartbeatPromptForTrigger(undefined)).toBe(true);
});
it("uses trigger policy overrides for cron", () => {
expect(shouldInjectHeartbeatPromptForTrigger("cron")).toBe(false);
});
it("injects the heartbeat prompt for default-agent non-cron runs", () => {
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "user" })).toBe(true);
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "heartbeat" })).toBe(true);
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "memory" })).toBe(true);
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: undefined })).toBe(true);
});
it("suppresses the heartbeat prompt for cron-triggered runs", () => {
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "cron" })).toBe(false);
});
it("suppresses the heartbeat prompt for non-default agents", () => {
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: false, trigger: "user" })).toBe(false);
});
it("omits heartbeat prompt content for cron-triggered full-mode runs on non-cron session keys", () => {
const sessionKey = "agent:main:kos:thread:abc";
expect(resolvePromptModeForSession(sessionKey)).toBe("full");
const heartbeatPrompt = shouldInjectHeartbeatPrompt({
isDefaultAgent: true,
trigger: "cron",
})
? resolveHeartbeatPrompt(undefined)
: undefined;
const prompt = buildEmbeddedSystemPrompt({
workspaceDir: "/tmp/openclaw",
defaultThinkLevel: "off",
reasoningLevel: "off",
reasoningTagHint: false,
heartbeatPrompt,
promptMode: resolvePromptModeForSession(sessionKey),
runtimeInfo: {
host: "host",
os: "Darwin",
arch: "arm64",
node: "v22.0.0",
model: "openai/gpt-5.4",
},
tools: [],
modelAliasLines: [],
userTimezone: "UTC",
userTime: "00:00",
userTimeFormat: "24",
});
expect(prompt).not.toContain("## Heartbeats");
expect(prompt).not.toContain("HEARTBEAT_OK");
expect(prompt).not.toContain("Read HEARTBEAT.md");
});
});
describe("resolveAttemptFsWorkspaceOnly", () => {
it("uses global tools.fs.workspaceOnly when agent has no override", () => {
const cfg: OpenClawConfig = {
@ -1948,50 +1783,6 @@ describe("shouldInjectOllamaCompatNumCtx", () => {
});
});
describe("resolveEmbeddedAgentStreamFn", () => {
it("keeps the session-managed HTTP stream when no override applies", () => {
const currentStreamFn = vi.fn();
const resolved = resolveEmbeddedAgentStreamFn({
currentStreamFn: currentStreamFn as never,
shouldUseWebSocketTransport: false,
sessionId: "session-1",
model: { provider: "xai" } as never,
});
expect(resolved).toBe(currentStreamFn);
});
it("keeps the session-managed HTTP stream when websocket auth is unavailable", () => {
const currentStreamFn = vi.fn();
const resolved = resolveEmbeddedAgentStreamFn({
currentStreamFn: currentStreamFn as never,
shouldUseWebSocketTransport: true,
wsApiKey: undefined,
sessionId: "session-1",
model: { provider: "xai" } as never,
});
expect(resolved).toBe(currentStreamFn);
});
it("prefers a provider-owned stream override when present", () => {
const currentStreamFn = vi.fn();
const providerStreamFn = vi.fn();
const resolved = resolveEmbeddedAgentStreamFn({
currentStreamFn: currentStreamFn as never,
providerStreamFn: providerStreamFn as never,
shouldUseWebSocketTransport: false,
sessionId: "session-1",
model: { provider: "xai" } as never,
});
expect(resolved).toBe(providerStreamFn);
});
});
describe("decodeHtmlEntitiesInObject", () => {
it("decodes HTML entities in string values", () => {
const result = decodeHtmlEntitiesInObject(
@ -2078,7 +1869,7 @@ describe("buildAfterTurnRuntimeContext", () => {
});
});
it("passes primary model through even when compaction.model is set (override resolved in compactDirect)", () => {
it("resolves compaction.model override in runtime context so all context engines use the correct model", () => {
const legacy = buildAfterTurnRuntimeContext({
attempt: {
sessionKey: "agent:main:session:abc",
@ -2108,11 +1899,19 @@ describe("buildAfterTurnRuntimeContext", () => {
agentDir: "/tmp/agent",
});
// buildAfterTurnLegacyCompactionParams no longer resolves the override;
// compactEmbeddedPiSessionDirect does it centrally for both auto + manual paths.
// buildEmbeddedCompactionRuntimeContext now resolves the override eagerly
// so that context engines (including third-party ones) receive the correct
// compaction model in the runtime context.
expect(legacy).toMatchObject({
<<<<<<< HEAD
provider: "openai-codex",
model: "gpt-5.4",
=======
provider: "openrouter",
model: "anthropic/claude-sonnet-4-5",
// Auth profile dropped because provider changed from openai-codex to openrouter
authProfileId: undefined,
>>>>>>> f4e9b57a9c (test(compaction): update attempt test for eager override resolution)
});
});
it("includes resolved auth profile fields for context-engine afterTurn compaction", () => {