Agents: stabilize overflow runner test harness

This commit is contained in:
Gustavo Madeira Santana
2026-03-18 01:06:43 +00:00
parent f2de673130
commit 53df7ff86d
7 changed files with 479 additions and 383 deletions

View File

@@ -0,0 +1,406 @@
import { vi, type Mock } from "vitest";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import type {
PluginHookAgentContext,
PluginHookBeforeAgentStartResult,
PluginHookBeforeModelResolveResult,
PluginHookBeforePromptBuildResult,
} from "../../plugins/types.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
type MockCompactionResult =
| {
ok: true;
compacted: true;
result: {
summary: string;
firstKeptEntryId?: string;
tokensBefore?: number;
tokensAfter?: number;
};
reason?: string;
}
| {
ok: false;
compacted: false;
reason: string;
result?: undefined;
};
export const mockedGlobalHookRunner = {
hasHooks: vi.fn((_hookName: string) => false),
runBeforeAgentStart: vi.fn(
async (
_event: { prompt: string; messages?: unknown[] },
_ctx: PluginHookAgentContext,
): Promise<PluginHookBeforeAgentStartResult | undefined> => undefined,
),
runBeforePromptBuild: vi.fn(
async (
_event: { prompt: string; messages: unknown[] },
_ctx: PluginHookAgentContext,
): Promise<PluginHookBeforePromptBuildResult | undefined> => undefined,
),
runBeforeModelResolve: vi.fn(
async (
_event: { prompt: string },
_ctx: PluginHookAgentContext,
): Promise<PluginHookBeforeModelResolveResult | undefined> => undefined,
),
runBeforeCompaction: vi.fn(async () => undefined),
runAfterCompaction: vi.fn(async () => undefined),
};
export const mockedContextEngine = {
info: { ownsCompaction: false as boolean },
compact: vi.fn<(params: unknown) => Promise<MockCompactionResult>>(async () => ({
ok: false as const,
compacted: false as const,
reason: "nothing to compact",
})),
};
export const mockedContextEngineCompact = mockedContextEngine.compact;
export const mockedCompactDirect = mockedContextEngine.compact;
export const mockedEnsureRuntimePluginsLoaded = vi.fn<(params?: unknown) => void>();
export const mockedPrepareProviderRuntimeAuth = vi.fn(async () => undefined);
export const mockedRunEmbeddedAttempt =
vi.fn<(params: unknown) => Promise<EmbeddedRunAttemptResult>>();
export const mockedSessionLikelyHasOversizedToolResults = vi.fn(() => false);
export const mockedTruncateOversizedToolResultsInSession = vi.fn<
() => Promise<MockTruncateOversizedToolResultsResult>
>(async () => ({
truncated: false,
truncatedCount: 0,
reason: "no oversized tool results",
}));
type MockFailoverErrorDescription = {
message: string;
reason: string | undefined;
status: number | undefined;
code: string | undefined;
};
type MockCoerceToFailoverError = (
err: unknown,
params?: { provider?: string; model?: string; profileId?: string },
) => unknown;
type MockDescribeFailoverError = (err: unknown) => MockFailoverErrorDescription;
type MockResolveFailoverStatus = (reason: string) => number | undefined;
type MockTruncateOversizedToolResultsResult = {
truncated: boolean;
truncatedCount: number;
reason?: string;
};
export const mockedCoerceToFailoverError = vi.fn<MockCoerceToFailoverError>();
export const mockedDescribeFailoverError = vi.fn<MockDescribeFailoverError>(
(err: unknown): MockFailoverErrorDescription => ({
message: err instanceof Error ? err.message : String(err),
reason: undefined,
status: undefined,
code: undefined,
}),
);
export const mockedResolveFailoverStatus = vi.fn<MockResolveFailoverStatus>();
export const mockedLog: {
debug: Mock<(...args: unknown[]) => void>;
info: Mock<(...args: unknown[]) => void>;
warn: Mock<(...args: unknown[]) => void>;
error: Mock<(...args: unknown[]) => void>;
isEnabled: Mock<(level?: string) => boolean>;
} = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
isEnabled: vi.fn(() => false),
};
export const mockedClassifyFailoverReason = vi.fn(() => null);
export const mockedExtractObservedOverflowTokenCount = vi.fn((msg?: string) => {
const match = msg?.match(/prompt is too long:\s*([\d,]+)\s+tokens\s*>\s*[\d,]+\s+maximum/i);
return match?.[1] ? Number(match[1].replaceAll(",", "")) : undefined;
});
export const mockedIsCompactionFailureError = vi.fn(() => false);
export const mockedIsLikelyContextOverflowError = vi.fn((msg?: string) => {
const lower = (msg ?? "").toLowerCase();
return (
lower.includes("request_too_large") ||
lower.includes("context window exceeded") ||
lower.includes("prompt is too long")
);
});
export const mockedPickFallbackThinkingLevel = vi.fn<(params?: unknown) => ThinkLevel | null>(
() => null,
);
export const overflowBaseRunParams = {
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "/tmp/session.json",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
runId: "run-1",
} as const;
export function resetRunOverflowCompactionHarnessMocks(): void {
mockedGlobalHookRunner.hasHooks.mockReset();
mockedGlobalHookRunner.hasHooks.mockReturnValue(false);
mockedGlobalHookRunner.runBeforeAgentStart.mockReset();
mockedGlobalHookRunner.runBeforeAgentStart.mockResolvedValue(undefined);
mockedGlobalHookRunner.runBeforePromptBuild.mockReset();
mockedGlobalHookRunner.runBeforePromptBuild.mockResolvedValue(undefined);
mockedGlobalHookRunner.runBeforeModelResolve.mockReset();
mockedGlobalHookRunner.runBeforeModelResolve.mockResolvedValue(undefined);
mockedGlobalHookRunner.runBeforeCompaction.mockReset();
mockedGlobalHookRunner.runBeforeCompaction.mockResolvedValue(undefined);
mockedGlobalHookRunner.runAfterCompaction.mockReset();
mockedGlobalHookRunner.runAfterCompaction.mockResolvedValue(undefined);
mockedContextEngine.info.ownsCompaction = false;
mockedContextEngineCompact.mockReset();
mockedContextEngineCompact.mockResolvedValue({
ok: false,
compacted: false,
reason: "nothing to compact",
});
mockedEnsureRuntimePluginsLoaded.mockReset();
mockedPrepareProviderRuntimeAuth.mockReset();
mockedPrepareProviderRuntimeAuth.mockResolvedValue(undefined);
mockedRunEmbeddedAttempt.mockReset();
mockedSessionLikelyHasOversizedToolResults.mockReset();
mockedSessionLikelyHasOversizedToolResults.mockReturnValue(false);
mockedTruncateOversizedToolResultsInSession.mockReset();
mockedTruncateOversizedToolResultsInSession.mockResolvedValue({
truncated: false,
truncatedCount: 0,
reason: "no oversized tool results",
});
mockedCoerceToFailoverError.mockReset();
mockedCoerceToFailoverError.mockReturnValue(null);
mockedDescribeFailoverError.mockReset();
mockedDescribeFailoverError.mockImplementation(
(err: unknown): MockFailoverErrorDescription => ({
message: err instanceof Error ? err.message : String(err),
reason: undefined,
status: undefined,
code: undefined,
}),
);
mockedResolveFailoverStatus.mockReset();
mockedResolveFailoverStatus.mockReturnValue(undefined);
mockedLog.debug.mockReset();
mockedLog.info.mockReset();
mockedLog.warn.mockReset();
mockedLog.error.mockReset();
mockedLog.isEnabled.mockReset();
mockedLog.isEnabled.mockReturnValue(false);
mockedClassifyFailoverReason.mockReset();
mockedClassifyFailoverReason.mockReturnValue(null);
mockedExtractObservedOverflowTokenCount.mockReset();
mockedExtractObservedOverflowTokenCount.mockImplementation((msg?: string) => {
const match = msg?.match(/prompt is too long:\s*([\d,]+)\s+tokens\s*>\s*[\d,]+\s+maximum/i);
return match?.[1] ? Number(match[1].replaceAll(",", "")) : undefined;
});
mockedIsCompactionFailureError.mockReset();
mockedIsCompactionFailureError.mockReturnValue(false);
mockedIsLikelyContextOverflowError.mockReset();
mockedIsLikelyContextOverflowError.mockImplementation((msg?: string) => {
const lower = (msg ?? "").toLowerCase();
return (
lower.includes("request_too_large") ||
lower.includes("context window exceeded") ||
lower.includes("prompt is too long")
);
});
mockedPickFallbackThinkingLevel.mockReset();
mockedPickFallbackThinkingLevel.mockReturnValue(null);
}
export async function loadRunOverflowCompactionHarness(): Promise<{
runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
}> {
resetRunOverflowCompactionHarnessMocks();
vi.resetModules();
vi.doMock("../../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: vi.fn(() => mockedGlobalHookRunner),
}));
vi.doMock("../../context-engine/index.js", () => ({
ensureContextEnginesInitialized: vi.fn(),
resolveContextEngine: vi.fn(async () => mockedContextEngine),
}));
vi.doMock("../runtime-plugins.js", () => ({
ensureRuntimePluginsLoaded: mockedEnsureRuntimePluginsLoaded,
}));
vi.doMock("../../plugins/provider-runtime.js", () => ({
prepareProviderRuntimeAuth: mockedPrepareProviderRuntimeAuth,
}));
vi.doMock("../auth-profiles.js", () => ({
isProfileInCooldown: vi.fn(() => false),
markAuthProfileFailure: vi.fn(async () => {}),
markAuthProfileGood: vi.fn(async () => {}),
markAuthProfileUsed: vi.fn(async () => {}),
resolveProfilesUnavailableReason: vi.fn(() => undefined),
}));
vi.doMock("../usage.js", () => ({
normalizeUsage: vi.fn((usage?: unknown) =>
usage && typeof usage === "object" ? usage : undefined,
),
derivePromptTokens: vi.fn(
(usage?: { input?: number; cacheRead?: number; cacheWrite?: number }) =>
usage
? (() => {
const sum = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
return sum > 0 ? sum : undefined;
})()
: undefined,
),
}));
vi.doMock("../workspace-run.js", () => ({
resolveRunWorkspaceDir: vi.fn((params: { workspaceDir: string }) => ({
workspaceDir: params.workspaceDir,
usedFallback: false,
fallbackReason: undefined,
agentId: "main",
})),
redactRunIdentifier: vi.fn((value?: string) => value ?? ""),
}));
vi.doMock("../pi-embedded-helpers.js", () => ({
formatBillingErrorMessage: vi.fn(() => ""),
classifyFailoverReason: mockedClassifyFailoverReason,
extractObservedOverflowTokenCount: mockedExtractObservedOverflowTokenCount,
formatAssistantErrorText: vi.fn(() => ""),
isAuthAssistantError: vi.fn(() => false),
isBillingAssistantError: vi.fn(() => false),
isCompactionFailureError: mockedIsCompactionFailureError,
isLikelyContextOverflowError: mockedIsLikelyContextOverflowError,
isFailoverAssistantError: vi.fn(() => false),
isFailoverErrorMessage: vi.fn(() => false),
parseImageSizeError: vi.fn(() => null),
parseImageDimensionError: vi.fn(() => null),
isRateLimitAssistantError: vi.fn(() => false),
isTimeoutErrorMessage: vi.fn(() => false),
pickFallbackThinkingLevel: mockedPickFallbackThinkingLevel,
}));
vi.doMock("./run/attempt.js", () => ({
runEmbeddedAttempt: mockedRunEmbeddedAttempt,
}));
vi.doMock("./model.js", () => ({
resolveModelAsync: vi.fn(async () => ({
model: {
id: "test-model",
provider: "anthropic",
contextWindow: 200000,
api: "messages",
},
error: null,
authStorage: {
setRuntimeApiKey: vi.fn(),
},
modelRegistry: {},
})),
}));
vi.doMock("../model-auth.js", () => ({
applyLocalNoAuthHeaderOverride: vi.fn((model: unknown) => model),
ensureAuthProfileStore: vi.fn(() => ({})),
getApiKeyForModel: vi.fn(async () => ({
apiKey: "test-key",
profileId: "test-profile",
source: "test",
})),
resolveAuthProfileOrder: vi.fn(() => []),
}));
vi.doMock("../models-config.js", () => ({
ensureOpenClawModelsJson: vi.fn(async () => {}),
}));
vi.doMock("../context-window-guard.js", () => ({
CONTEXT_WINDOW_HARD_MIN_TOKENS: 1000,
CONTEXT_WINDOW_WARN_BELOW_TOKENS: 5000,
evaluateContextWindowGuard: vi.fn(() => ({
shouldWarn: false,
shouldBlock: false,
tokens: 200000,
source: "model",
})),
resolveContextWindowInfo: vi.fn(() => ({
tokens: 200000,
source: "model",
})),
}));
vi.doMock("../../process/command-queue.js", () => ({
enqueueCommandInLane: vi.fn((_lane: string, task: () => unknown) => task()),
}));
vi.doMock("../../utils/message-channel.js", () => ({
isMarkdownCapableMessageChannel: vi.fn(() => true),
}));
vi.doMock("../agent-paths.js", () => ({
resolveOpenClawAgentDir: vi.fn(() => "/tmp/agent-dir"),
}));
vi.doMock("../defaults.js", () => ({
DEFAULT_CONTEXT_TOKENS: 200000,
DEFAULT_MODEL: "test-model",
DEFAULT_PROVIDER: "anthropic",
}));
vi.doMock("../failover-error.js", () => ({
FailoverError: class extends Error {},
coerceToFailoverError: mockedCoerceToFailoverError,
describeFailoverError: mockedDescribeFailoverError,
resolveFailoverStatus: mockedResolveFailoverStatus,
}));
vi.doMock("./lanes.js", () => ({
resolveSessionLane: vi.fn(() => "session-lane"),
resolveGlobalLane: vi.fn(() => "global-lane"),
}));
vi.doMock("./logger.js", () => ({
log: mockedLog,
}));
vi.doMock("./run/payloads.js", () => ({
buildEmbeddedRunPayloads: vi.fn(() => []),
}));
vi.doMock("./tool-result-truncation.js", () => ({
truncateOversizedToolResultsInSession: mockedTruncateOversizedToolResultsInSession,
sessionLikelyHasOversizedToolResults: mockedSessionLikelyHasOversizedToolResults,
}));
vi.doMock("./utils.js", () => ({
describeUnknownError: vi.fn((err: unknown) => {
if (err instanceof Error) {
return err.message;
}
return String(err);
}),
}));
const { runEmbeddedPiAgent } = await import("./run.js");
return { runEmbeddedPiAgent };
}

