mirror of https://github.com/openclaw/openclaw.git
test(compaction): update attempt test for eager override resolution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b1852c1dfb
commit
00bbc95bdb
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue