refactor(test): dedupe agent harnesses and routing fixtures

This commit is contained in:
Peter Steinberger
2026-02-18 04:48:40 +00:00
parent 8a9fddedc9
commit 31f83c86b2
12 changed files with 440 additions and 755 deletions

View File

@@ -10,34 +10,35 @@ import {
markExited,
resetProcessRegistryForTests,
} from "./bash-process-registry.js";
import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js";
describe("bash process registry", () => {
function createRegistrySession(params: {
id?: string;
maxOutputChars: number;
pendingMaxOutputChars: number;
backgrounded: boolean;
}): ProcessSession {
return createProcessSessionFixture({
id: params.id ?? "sess",
command: "echo test",
child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams,
maxOutputChars: params.maxOutputChars,
pendingMaxOutputChars: params.pendingMaxOutputChars,
backgrounded: params.backgrounded,
});
}
beforeEach(() => {
resetProcessRegistryForTests();
});
it("captures output and truncates", () => {
const session: ProcessSession = {
id: "sess",
command: "echo test",
child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams,
startedAt: Date.now(),
cwd: "/tmp",
const session = createRegistrySession({
maxOutputChars: 10,
pendingMaxOutputChars: 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: false,
};
});
addSession(session);
appendOutput(session, "stdout", "0123456789");
@@ -48,27 +49,11 @@ describe("bash process registry", () => {
});
it("caps pending output to avoid runaway polls", () => {
const session: ProcessSession = {
id: "sess",
command: "echo test",
child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams,
startedAt: Date.now(),
cwd: "/tmp",
const session = createRegistrySession({
maxOutputChars: 100_000,
pendingMaxOutputChars: 20_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: true,
};
});
addSession(session);
const payload = `${"a".repeat(70_000)}${"b".repeat(20_000)}`;
@@ -82,27 +67,11 @@ describe("bash process registry", () => {
});
it("respects max output cap when pending cap is larger", () => {
const session: ProcessSession = {
id: "sess",
command: "echo test",
child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams,
startedAt: Date.now(),
cwd: "/tmp",
const session = createRegistrySession({
maxOutputChars: 5_000,
pendingMaxOutputChars: 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: true,
};
});
addSession(session);
appendOutput(session, "stdout", "x".repeat(10_000));
@@ -113,27 +82,11 @@ describe("bash process registry", () => {
});
it("caps stdout and stderr independently", () => {
const session: ProcessSession = {
id: "sess",
command: "echo test",
child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams,
startedAt: Date.now(),
cwd: "/tmp",
const session = createRegistrySession({
maxOutputChars: 100,
pendingMaxOutputChars: 10,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: true,
};
});
addSession(session);
appendOutput(session, "stdout", "a".repeat(6));
@@ -147,27 +100,11 @@ describe("bash process registry", () => {
});
it("only persists finished sessions when backgrounded", () => {
const session: ProcessSession = {
id: "sess",
command: "echo test",
child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams,
startedAt: Date.now(),
cwd: "/tmp",
const session = createRegistrySession({
maxOutputChars: 100,
pendingMaxOutputChars: 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: false,
};
});
addSession(session);
markExited(session, 0, null, "completed");

View File

@@ -0,0 +1,42 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import type { ProcessSession } from "./bash-process-registry.js";
export function createProcessSessionFixture(params: {
id: string;
command?: string;
startedAt?: number;
cwd?: string;
maxOutputChars?: number;
pendingMaxOutputChars?: number;
backgrounded?: boolean;
pid?: number;
child?: ChildProcessWithoutNullStreams;
}): ProcessSession {
const session: ProcessSession = {
id: params.id,
command: params.command ?? "test",
startedAt: params.startedAt ?? Date.now(),
cwd: params.cwd ?? "/tmp",
maxOutputChars: params.maxOutputChars ?? 10_000,
pendingMaxOutputChars: params.pendingMaxOutputChars ?? 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: params.backgrounded ?? false,
};
if (params.pid !== undefined) {
session.pid = params.pid;
}
if (params.child) {
session.child = params.child;
}
return session;
}

View File

@@ -1,12 +1,12 @@
import { afterEach, expect, test, vi } from "vitest";
import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js";
import type { ProcessSession } from "./bash-process-registry.js";
import {
addSession,
appendOutput,
markExited,
resetProcessRegistryForTests,
} from "./bash-process-registry.js";
import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js";
import { createProcessTool } from "./bash-tools.process.js";
afterEach(() => {
@@ -14,32 +14,13 @@ afterEach(() => {
resetDiagnosticSessionStateForTest();
});
function createBackgroundSession(id: string): ProcessSession {
return {
id,
command: "test",
startedAt: Date.now(),
cwd: "/tmp",
maxOutputChars: 10_000,
pendingMaxOutputChars: 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: true,
};
}
function createProcessSessionHarness(sessionId: string) {
const processTool = createProcessTool();
const session = createBackgroundSession(sessionId);
const session = createProcessSessionFixture({
id: sessionId,
command: "test",
backgrounded: true,
});
addSession(session);
return { processTool, session };
}

View File

@@ -1,11 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProcessSession } from "./bash-process-registry.js";
import {
addSession,
getFinishedSession,
getSession,
resetProcessRegistryForTests,
} from "./bash-process-registry.js";
import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js";
import { createProcessTool } from "./bash-tools.process.js";
const { supervisorMock } = vi.hoisted(() => ({
@@ -30,28 +30,13 @@ vi.mock("../process/kill-tree.js", () => ({
killProcessTree: (...args: unknown[]) => killProcessTreeMock(...args),
}));
function createBackgroundSession(id: string, pid?: number): ProcessSession {
return {
function createBackgroundSession(id: string, pid?: number) {
return createProcessSessionFixture({
id,
command: "sleep 999",
startedAt: Date.now(),
cwd: "/tmp",
maxOutputChars: 10_000,
pendingMaxOutputChars: 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
pid,
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: true,
};
...(pid === undefined ? {} : { pid }),
});
}
describe("process tool supervisor cancellation", () => {

View File

@@ -13,15 +13,40 @@ vi.mock("../media/image-ops.js", () => ({
import "./test-helpers/fast-core-tools.js";
import { createOpenClawTools } from "./openclaw-tools.js";
describe("nodes camera_snap", () => {
beforeEach(() => {
callGateway.mockReset();
});
const NODE_ID = "mac-1";
const BASE_RUN_INPUT = { action: "run", node: NODE_ID, command: ["echo", "hi"] } as const;
function unexpectedGatewayMethod(method: unknown): never {
throw new Error(`unexpected method: ${String(method)}`);
}
function getNodesTool() {
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
return tool;
}
async function executeNodes(input: Record<string, unknown>) {
return getNodesTool().execute("call1", input as never);
}
function mockNodeList(commands?: string[]) {
return {
nodes: [{ nodeId: NODE_ID, ...(commands ? { commands } : {}) }],
};
}
beforeEach(() => {
callGateway.mockReset();
});
describe("nodes camera_snap", () => {
it("maps jpg payloads to image/jpeg", async () => {
callGateway.mockImplementation(async ({ method }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1" }] };
return mockNodeList();
}
if (method === "node.invoke") {
return {
@@ -33,17 +58,12 @@ describe("nodes camera_snap", () => {
},
};
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
const result = await tool.execute("call1", {
const result = await executeNodes({
action: "camera_snap",
node: "mac-1",
node: NODE_ID,
facing: "front",
});
@@ -55,7 +75,7 @@ describe("nodes camera_snap", () => {
it("passes deviceId when provided", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1" }] };
return mockNodeList();
}
if (method === "node.invoke") {
expect(params).toMatchObject({
@@ -71,17 +91,12 @@ describe("nodes camera_snap", () => {
},
};
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
await tool.execute("call1", {
await executeNodes({
action: "camera_snap",
node: "mac-1",
node: NODE_ID,
facing: "front",
deviceId: "cam-123",
});
@@ -89,18 +104,14 @@ describe("nodes camera_snap", () => {
});
describe("nodes run", () => {
beforeEach(() => {
callGateway.mockReset();
});
it("passes invoke and command timeouts", async () => {
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1", commands: ["system.run"] }] };
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
expect(params).toMatchObject({
nodeId: "mac-1",
nodeId: NODE_ID,
command: "system.run",
timeoutMs: 45_000,
params: {
@@ -114,18 +125,11 @@ describe("nodes run", () => {
payload: { stdout: "", stderr: "", exitCode: 0, success: true },
};
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
await tool.execute("call1", {
action: "run",
node: "mac-1",
command: ["echo", "hi"],
await executeNodes({
...BASE_RUN_INPUT,
cwd: "/tmp",
env: ["FOO=bar"],
commandTimeoutMs: 12_000,
@@ -138,7 +142,7 @@ describe("nodes run", () => {
let approvalId: string | null = null;
callGateway.mockImplementation(async ({ method, params }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1", commands: ["system.run"] }] };
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
invokeCalls += 1;
@@ -146,7 +150,7 @@ describe("nodes run", () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
}
expect(params).toMatchObject({
nodeId: "mac-1",
nodeId: NODE_ID,
command: "system.run",
params: {
command: ["echo", "hi"],
@@ -170,26 +174,17 @@ describe("nodes run", () => {
: null;
return { decision: "allow-once" };
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
await tool.execute("call1", {
action: "run",
node: "mac-1",
command: ["echo", "hi"],
});
await executeNodes(BASE_RUN_INPUT);
expect(invokeCalls).toBe(2);
});
it("fails with user denied when approval decision is deny", async () => {
callGateway.mockImplementation(async ({ method }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1", commands: ["system.run"] }] };
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
throw new Error("SYSTEM_RUN_DENIED: approval required");
@@ -197,32 +192,16 @@ describe("nodes run", () => {
if (method === "exec.approval.request") {
return { decision: "deny" };
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
await expect(
tool.execute("call1", {
action: "run",
node: "mac-1",
command: ["echo", "hi"],
}),
).rejects.toThrow("exec denied: user denied");
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: user denied");
});
it("fails closed for timeout and invalid approval decisions", async () => {
const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes");
if (!tool) {
throw new Error("missing nodes tool");
}
callGateway.mockImplementation(async ({ method }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1", commands: ["system.run"] }] };
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
throw new Error("SYSTEM_RUN_DENIED: approval required");
@@ -230,19 +209,13 @@ describe("nodes run", () => {
if (method === "exec.approval.request") {
return {};
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
await expect(
tool.execute("call1", {
action: "run",
node: "mac-1",
command: ["echo", "hi"],
}),
).rejects.toThrow("exec denied: approval timed out");
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: approval timed out");
callGateway.mockImplementation(async ({ method }) => {
if (method === "node.list") {
return { nodes: [{ nodeId: "mac-1", commands: ["system.run"] }] };
return mockNodeList(["system.run"]);
}
if (method === "node.invoke") {
throw new Error("SYSTEM_RUN_DENIED: approval required");
@@ -250,14 +223,10 @@ describe("nodes run", () => {
if (method === "exec.approval.request") {
return { decision: "allow-never" };
}
throw new Error(`unexpected method: ${String(method)}`);
return unexpectedGatewayMethod(method);
});
await expect(
tool.execute("call1", {
action: "run",
node: "mac-1",
command: ["echo", "hi"],
}),
).rejects.toThrow("exec denied: invalid approval decision");
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow(
"exec denied: invalid approval decision",
);
});
});

View File

@@ -85,6 +85,42 @@ function makeUser(text: string): AgentMessage {
return { role: "user", content: text, timestamp: Date.now() };
}
type ContextPruningSettings = NonNullable<ReturnType<typeof computeEffectiveSettings>>;
type PruneArgs = Parameters<typeof pruneContextMessages>[0];
type PruneOverrides = Omit<PruneArgs, "messages" | "settings" | "ctx">;
const CONTEXT_WINDOW_1000 = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
function makeAggressiveSettings(
overrides: Partial<ContextPruningSettings> = {},
): ContextPruningSettings {
return {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0,
hardClearRatio: 0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
...overrides,
};
}
function pruneWithAggressiveDefaults(
messages: AgentMessage[],
settingsOverrides: Partial<ContextPruningSettings> = {},
extra: PruneOverrides = {},
): AgentMessage[] {
return pruneContextMessages({
messages,
settings: makeAggressiveSettings(settingsOverrides),
ctx: CONTEXT_WINDOW_1000,
...extra,
});
}
type ContextHandler = (
event: { messages: AgentMessage[] },
ctx: ExtensionContext,
@@ -157,21 +193,7 @@ describe("context-pruning", () => {
}),
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 3,
softTrimRatio: 0.0,
hardClearRatio: 0.0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
};
const ctx = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx });
const next = pruneWithAggressiveDefaults(messages, { keepLastAssistants: 3 });
expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000));
expect(toolText(findToolResult(next, "t3"))).toContain("z".repeat(20_000));
@@ -180,16 +202,6 @@ describe("context-pruning", () => {
});
it("never prunes tool results before the first user message", () => {
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0.0,
hardClearRatio: 0.0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
};
const messages: AgentMessage[] = [
makeAssistant("bootstrap tool calls"),
makeToolResult({
@@ -206,13 +218,14 @@ describe("context-pruning", () => {
}),
];
const next = pruneContextMessages({
const next = pruneWithAggressiveDefaults(
messages,
settings,
ctx: { model: { contextWindow: 1000 } } as unknown as ExtensionContext,
isToolPrunable: () => true,
contextWindowTokensOverride: 1000,
});
{},
{
isToolPrunable: () => true,
contextWindowTokensOverride: 1000,
},
);
expect(toolText(findToolResult(next, "t0"))).toBe("x".repeat(20_000));
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
@@ -241,19 +254,11 @@ describe("context-pruning", () => {
}),
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
const next = pruneWithAggressiveDefaults(messages, {
keepLastAssistants: 1,
softTrimRatio: 10.0,
hardClearRatio: 0.0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
};
const ctx = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx });
softTrim: DEFAULT_CONTEXT_PRUNING_SETTINGS.softTrim,
});
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
expect(toolText(findToolResult(next, "t2"))).toBe("[cleared]");
@@ -273,19 +278,9 @@ describe("context-pruning", () => {
makeAssistant("a2"),
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0,
hardClearRatio: 0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
};
const next = pruneContextMessages({
messages,
settings,
settings: makeAggressiveSettings(),
ctx: { model: undefined } as unknown as ExtensionContext,
contextWindowTokensOverride: 1000,
});
@@ -297,15 +292,7 @@ describe("context-pruning", () => {
const sessionManager = {};
setContextPruningRuntime(sessionManager, {
settings: {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0,
hardClearRatio: 0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
},
settings: makeAggressiveSettings(),
contextWindowTokens: 1000,
isToolPrunable: () => true,
lastCacheTouchAt: Date.now() - DEFAULT_CONTEXT_PRUNING_SETTINGS.ttlMs - 1000,
@@ -336,15 +323,7 @@ describe("context-pruning", () => {
const lastTouch = Date.now() - DEFAULT_CONTEXT_PRUNING_SETTINGS.ttlMs - 1000;
setContextPruningRuntime(sessionManager, {
settings: {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0,
hardClearRatio: 0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
},
settings: makeAggressiveSettings(),
contextWindowTokens: 1000,
isToolPrunable: () => true,
lastCacheTouchAt: lastTouch,
@@ -392,21 +371,9 @@ describe("context-pruning", () => {
}),
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0.0,
hardClearRatio: 0.0,
minPrunableToolChars: 0,
const next = pruneWithAggressiveDefaults(messages, {
tools: { allow: ["ex*"], deny: ["exec"] },
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
};
const ctx = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx });
});
// Deny wins => exec is not pruned, even though allow matches.
expect(toolText(findToolResult(next, "t1"))).toContain("x".repeat(20_000));
@@ -424,20 +391,7 @@ describe("context-pruning", () => {
}),
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0.0,
hardClearRatio: 0.0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
};
const ctx = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx });
const next = pruneWithAggressiveDefaults(messages);
const tool = findToolResult(next, "t1");
if (!tool || tool.role !== "toolResult") {
@@ -463,18 +417,10 @@ describe("context-pruning", () => {
} as unknown as AgentMessage,
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0.0,
const next = pruneWithAggressiveDefaults(messages, {
hardClearRatio: 10.0,
softTrim: { maxChars: 5, headChars: 7, tailChars: 3 },
};
const ctx = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx });
});
const text = toolText(findToolResult(next, "t1"));
expect(text).toContain("AAAAA\nB");
@@ -492,20 +438,10 @@ describe("context-pruning", () => {
}),
];
const settings = {
...DEFAULT_CONTEXT_PRUNING_SETTINGS,
keepLastAssistants: 0,
softTrimRatio: 0.0,
const next = pruneWithAggressiveDefaults(messages, {
hardClearRatio: 10.0,
minPrunableToolChars: 0,
hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 6, tailChars: 6 },
};
const ctx = {
model: { contextWindow: 1000 },
} as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx });
});
const tool = findToolResult(next, "t1");
const text = toolText(tool);