View File

@@ -1,17 +1,4 @@
import "./run.overflow-compaction.mocks.shared.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { isCompactionFailureError, isLikelyContextOverflowError } from "../pi-embedded-helpers.js";
vi.mock(import("../../utils.js"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
resolveUserPath: vi.fn((p: string) => p),
};
});
import { log } from "./logger.js";
import { runEmbeddedPiAgent } from "./run.js";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import {
makeAttemptResult,
makeCompactionSuccess,
@@ -20,26 +7,38 @@ import {
queueOverflowAttemptWithOversizedToolOutput,
} from "./run.overflow-compaction.fixture.js";
import {
loadRunOverflowCompactionHarness,
mockedContextEngine,
mockedCompactDirect,
mockedIsCompactionFailureError,
mockedIsLikelyContextOverflowError,
mockedLog,
mockedRunEmbeddedAttempt,
mockedSessionLikelyHasOversizedToolResults,
mockedTruncateOversizedToolResultsInSession,
overflowBaseRunParams as baseParams,
} from "./run.overflow-compaction.shared-test.js";
} from "./run.overflow-compaction.harness.js";
import type { EmbeddedRunAttemptResult } from "./run/types.js";
const mockedIsCompactionFailureError = vi.mocked(isCompactionFailureError);
const mockedIsLikelyContextOverflowError = vi.mocked(isLikelyContextOverflowError);
let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
describe("overflow compaction in run loop", () => {
beforeAll(async () => {
({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness());
});
beforeEach(() => {
vi.clearAllMocks();
mockedRunEmbeddedAttempt.mockReset();
mockedCompactDirect.mockReset();
mockedSessionLikelyHasOversizedToolResults.mockReset();
mockedTruncateOversizedToolResultsInSession.mockReset();
mockedContextEngine.info.ownsCompaction = false;
mockedLog.debug.mockReset();
mockedLog.info.mockReset();
mockedLog.warn.mockReset();
mockedLog.error.mockReset();
mockedLog.isEnabled.mockReset();
mockedLog.isEnabled.mockReturnValue(false);
mockedIsCompactionFailureError.mockImplementation((msg?: string) => {
if (!msg) {
return false;
@@ -87,12 +86,14 @@ describe("overflow compaction in run loop", () => {
}),
);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(log.warn).toHaveBeenCalledWith(
expect(mockedLog.warn).toHaveBeenCalledWith(
expect.stringContaining(
"context overflow detected (attempt 1/3); attempting auto-compaction",
),
);
expect(log.info).toHaveBeenCalledWith(expect.stringContaining("auto-compaction succeeded"));
expect(mockedLog.info).toHaveBeenCalledWith(
expect.stringContaining("auto-compaction succeeded"),
);
// Should not be an error result
expect(result.meta.error).toBeUndefined();
});
@@ -116,7 +117,7 @@ describe("overflow compaction in run loop", () => {
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("source=promptError"));
expect(mockedLog.warn).toHaveBeenCalledWith(expect.stringContaining("source=promptError"));
expect(result.meta.error).toBeUndefined();
});
@@ -137,7 +138,7 @@ describe("overflow compaction in run loop", () => {
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
expect(result.meta.error?.kind).toBe("context_overflow");
expect(result.payloads?.[0]?.isError).toBe(true);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("auto-compaction failed"));
expect(mockedLog.warn).toHaveBeenCalledWith(expect.stringContaining("auto-compaction failed"));
});
it("falls back to tool-result truncation and retries when oversized results are detected", async () => {
@@ -165,7 +166,9 @@ describe("overflow compaction in run loop", () => {
expect.objectContaining({ sessionFile: "/tmp/session.json" }),
);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(log.info).toHaveBeenCalledWith(expect.stringContaining("Truncated 1 tool result(s)"));
expect(mockedLog.info).toHaveBeenCalledWith(
expect.stringContaining("Truncated 1 tool result(s)"),
);
expect(result.meta.error).toBeUndefined();
});
@@ -284,7 +287,7 @@ describe("overflow compaction in run loop", () => {
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("source=assistantError"));
expect(mockedLog.warn).toHaveBeenCalledWith(expect.stringContaining("source=assistantError"));
expect(result.meta.error).toBeUndefined();
});
@@ -302,7 +305,9 @@ describe("overflow compaction in run loop", () => {
await expect(runEmbeddedPiAgent(baseParams)).rejects.toThrow("transport disconnected");
expect(mockedCompactDirect).not.toHaveBeenCalled();
expect(log.warn).not.toHaveBeenCalledWith(expect.stringContaining("source=assistantError"));
expect(mockedLog.warn).not.toHaveBeenCalledWith(
expect.stringContaining("source=assistantError"),
);
});
it("returns an explicit timeout payload when the run times out before producing any reply", async () => {

View File

@@ -1,292 +0,0 @@
import { vi } from "vitest";
import type {
PluginHookAgentContext,
PluginHookBeforeAgentStartResult,
PluginHookBeforeModelResolveResult,
PluginHookBeforePromptBuildResult,
} from "../../plugins/types.js";
type MockCompactionResult =
| {
ok: true;
compacted: true;
result: {
summary: string;
firstKeptEntryId?: string;
tokensBefore?: number;
tokensAfter?: number;
};
reason?: string;
}
| {
ok: false;
compacted: false;
reason: string;
result?: undefined;
};
export const mockedGlobalHookRunner = {
hasHooks: vi.fn((_hookName: string) => false),
runBeforeAgentStart: vi.fn(
async (
_event: { prompt: string; messages?: unknown[] },
_ctx: PluginHookAgentContext,
): Promise<PluginHookBeforeAgentStartResult | undefined> => undefined,
),
runBeforePromptBuild: vi.fn(
async (
_event: { prompt: string; messages: unknown[] },
_ctx: PluginHookAgentContext,
): Promise<PluginHookBeforePromptBuildResult | undefined> => undefined,
),
runBeforeModelResolve: vi.fn(
async (
_event: { prompt: string },
_ctx: PluginHookAgentContext,
): Promise<PluginHookBeforeModelResolveResult | undefined> => undefined,
),
runBeforeCompaction: vi.fn(async () => undefined),
runAfterCompaction: vi.fn(async () => undefined),
};
export const mockedContextEngine = {
info: { ownsCompaction: false as boolean },
compact: vi.fn<(params: unknown) => Promise<MockCompactionResult>>(async () => ({
ok: false as const,
compacted: false as const,
reason: "nothing to compact",
})),
};
export const mockedContextEngineCompact = vi.mocked(mockedContextEngine.compact);
export const mockedEnsureRuntimePluginsLoaded: (...args: unknown[]) => void = vi.fn();
vi.mock("../../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: vi.fn(() => mockedGlobalHookRunner),
}));
vi.mock("../../context-engine/index.js", () => ({
ensureContextEnginesInitialized: vi.fn(),
resolveContextEngine: vi.fn(async () => mockedContextEngine),
}));
vi.mock("../runtime-plugins.js", () => ({
ensureRuntimePluginsLoaded: mockedEnsureRuntimePluginsLoaded,
}));
vi.mock("../auth-profiles.js", () => ({
isProfileInCooldown: vi.fn(() => false),
markAuthProfileFailure: vi.fn(async () => {}),
markAuthProfileGood: vi.fn(async () => {}),
markAuthProfileUsed: vi.fn(async () => {}),
}));
vi.mock("../usage.js", () => ({
normalizeUsage: vi.fn((usage?: unknown) =>
usage && typeof usage === "object" ? usage : undefined,
),
derivePromptTokens: vi.fn((usage?: { input?: number; cacheRead?: number; cacheWrite?: number }) =>
usage
? (() => {
const sum = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
return sum > 0 ? sum : undefined;
})()
: undefined,
),
hasNonzeroUsage: vi.fn(() => false),
}));
vi.mock("../workspace-run.js", () => ({
resolveRunWorkspaceDir: vi.fn((params: { workspaceDir: string }) => ({
workspaceDir: params.workspaceDir,
usedFallback: false,
fallbackReason: undefined,
agentId: "main",
})),
redactRunIdentifier: vi.fn((value?: string) => value ?? ""),
}));
vi.mock("../pi-embedded-helpers.js", () => ({
formatBillingErrorMessage: vi.fn(() => ""),
classifyFailoverReason: vi.fn(() => null),
extractObservedOverflowTokenCount: vi.fn((msg?: string) => {
const match = msg?.match(/prompt is too long:\s*([\d,]+)\s+tokens\s*>\s*[\d,]+\s+maximum/i);
return match?.[1] ? Number(match[1].replaceAll(",", "")) : undefined;
}),
formatAssistantErrorText: vi.fn(() => ""),
isAuthAssistantError: vi.fn(() => false),
isBillingAssistantError: vi.fn(() => false),
isCompactionFailureError: vi.fn(() => false),
isLikelyContextOverflowError: vi.fn((msg?: string) => {
const lower = (msg ?? "").toLowerCase();
return (
lower.includes("request_too_large") ||
lower.includes("context window exceeded") ||
lower.includes("prompt is too long")
);
}),
isFailoverAssistantError: vi.fn(() => false),
isFailoverErrorMessage: vi.fn(() => false),
parseImageSizeError: vi.fn(() => null),
parseImageDimensionError: vi.fn(() => null),
isRateLimitAssistantError: vi.fn(() => false),
isTimeoutErrorMessage: vi.fn(() => false),
pickFallbackThinkingLevel: vi.fn(() => null),
}));
vi.mock("./run/attempt.js", () => ({
runEmbeddedAttempt: vi.fn(),
}));
vi.mock("./compact.js", () => ({
compactEmbeddedPiSessionDirect: vi.fn(),
}));
vi.mock("./model.js", () => ({
resolveModel: vi.fn(() => ({
model: {
id: "test-model",
provider: "anthropic",
contextWindow: 200000,
api: "messages",
},
error: null,
authStorage: {
setRuntimeApiKey: vi.fn(),
},
modelRegistry: {},
})),
resolveModelAsync: vi.fn(async () => ({
model: {
id: "test-model",
provider: "anthropic",
contextWindow: 200000,
api: "messages",
},
error: null,
authStorage: {
setRuntimeApiKey: vi.fn(),
},
modelRegistry: {},
})),
}));
vi.mock("../model-auth.js", () => ({
ensureAuthProfileStore: vi.fn(() => ({})),
getApiKeyForModel: vi.fn(async () => ({
apiKey: "test-key",
profileId: "test-profile",
source: "test",
})),
resolveAuthProfileOrder: vi.fn(() => []),
}));
vi.mock("../models-config.js", () => ({
ensureOpenClawModelsJson: vi.fn(async () => {}),
}));
vi.mock("../context-window-guard.js", () => ({
CONTEXT_WINDOW_HARD_MIN_TOKENS: 1000,
CONTEXT_WINDOW_WARN_BELOW_TOKENS: 5000,
evaluateContextWindowGuard: vi.fn(() => ({
shouldWarn: false,
shouldBlock: false,
tokens: 200000,
source: "model",
})),
resolveContextWindowInfo: vi.fn(() => ({
tokens: 200000,
source: "model",
})),
}));
vi.mock("../../process/command-queue.js", () => ({
enqueueCommandInLane: vi.fn((_lane: string, task: () => unknown) => task()),
}));
vi.mock(import("../../utils/message-channel.js"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
isMarkdownCapableMessageChannel: vi.fn(() => true),
};
});
vi.mock("../agent-paths.js", () => ({
resolveOpenClawAgentDir: vi.fn(() => "/tmp/agent-dir"),
}));
vi.mock("../defaults.js", () => ({
DEFAULT_CONTEXT_TOKENS: 200000,
DEFAULT_MODEL: "test-model",
DEFAULT_PROVIDER: "anthropic",
}));
type MockFailoverErrorDescription = {
message: string;
reason: string | undefined;
status: number | undefined;
code: string | undefined;
};
type MockCoerceToFailoverError = (
err: unknown,
params?: { provider?: string; model?: string; profileId?: string },
) => unknown;
type MockDescribeFailoverError = (err: unknown) => MockFailoverErrorDescription;
type MockResolveFailoverStatus = (reason: string) => number | undefined;
export const mockedCoerceToFailoverError = vi.fn<MockCoerceToFailoverError>();
export const mockedDescribeFailoverError = vi.fn<MockDescribeFailoverError>(
(err: unknown): MockFailoverErrorDescription => ({
message: err instanceof Error ? err.message : String(err),
reason: undefined,
status: undefined,
code: undefined,
}),
);
export const mockedResolveFailoverStatus = vi.fn<MockResolveFailoverStatus>();
vi.mock("../failover-error.js", () => ({
FailoverError: class extends Error {},
coerceToFailoverError: mockedCoerceToFailoverError,
describeFailoverError: mockedDescribeFailoverError,
resolveFailoverStatus: mockedResolveFailoverStatus,
}));
vi.mock("./lanes.js", () => ({
resolveSessionLane: vi.fn(() => "session-lane"),
resolveGlobalLane: vi.fn(() => "global-lane"),
}));
vi.mock("./logger.js", () => ({
log: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
isEnabled: vi.fn(() => false),
},
}));
vi.mock("./run/payloads.js", () => ({
buildEmbeddedRunPayloads: vi.fn(() => []),
}));
vi.mock("./tool-result-truncation.js", () => ({
truncateOversizedToolResultsInSession: vi.fn(async () => ({
truncated: false,
truncatedCount: 0,
reason: "no oversized tool results",
})),
sessionLikelyHasOversizedToolResults: vi.fn(() => false),
}));
vi.mock("./utils.js", () => ({
describeUnknownError: vi.fn((err: unknown) => {
if (err instanceof Error) {
return err.message;
}
return String(err);
}),
}));

View File

@@ -1,30 +0,0 @@
import { vi } from "vitest";
import {
mockedContextEngine,
mockedContextEngineCompact,
} from "./run.overflow-compaction.mocks.shared.js";
import { runEmbeddedAttempt } from "./run/attempt.js";
import {
sessionLikelyHasOversizedToolResults,
truncateOversizedToolResultsInSession,
} from "./tool-result-truncation.js";
export const mockedRunEmbeddedAttempt = vi.mocked(runEmbeddedAttempt);
export const mockedCompactDirect = mockedContextEngineCompact;
export const mockedSessionLikelyHasOversizedToolResults = vi.mocked(
sessionLikelyHasOversizedToolResults,
);
export const mockedTruncateOversizedToolResultsInSession = vi.mocked(
truncateOversizedToolResultsInSession,
);
export { mockedContextEngine };
export const overflowBaseRunParams = {
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "/tmp/session.json",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
runId: "run-1",
} as const;

View File

@@ -1,7 +1,4 @@
import "./run.overflow-compaction.mocks.shared.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { pickFallbackThinkingLevel } from "../pi-embedded-helpers.js";
import { runEmbeddedPiAgent } from "./run.js";
import { beforeEach, describe, expect, it } from "vitest";
import {
makeAttemptResult,
makeCompactionSuccess,
@@ -10,24 +7,30 @@ import {
queueOverflowAttemptWithOversizedToolOutput,
} from "./run.overflow-compaction.fixture.js";
import {
loadRunOverflowCompactionHarness,
mockedCoerceToFailoverError,
mockedDescribeFailoverError,
mockedGlobalHookRunner,
mockedPickFallbackThinkingLevel,
mockedResolveFailoverStatus,
} from "./run.overflow-compaction.mocks.shared.js";
import {
mockedContextEngine,
mockedCompactDirect,
mockedRunEmbeddedAttempt,
mockedSessionLikelyHasOversizedToolResults,
mockedTruncateOversizedToolResultsInSession,
overflowBaseRunParams,
} from "./run.overflow-compaction.shared-test.js";
const mockedPickFallbackThinkingLevel = vi.mocked(pickFallbackThinkingLevel);
} from "./run.overflow-compaction.harness.js";
let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
beforeEach(() => {
vi.clearAllMocks();
return loadRunOverflowCompactionHarness().then((loaded) => {
runEmbeddedPiAgent = loaded.runEmbeddedPiAgent;
});
});
beforeEach(() => {
mockedRunEmbeddedAttempt.mockReset();
mockedCompactDirect.mockReset();
mockedCoerceToFailoverError.mockReset();
@@ -257,7 +260,8 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
it("returns retry_limit when repeated retries never converge", async () => {
mockedRunEmbeddedAttempt.mockClear();
mockedCompactDirect.mockClear();
mockedPickFallbackThinkingLevel.mockClear();
mockedPickFallbackThinkingLevel.mockReset();
mockedPickFallbackThinkingLevel.mockReturnValue(null);
mockedRunEmbeddedAttempt.mockResolvedValue(
makeAttemptResult({ promptError: new Error("unsupported reasoning mode") }),
);
@@ -288,15 +292,15 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
status: 429,
});
mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError }));
mockedCoerceToFailoverError.mockReturnValueOnce(normalized);
mockedRunEmbeddedAttempt.mockResolvedValue(makeAttemptResult({ promptError }));
mockedCoerceToFailoverError.mockReturnValue(normalized);
mockedDescribeFailoverError.mockImplementation((err: unknown) => ({
message: err instanceof Error ? err.message : String(err),
reason: err === normalized ? "rate_limit" : undefined,
status: err === normalized ? 429 : undefined,
code: undefined,
}));
mockedResolveFailoverStatus.mockReturnValueOnce(429);
mockedResolveFailoverStatus.mockReturnValue(429);
await expect(
runEmbeddedPiAgent({

View File

@@ -3,20 +3,25 @@
* with no pending tool calls, so the parent session is idle when subagent
* results arrive.
*/
import "./run.overflow-compaction.mocks.shared.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { runEmbeddedPiAgent } from "./run.js";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import { makeAttemptResult } from "./run.overflow-compaction.fixture.js";
import { mockedGlobalHookRunner } from "./run.overflow-compaction.mocks.shared.js";
import {
loadRunOverflowCompactionHarness,
mockedGlobalHookRunner,
mockedRunEmbeddedAttempt,
overflowBaseRunParams,
} from "./run.overflow-compaction.shared-test.js";
} from "./run.overflow-compaction.harness.js";
import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./runs.js";
let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
describe("sessions_yield orchestration", () => {
beforeAll(async () => {
({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness());
});
beforeEach(() => {
vi.clearAllMocks();
mockedRunEmbeddedAttempt.mockReset();
mockedGlobalHookRunner.hasHooks.mockImplementation(() => false);
});

View File

@@ -1,22 +1,20 @@
import "./run.overflow-compaction.mocks.shared.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import {
loadRunOverflowCompactionHarness,
mockedEnsureRuntimePluginsLoaded,
mockedRunEmbeddedAttempt,
} from "./run.overflow-compaction.harness.js";
const runtimePluginMocks = vi.hoisted(() => ({
ensureRuntimePluginsLoaded: vi.fn(),
}));
vi.mock("../runtime-plugins.js", () => ({
ensureRuntimePluginsLoaded: runtimePluginMocks.ensureRuntimePluginsLoaded,
}));
import { runEmbeddedPiAgent } from "./run.js";
import { runEmbeddedAttempt } from "./run/attempt.js";
const mockedRunEmbeddedAttempt = vi.mocked(runEmbeddedAttempt);
let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
describe("runEmbeddedPiAgent usage reporting", () => {
beforeAll(async () => {
({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness());
});
beforeEach(() => {
vi.clearAllMocks();
mockedEnsureRuntimePluginsLoaded.mockReset();
mockedRunEmbeddedAttempt.mockReset();
});
it("bootstraps runtime plugins with the resolved workspace before running", async () => {
@@ -39,7 +37,7 @@ describe("runEmbeddedPiAgent usage reporting", () => {
runId: "run-plugin-bootstrap",
});
expect(runtimePluginMocks.ensureRuntimePluginsLoaded).toHaveBeenCalledWith({
expect(mockedEnsureRuntimePluginsLoaded).toHaveBeenCalledWith({
config: undefined,
workspaceDir: "/tmp/workspace",
});
@@ -66,7 +64,7 @@ describe("runEmbeddedPiAgent usage reporting", () => {
allowGatewaySubagentBinding: true,
});
expect(runtimePluginMocks.ensureRuntimePluginsLoaded).toHaveBeenCalledWith({
expect(mockedEnsureRuntimePluginsLoaded).toHaveBeenCalledWith({
config: undefined,
workspaceDir: "/tmp/workspace",
allowGatewaySubagentBinding: true,