mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:11:23 +00:00
refactor(test): dedupe agent harnesses and routing fixtures
This commit is contained in:
@@ -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");
|
||||
|
||||
42
src/agents/bash-process-registry.test-helpers.ts
Normal file
42
src/agents/bash-process-registry.test-helpers.ts
Normal 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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user