refactor: eliminate dead-export baseline (#108376)

Burn the grandfathered unused-export baseline to zero and enforce a hard-zero Knip gate.
This commit is contained in:
Peter Steinberger
2026-07-15 17:05:07 +01:00
committed by GitHub
parent d11f304519
commit 9497450511
276 changed files with 2970 additions and 1151 deletions
+4
View File
@@ -21,6 +21,10 @@ const rootEntries = [
"src/agents/model-provider-auth.worker.ts!",
// Loaded lazily by the registry; its callbacks form the orphan-recovery runtime contract.
"src/agents/subagent-orphan-recovery.ts!",
// Task cancellation loads this control facade by string path to avoid a registry cycle.
"src/tasks/task-registry-control.runtime.ts!",
// Human plugin listing lazily loads its formatter to keep JSON startup lean.
"src/cli/plugins-list-format.ts!",
"src/infra/kysely-node-sqlite.ts!",
"src/infra/warning-filter.ts!",
"src/infra/command-explainer/index.ts!",
+1 -1
View File
@@ -125,7 +125,7 @@ job budget.
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
The `check-dependencies` shard runs production Knip dependency, unused-file, and unused-export checks. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. The unused-export guard excludes test-support files, then fails on new findings or stale required baseline entries; after deleting dead exports, regenerate the shrink-only baseline with `pnpm deadcode:exports:update`. Historical targets run the export guard when they provide it and retain their older dead-code fallback otherwise.
The `check-dependencies` shard runs production Knip dependency, unused-file, and unused-export checks. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. The unused-export guard excludes test-support files and fails on every unused production export; intentional dynamic consumers must be modeled in `config/knip.config.ts`. Historical targets run the export guard when they provide it and retain their older dead-code fallback otherwise.
## ClawSweeper activity forwarding
-1
View File
@@ -1591,7 +1591,6 @@
"crabbox:warmup": "node scripts/crabbox-wrapper.mjs warmup",
"deadcode:dependencies": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --dependencies --no-config-hints",
"deadcode:exports": "node scripts/check-deadcode-exports.mjs",
"deadcode:exports:update": "node scripts/check-deadcode-exports.mjs --update",
"deadcode:knip": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --files --dependencies",
"deadcode:report": "pnpm deadcode:knip; pnpm deadcode:exports",
"deadcode:unused-files": "node scripts/check-deadcode-unused-files.mjs",
+3 -32
View File
@@ -5,38 +5,9 @@ export function parseKnipCompactUnusedExportsResult(output: string): {
entries: string[];
sawExportSection: boolean;
};
/** Compares detected unused exports against the checked-in baseline. */
export function compareUnusedExportsToBaseline(
actualEntries: string[],
baselineEntries: string[],
optionalBaselineEntries?: string[],
): {
actual: string[];
allowed: string[];
unexpected: string[];
stale: string[];
duplicateAllowedCount: number;
allowlistIsSorted: boolean;
};
/** Emits the checked-in baseline module used by --update. */
export function formatUnusedExportBaseline(
requiredEntries: string[],
optionalEntries?: string[],
): string;
/** Checks Knip output against the current baseline. */
export function checkUnusedExports(
output: string,
baselineEntries?: string[],
optionalBaselineEntries?: string[],
): {
/** Rejects every unused export reported by Knip. */
export function checkUnusedExports(output: string): {
ok: boolean;
comparison: {
actual: string[];
allowed: string[];
unexpected: string[];
stale: string[];
duplicateAllowedCount: number;
allowlistIsSorted: boolean;
};
entries: string[];
message: string;
};
+16 -108
View File
@@ -1,17 +1,7 @@
#!/usr/bin/env node
// Enforces a ratcheting baseline for Knip's unused exports.
import { writeFile } from "node:fs/promises";
// Enforces a hard-zero policy for Knip's unused exports.
import { fileURLToPath } from "node:url";
import {
KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE,
KNIP_UNUSED_EXPORT_BASELINE,
} from "./deadcode-exports.baseline.mjs";
import {
compareStringListToAllowlist,
isLikelyRepoFilePath,
runKnip,
uniqueSorted,
} from "./deadcode-knip-runner.mjs";
import { isLikelyRepoFilePath, runKnip, uniqueSorted } from "./deadcode-knip-runner.mjs";
const KNIP_ARGS = [
"--config",
@@ -25,10 +15,6 @@ const KNIP_ARGS = [
"--no-config-hints",
];
const BASELINE_HEADER = `// Pre-existing unused exports awaiting deletion.
// New entries fail CI. After deleting dead code, run \`pnpm deadcode:exports:update\`.
// Do not add entries to avoid fixing new findings.`;
/** Parses compact Knip export sections into one path-and-symbol entry per finding. */
export function parseKnipCompactUnusedExportsResult(output) {
const entries = [];
@@ -77,90 +63,24 @@ export function parseKnipCompactUnusedExports(output) {
return parseKnipCompactUnusedExportsResult(output).entries;
}
/** Compares detected unused exports against the checked-in baseline. */
export function compareUnusedExportsToBaseline(
actualEntries,
baselineEntries,
optionalBaselineEntries = [],
) {
return compareStringListToAllowlist(actualEntries, baselineEntries, optionalBaselineEntries);
}
function formatUnusedExportComparison(comparison) {
const lines = [];
if (!comparison.allowlistIsSorted) {
lines.push("deadcode unused-export baseline is not sorted.");
}
if (comparison.duplicateAllowedCount > 0) {
lines.push(
`deadcode unused-export baseline contains ${comparison.duplicateAllowedCount} duplicate entr${
comparison.duplicateAllowedCount === 1 ? "y" : "ies"
}.`,
);
}
if (comparison.unexpected.length > 0) {
lines.push("Unexpected unused exports:");
lines.push(...comparison.unexpected.map((entry) => ` ${entry}`));
}
if (comparison.stale.length > 0) {
lines.push("Stale required baseline entries:");
lines.push(...comparison.stale.map((entry) => ` ${entry}`));
}
if (lines.length > 0) {
lines.push("Run `pnpm deadcode:exports:update` after removing dead code.");
}
return lines.join("\n");
}
function formatArrayExport(name, entries) {
if (entries.length === 0) {
return `export const ${name} = [];`;
}
return `export const ${name} = [\n${entries.map((entry) => ` ${JSON.stringify(entry)},`).join("\n")}\n];`;
}
/** Emits the checked-in baseline module used by --update. */
export function formatUnusedExportBaseline(
requiredEntries,
optionalEntries = KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE,
) {
// Optional entries are platform-variant: promoting one into the required
// list would make CI stale-fail on platforms where it is legitimately absent.
const optionalSet = new Set(uniqueSorted(optionalEntries));
return `${BASELINE_HEADER}\n${formatArrayExport(
"KNIP_UNUSED_EXPORT_BASELINE",
uniqueSorted(requiredEntries).filter((entry) => !optionalSet.has(entry)),
)}\n\n// Platform-variant findings. Allowed when present; never required.\n${formatArrayExport(
"KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE",
uniqueSorted(optionalEntries),
)}\n`;
}
/** Checks Knip output against the current baseline. */
export function checkUnusedExports(
output,
baselineEntries = KNIP_UNUSED_EXPORT_BASELINE,
optionalBaselineEntries = KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE,
) {
const actual = parseKnipCompactUnusedExports(output);
const comparison = compareUnusedExportsToBaseline(
actual,
baselineEntries,
optionalBaselineEntries,
);
/** Rejects every unused export reported by Knip. */
export function checkUnusedExports(output) {
const entries = parseKnipCompactUnusedExports(output);
return {
ok:
comparison.allowlistIsSorted &&
comparison.duplicateAllowedCount === 0 &&
comparison.unexpected.length === 0 &&
comparison.stale.length === 0,
comparison,
message: formatUnusedExportComparison(comparison),
ok: entries.length === 0,
entries,
message:
entries.length === 0
? ""
: [
"Unused exports are not allowed:",
...entries.map((entry) => ` ${entry}`),
"Delete the exports or model their real production consumers in Knip.",
].join("\n"),
};
}
async function main() {
const update = process.argv.slice(2).includes("--update");
const result = await runKnip(KNIP_ARGS, { scanName: "unused-export scan" });
if (result.errorCode || result.status === null) {
console.error(
@@ -188,25 +108,13 @@ async function main() {
return;
}
const actual = parsed.entries;
if (update) {
const baselinePath = fileURLToPath(new URL("./deadcode-exports.baseline.mjs", import.meta.url));
await writeFile(
baselinePath,
formatUnusedExportBaseline(actual, KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE),
"utf8",
);
console.log(`[deadcode] Updated unused-export baseline with ${actual.length} entries.`);
return;
}
const check = checkUnusedExports(result.output);
if (!check.ok) {
console.error(check.message);
process.exitCode = 1;
return;
}
console.log(`[deadcode] Knip unused-export baseline matched ${actual.length} entries.`);
console.log("[deadcode] Knip unused-export check passed with 0 entries.");
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
-103
View File
@@ -1,103 +0,0 @@
// Pre-existing unused exports awaiting deletion.
// New entries fail CI. After deleting dead code, run `pnpm deadcode:exports:update`.
// Do not add entries to avoid fixing new findings.
export const KNIP_UNUSED_EXPORT_BASELINE = [
"src/agents/agent-hooks/compaction-safeguard.ts: testing",
"src/agents/agent-tools.before-tool-call.ts: testing",
"src/agents/apply-patch.ts: applyPatch",
"src/agents/code-mode.ts: testing",
"src/agents/command/attempt-execution.helpers.ts: claudeCliSessionTranscriptPath",
"src/agents/command/attempt-execution.helpers.ts: formatClaudeCliFallbackPrelude",
"src/agents/compaction.ts: buildCompactionSummarizationInstructions",
"src/agents/compaction.ts: summarizeWithFallback",
"src/agents/embedded-agent-runner/extra-params.ts: testing",
"src/agents/embedded-agent-runner/run.ts: testing",
"src/agents/embedded-agent-runner/runs.ts: testing",
"src/agents/embedded-agent-runner/session-manager-cache.ts: createSessionManagerCache",
"src/agents/embedded-agent-subscribe.handlers.messages.ts: buildAssistantStreamData",
"src/agents/embedded-agent-subscribe.handlers.messages.ts: recordPendingAssistantReplyDirectives",
"src/agents/embedded-agent-subscribe.handlers.messages.ts: resolveSilentReplyFallbackText",
"src/agents/embedded-agent-subscribe.tools.ts: isToolResultMediaTrusted",
"src/agents/harness/lifecycle-hook-helpers.ts: clearAgentHarnessFinalizeRetryBudget",
"src/agents/model-fallback.ts: testing",
"src/agents/modes/interactive/theme/theme.ts: setTheme",
"src/agents/openai-completions-compat.ts: resolveOpenAICompletionsCompatDefaults",
"src/agents/session-suspension.ts: testing",
"src/agents/sessions/tools/bash.ts: resolveBashTimeoutMs",
"src/agents/tool-search.ts: testing",
"src/agents/tool-search.ts: ToolSearchCatalogSession",
"src/agents/tools/image-generate-tool.ts: resolveImageGenerationModelConfigForTool",
"src/agents/tools/image-tool.ts: resolveImageModelConfigForTool",
"src/agents/tools/image-tool.ts: testing",
"src/agents/tools/model-config.helpers.ts: hasDirectProviderApiKeyAuthForTool",
"src/agents/tools/sessions-resolution.ts: testing",
"src/agents/tools/sessions-send-tool.a2a.ts: testing",
"src/agents/tools/video-generate-tool.ts: resolveVideoGenerationModelConfigForTool",
"src/agents/tools/web-fetch.ts: sanitizeWebFetchUrl",
"src/agents/utils/tools-manager.ts: testing",
"src/auto-reply/reply/abort.ts: testing",
"src/auto-reply/reply/acp-reset-target.ts: testing",
"src/auto-reply/reply/agent-runner-context-recovery.ts: computeContextAwareReserveTokensFloor",
"src/auto-reply/reply/agent-runner-memory.ts: setAgentRunnerMemoryTestDeps",
"src/auto-reply/reply/agent-runner-session-reset.ts: setAgentRunnerSessionResetTestDeps",
"src/auto-reply/reply/commands-login.ts: testing",
"src/auto-reply/reply/dispatch-from-config.ts: testing",
"src/auto-reply/reply/queue/cleanup.ts: testing",
"src/auto-reply/reply/queue/enqueue.ts: resetRecentQueuedMessageIdDedupe",
"src/auto-reply/reply/reply-run-registry.ts: testing",
"src/auto-reply/reply/stage-sandbox-media.ts: testing",
"src/auto-reply/usage-bar/template.ts: clearUsageBarTemplateCacheForTest",
"src/cli/channel-options.ts: testing",
"src/cli/command-secret-gateway.ts: testing",
"src/cli/gateway-cli/run.ts: testing",
"src/cli/plugins-install-command.ts: loadConfigForInstall",
"src/cli/plugins-list-format.ts: formatPluginLine",
"src/cli/startup-metadata.ts: testing",
"src/cli/update-cli/update-command-plugins.ts: buildInvalidConfigPostCoreUpdateResult",
"src/cli/update-cli/update-command-plugins.ts: collectMissingPluginInstallPayloads",
"src/cli/update-cli/update-command-plugins.ts: resolvePostSyncPluginUpdateSkipIds",
"src/cli/update-cli/update-command-service.ts: formatPostUpdateGatewayRecoveryInstructions",
"src/cli/update-cli/update-command-service.ts: recoverInstalledLaunchAgentAfterUpdate",
"src/cli/update-cli/update-command-service.ts: recoverLaunchAgentAndRecheckGatewayHealth",
"src/cli/update-cli/update-command-service.ts: shouldUseLegacyProcessRestartAfterUpdate",
"src/commands/audit.ts: testApi",
"src/commands/backup-shared.ts: resolveBackupPlanFromPaths",
"src/commands/doctor-auth-oauth-sidecar.ts: testing",
"src/commands/doctor-auth.ts: legacyCodexProviderOverrideToHealthFinding",
"src/commands/doctor-heartbeat-main-session-repair.ts: moveHeartbeatMainSessionEntry",
"src/commands/doctor-heartbeat-main-session-repair.ts: resolveHeartbeatMainSessionRepairCandidate",
"src/commands/doctor-sandbox.ts: resolveSandboxScript",
"src/commands/doctor-session-snapshots.ts: resolveSessionSnapshotBundledSkillsDir",
"src/commands/doctor-session-snapshots.ts: scanSessionStoreForStaleRuntimeSnapshotPaths",
"src/commands/doctor-whatsapp-responsiveness.ts: listLocalTuiProcesses",
"src/commands/doctor-whatsapp-responsiveness.ts: terminateLocalTuiProcesses",
"src/commands/doctor/shared/codex-native-assets.ts: scanCodexNativeAssets",
"src/commands/doctor/shared/codex-route-warnings.ts: repairCodexSessionStoreRoutes",
"src/commands/doctor/shared/legacy-oauth-sidecar.ts: legacyOAuthSidecarInternalTestUtils",
"src/commands/doctor/shared/plugin-dependency-cleanup.ts: testing",
"src/commands/doctor/shared/stale-auth-order.ts: repairStaleConfiguredAuthOrders",
"src/commands/doctor/shared/stale-oauth-profile-shadows.ts: testing",
"src/commands/onboard-inference.ts: detectNativeCodexAppServer",
"src/commands/onboard-non-interactive/local.ts: resolveGatewayHealthProbeToken",
"src/commands/onboard-non-interactive/local.ts: resolveInstallDaemonGatewayHealthTiming",
"src/commands/onboard-skills.ts: testing",
"src/commands/onboarding-plugin-install.ts: testing",
"src/commands/sessions-tail.ts: setSessionsTailFollowIntervalMsForTests",
"src/commands/sessions.ts: testing",
"src/commands/status.command.ts: resolvePairingRecoveryContext",
"src/cron/isolated-agent/delivery-dispatch.ts: getCompletedDirectCronDeliveriesCountForTests",
"src/cron/isolated-agent/delivery-dispatch.ts: resetCompletedDirectCronDeliveriesForTests",
"src/cron/schedule.ts: clearCronScheduleCacheForTest",
"src/cron/schedule.ts: getCronScheduleCacheMaxForTest",
"src/cron/schedule.ts: getCronScheduleCacheSizeForTest",
"src/cron/schedule.ts: hasCronInCacheForTest",
"src/cron/service/active-run-cancellation.ts: cancelActiveCronTaskRun",
"src/cron/service/active-run-cancellation.ts: resetActiveCronTaskRunsForTests",
"src/cron/service/timer.ts: executeJobCore",
"src/cron/service/timer.ts: onTimer",
"src/cron/session-reaper.ts: resetReaperThrottle",
"src/logging/redact-internal.ts: withFullContextToolPayloadRedaction",
];
// Platform-variant findings. Allowed when present; never required.
export const KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE = [];
@@ -0,0 +1,46 @@
import type { summarizeInStages } from "../compaction.js";
import "./compaction-safeguard.js";
type CompactionSafeguardTestApi = {
setSummarizeInStagesForTest(next?: typeof summarizeInStages): void;
collectToolFailures: CallableFunction;
formatToolFailuresSection: CallableFunction;
splitPreservedRecentTurns: CallableFunction;
formatPreservedTurnsSection: CallableFunction;
formatSplitTurnContextSection: CallableFunction;
buildCompactionStructureInstructions: CallableFunction;
buildStructuredFallbackSummary: CallableFunction;
prependPreviousSummaryForRedistill: CallableFunction;
appendSummarySection: CallableFunction;
resolveRecentTurnsPreserve: CallableFunction;
resolveQualityGuardMaxRetries: CallableFunction;
extractOpaqueIdentifiers: CallableFunction;
auditSummaryQuality: CallableFunction;
capCompactionSummary: CallableFunction;
capCompactionSummaryPreservingSuffix: CallableFunction;
formatFileOperations: CallableFunction;
computeAdaptiveChunkRatio: CallableFunction;
isOversizedForSummary: CallableFunction;
readWorkspaceContextForSummary: CallableFunction;
hasMeaningfulConversationContent: CallableFunction;
isRealConversationMessage: CallableFunction;
BASE_CHUNK_RATIO: number;
MIN_CHUNK_RATIO: number;
SAFETY_MARGIN: number;
MAX_COMPACTION_SUMMARY_CHARS: number;
MAX_FILE_OPS_SECTION_CHARS: number;
MAX_FILE_OPS_LIST_CHARS: number;
SUMMARY_TRUNCATED_MARKER: string;
};
function getTestApi(): CompactionSafeguardTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.compactionSafeguardTestApi")
];
if (!api) {
throw new Error("compaction safeguard test API is unavailable");
}
return api as CompactionSafeguardTestApi;
}
export const testing = getTestApi();
@@ -21,7 +21,8 @@ import {
setCompactionSafeguardCancelReason,
setCompactionSafeguardRuntime,
} from "./compaction-safeguard-runtime.js";
import compactionSafeguardExtension, { testing } from "./compaction-safeguard.js";
import compactionSafeguardExtension from "./compaction-safeguard.js";
import { testing } from "./compaction-safeguard.test-support.js";
vi.mock("../compaction.js", async () => {
const actual = await vi.importActual<typeof compactionModule>("../compaction.js");
@@ -310,7 +311,7 @@ describe("compaction-safeguard tool failures", () => {
];
const failures = collectToolFailures(messages);
expect(failures.map((failure) => failure.toolCallId)).toEqual([
expect(failures.map((failure: { toolCallId: string }) => failure.toolCallId)).toEqual([
"call-spawn-error",
"call-spawn-forbidden",
]);
@@ -358,7 +359,7 @@ describe("compaction-safeguard tool failures", () => {
];
const failures = collectToolFailures(messages);
expect(failures.map((failure) => failure.toolCallId)).toEqual([
expect(failures.map((failure: { toolCallId: string }) => failure.toolCallId)).toEqual([
"call-exec-failed",
"call-other-lookalike",
]);
@@ -848,7 +849,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
recentTurnsPreserve: 1,
});
expect(split.preservedMessages.map((msg) => msg.role)).toEqual([
expect(split.preservedMessages.map((msg: AgentMessage) => msg.role)).toEqual([
"user",
"assistant",
"toolResult",
@@ -856,13 +857,14 @@ describe("compaction-safeguard recent-turn preservation", () => {
]);
expect(
split.preservedMessages.some(
(msg) => msg.role === "user" && (msg as { content?: unknown }).content === "recent ask",
(msg: AgentMessage) =>
msg.role === "user" && (msg as { content?: unknown }).content === "recent ask",
),
).toBe(true);
const summarizableToolResultIds = split.summarizableMessages
.filter((msg) => msg.role === "toolResult")
.map((msg) => (msg as { toolCallId?: unknown }).toolCallId);
.filter((msg: AgentMessage) => msg.role === "toolResult")
.map((msg: AgentMessage) => (msg as { toolCallId?: unknown }).toolCallId);
expect(summarizableToolResultIds).toContain("call_old");
expect(summarizableToolResultIds).not.toContain("call_recent");
});
@@ -1016,7 +1018,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
expect(split.preservedMessages).toHaveLength(6);
expect(
split.preservedMessages.some(
(msg) =>
(msg: AgentMessage) =>
msg.role === "user" && (msg as { content?: unknown }).content === "single user prompt",
),
).toBe(true);
@@ -1080,7 +1082,10 @@ describe("compaction-safeguard recent-turn preservation", () => {
"Track id a1b2c3d4e5f6 plus A1B2C3D4E5F6 and again a1b2c3d4e5f6",
);
expect(
identifiers.reduce((count, id) => count + (id === "A1B2C3D4E5F6" ? 1 : 0), 0), // pragma: allowlist secret
identifiers.reduce(
(count: number, id: string) => count + (id === "A1B2C3D4E5F6" ? 1 : 0), // pragma: allowlist secret
0,
),
).toBe(1);
});
@@ -1329,7 +1329,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
});
}
export const testing = {
const testing = {
setSummarizeInStagesForTest(next?: typeof summarizeInStages) {
compactionSafeguardDeps.summarizeInStages = next ?? summarizeInStages;
},
@@ -1362,4 +1362,9 @@ export const testing = {
MAX_FILE_OPS_LIST_CHARS,
SUMMARY_TRUNCATED_MARKER,
} as const;
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.compactionSafeguardTestApi")] =
testing;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -24,12 +24,16 @@ import type { PluginHookRegistration } from "../plugins/types.js";
import { toClientToolDefinitions, toToolDefinitions } from "./agent-tool-definition-adapter.js";
import { wrapToolWithAbortSignal } from "./agent-tools.abort.js";
import {
testing as beforeToolCallTesting,
consumeAdjustedParamsForToolCall,
finalizeToolTerminalPresentation,
isToolWrappedWithBeforeToolCallHook,
wrapToolWithBeforeToolCallHook,
} from "./agent-tools.before-tool-call.js";
import {
adjustedParamsByToolCallId,
buildAdjustedParamsKey,
structuredReplaySafeToolCallIds,
} from "./agent-tools.before-tool-call.state.js";
import { normalizeToolParameters } from "./agent-tools.schema.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import { markCodeModeControlTool } from "./code-mode-control-tools.js";
@@ -40,6 +44,12 @@ import { setToolTerminalPresentation } from "./tool-terminal-presentation.js";
type BeforeToolCallHandlerMock = ReturnType<typeof vi.fn>;
const beforeToolCallTesting = {
adjustedParamsByToolCallId,
buildAdjustedParamsKey,
structuredReplaySafeToolCallIds,
};
function asAgentTool(tool: {
description?: string;
execute: ReturnType<typeof vi.fn>;
@@ -2064,21 +2064,6 @@ function recordPreExecutionBlockedToolCall(toolCallId?: string, runId?: string):
}
}
/** Test-only access to before_tool_call internals. */
export const testing = {
BEFORE_TOOL_CALL_DIAGNOSTIC_OPTIONS,
BEFORE_TOOL_CALL_HOOK_CONTEXT,
BEFORE_TOOL_CALL_SOURCE_TOOL,
BEFORE_TOOL_CALL_WRAPPED,
buildAdjustedParamsKey,
adjustedParamsByToolCallId,
preExecutionBlockedToolCallIds,
structuredReplaySafeToolCallIds,
runBeforeToolCallHook,
mergeParamsWithApprovalOverrides,
isPlainObject,
};
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
+9 -1
View File
@@ -11,11 +11,19 @@ import { Type, type TSchema } from "typebox";
import { describe, expect, it, vi } from "vitest";
import {
isToolWrappedWithBeforeToolCallHook,
testing as beforeToolCallTesting,
wrapToolWithBeforeToolCallHook,
} from "./agent-tools.before-tool-call.js";
import { normalizeToolParameters } from "./agent-tools.schema.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import {
BEFORE_TOOL_CALL_HOOK_CONTEXT,
BEFORE_TOOL_CALL_SOURCE_TOOL,
} from "./before-tool-call-metadata.js";
const beforeToolCallTesting = {
BEFORE_TOOL_CALL_HOOK_CONTEXT,
BEFORE_TOOL_CALL_SOURCE_TOOL,
};
const TEST_USAGE = {
input: 0,
+37
View File
@@ -0,0 +1,37 @@
import type { ApplyPatchSummary } from "./apply-patch.js";
import "./apply-patch.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
type ApplyPatchOptions = {
cwd: string;
sandbox?: { root: string; bridge: SandboxFsBridge };
workspaceOnly?: boolean;
signal?: AbortSignal;
};
type ApplyPatchResult = {
summary: ApplyPatchSummary;
text: string;
noOp?: boolean;
};
type ApplyPatchTestApi = {
applyPatch(input: string, options: ApplyPatchOptions): Promise<ApplyPatchResult>;
};
function getTestApi(): ApplyPatchTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.applyPatchTestApi")
];
if (!api) {
throw new Error("apply patch test API is unavailable");
}
return api as ApplyPatchTestApi;
}
export async function applyPatch(
input: string,
options: ApplyPatchOptions,
): Promise<ApplyPatchResult> {
return await getTestApi().applyPatch(input, options);
}
+2 -1
View File
@@ -11,7 +11,8 @@ import {
createRebindableDirectoryAlias,
withRealpathSymlinkRebindRace,
} from "../test-utils/symlink-rebind-race.js";
import { applyPatch, createApplyPatchTool } from "./apply-patch.js";
import { createApplyPatchTool } from "./apply-patch.js";
import { applyPatch } from "./apply-patch.test-support.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
async function withTempDir<T>(fn: (dir: string) => Promise<T>) {
+7 -4
View File
@@ -137,10 +137,7 @@ export function createApplyPatchTool(
}
/** Parse and apply a patch envelope to the configured filesystem target. */
export async function applyPatch(
input: string,
options: ApplyPatchOptions,
): Promise<ApplyPatchResult> {
async function applyPatch(input: string, options: ApplyPatchOptions): Promise<ApplyPatchResult> {
const parsed = parsePatchText(input);
if (parsed.hunks.length === 0) {
throw new Error("No files were modified.");
@@ -714,3 +711,9 @@ function parseUpdateFileChunk(
return { chunk, consumed: parsedLines + startIndex };
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.applyPatchTestApi")] = {
applyPatch,
};
}
+2 -5
View File
@@ -7,11 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createSolidPngBuffer } from "../../test/helpers/image-fixtures.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { getReplyPayloadMetadata } from "../auto-reply/reply-payload.js";
import {
testing as replyRunTesting,
createReplyOperation,
replyRunRegistry,
} from "../auto-reply/reply/reply-run-registry.js";
import { createReplyOperation, replyRunRegistry } from "../auto-reply/reply/reply-run-registry.js";
import { testing as replyRunTesting } from "../auto-reply/reply/reply-run-registry.test-support.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { loadTranscriptEvents, upsertSessionEntry } from "../config/sessions/session-accessor.js";
import { CURRENT_SESSION_VERSION } from "../config/sessions/version.js";
+2 -5
View File
@@ -4,11 +4,8 @@ import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
testing as replyRunTesting,
createReplyOperation,
replyRunRegistry,
} from "../auto-reply/reply/reply-run-registry.js";
import { createReplyOperation, replyRunRegistry } from "../auto-reply/reply/reply-run-registry.js";
import { testing as replyRunTesting } from "../auto-reply/reply/reply-run-registry.test-support.js";
import {
markMcpLoopbackToolCallFinished,
markMcpLoopbackToolCallStarted,
+2 -1
View File
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../shared/deferred.js";
import { runCodeModeScriptHeadless, testing, type CodeModeHeadlessResult } from "./code-mode.js";
import { runCodeModeScriptHeadless, type CodeModeHeadlessResult } from "./code-mode.js";
import { testing } from "./code-mode.test-support.js";
import {
createToolSearchCatalogRef,
registerHeadlessToolSearchCatalog,
+78
View File
@@ -0,0 +1,78 @@
import "./code-mode.js";
import type { ToolSearchToolContext } from "./tool-search.js";
type CodeModeConfig = {
enabled: boolean;
runtime: "quickjs-wasi";
mode: "only";
languages: ("javascript" | "typescript")[];
timeoutMs: number;
memoryLimitBytes: number;
maxOutputBytes: number;
maxSnapshotBytes: number;
maxPendingToolCalls: number;
snapshotTtlSeconds: number;
searchDefaultLimit: number;
maxSearchLimit: number;
};
type CodeModeFailureCode =
| "aborted"
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "output_limit_exceeded"
| "snapshot_limit_exceeded"
| "internal_error";
type CodeModeWorkerResult =
| { status: "completed"; value: unknown; output: unknown[] }
| {
status: "waiting";
snapshotBytes: Uint8Array;
pendingRequests: Array<{ id: string; method: string; args: unknown[] }>;
output: unknown[];
}
| { status: "failed"; error: string; code: CodeModeFailureCode; output: unknown[] };
type CodeModeTestApi = {
activeRuns: Map<string, { config: CodeModeConfig; expiresAt: number }>;
resumingRunIds: Set<string>;
createHeadlessAbortScope(
signal: AbortSignal | undefined,
wallClockMs: number,
): { signal: AbortSignal; cleanup: () => void };
normalizeCodeModeWorkerResult(result: CodeModeWorkerResult): CodeModeWorkerResult;
runCodeModeWorker(
workerData: unknown,
timeoutMs: number,
workerUrl?: URL,
signal?: AbortSignal,
): Promise<CodeModeWorkerResult>;
resolveCodeModeHeadlessConfig(
ctx: ToolSearchToolContext,
overrides?: Partial<
Pick<
CodeModeConfig,
| "timeoutMs"
| "memoryLimitBytes"
| "maxOutputBytes"
| "maxSnapshotBytes"
| "maxPendingToolCalls"
>
>,
): CodeModeConfig;
resolveCodeModeWorkerUrl(currentModuleUrl: string): URL;
getTypescriptRuntimePromise(): Promise<typeof import("typescript")> | null;
setTypescriptRuntimeForTest(runtime: typeof import("typescript") | null): void;
};
function getTestApi(): CodeModeTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.codeModeTestApi")];
if (!api) {
throw new Error("code mode test API is unavailable");
}
return api as CodeModeTestApi;
}
export const testing = getTestApi();
+1 -1
View File
@@ -19,8 +19,8 @@ import {
CODE_MODE_WAIT_TOOL_NAME,
createCodeModeTools,
resolveCodeModeConfig,
testing,
} from "./code-mode.js";
import { testing } from "./code-mode.test-support.js";
import { createToolSearchCatalogRef, type ToolSearchCatalogRef } from "./tool-search.js";
import {
TOOL_CALL_RAW_TOOL_NAME,
+5 -3
View File
@@ -1727,20 +1727,22 @@ export function addClientToolsToCodeModeCatalog(params: {
}
/** Test-only hooks and state accessors for Code Mode worker orchestration. */
export const testing = {
const testing = {
activeRuns,
resumingRunIds,
codeModeWorkerUrl,
createHeadlessAbortScope,
normalizeCodeModeWorkerResult,
runCodeModeWorker,
resolveCodeModeHeadlessConfig,
resolveCodeModeWorkerUrl,
resolveCodeModeConfig,
getTypescriptRuntimePromise: (): Promise<typeof import("typescript")> | null =>
typescriptRuntimeLoader.peek() ?? null,
setTypescriptRuntimeForTest: (runtime: typeof import("typescript") | null) => {
typescriptRuntimeForTest = runtime;
},
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.codeModeTestApi")] = testing;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -0,0 +1,37 @@
import type { ClaudeCliFallbackSeed } from "../../gateway/cli-session-history.js";
import "./attempt-execution.helpers.js";
type AttemptExecutionHelpersTestApi = {
claudeCliSessionTranscriptPath(params: {
sessionId: string | undefined;
workspaceDir: string | undefined;
homeDir?: string;
}): string | null;
formatClaudeCliFallbackPrelude(
seed: ClaudeCliFallbackSeed,
options?: { charBudget?: number },
): string;
};
function getTestApi(): AttemptExecutionHelpersTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.attemptExecutionHelpersTestApi")
];
if (!api) {
throw new Error("attempt execution helpers test API is unavailable");
}
return api as AttemptExecutionHelpersTestApi;
}
export function claudeCliSessionTranscriptPath(
params: Parameters<AttemptExecutionHelpersTestApi["claudeCliSessionTranscriptPath"]>[0],
): string | null {
return getTestApi().claudeCliSessionTranscriptPath(params);
}
export function formatClaudeCliFallbackPrelude(
seed: ClaudeCliFallbackSeed,
options?: { charBudget?: number },
): string {
return getTestApi().formatClaudeCliFallbackPrelude(seed, options);
}
@@ -93,7 +93,7 @@ export async function sessionFileHasContent(sessionFile: string | undefined): Pr
}
/** Resolves the expected Claude CLI transcript JSONL path for a session. */
export function claudeCliSessionTranscriptPath(params: {
function claudeCliSessionTranscriptPath(params: {
sessionId: string | undefined;
workspaceDir: string | undefined;
homeDir?: string;
@@ -383,7 +383,7 @@ function formatFallbackTurns(
* Returns an empty string when neither a summary nor any usable turn fits in
* the budget; callers can treat that as "no context to seed".
*/
export function formatClaudeCliFallbackPrelude(
function formatClaudeCliFallbackPrelude(
seed: ClaudeCliFallbackSeed,
options?: { charBudget?: number },
): string {
@@ -540,3 +540,9 @@ export function createAcpVisibleTextAccumulator() {
},
};
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.attemptExecutionHelpersTestApi")
] = { claudeCliSessionTranscriptPath, formatClaudeCliFallbackPrelude };
}
+4 -2
View File
@@ -8,13 +8,15 @@ import { cliBackendLog } from "../cli-runner/log.js";
import {
buildClaudeCliFallbackContextPrelude,
claudeCliSessionTranscriptHasContent,
claudeCliSessionTranscriptPath,
claudeCliSessionTranscriptHasOrphanedToolUse,
createAcpVisibleTextAccumulator,
formatClaudeCliFallbackPrelude,
resolveFallbackRetryPrompt,
sessionFileHasContent,
} from "./attempt-execution.helpers.js";
import {
claudeCliSessionTranscriptPath,
formatClaudeCliFallbackPrelude,
} from "./attempt-execution.helpers.test-support.js";
import { resolveClaudeCliProjectDirForWorkspace } from "./claude-cli-project-dir.js";
describe("resolveFallbackRetryPrompt", () => {
@@ -58,11 +58,11 @@ vi.mock("../infra/retry.js", async () => {
};
});
let summarizeWithFallback: typeof import("./compaction.js").summarizeWithFallback;
let summarizeWithFallback: typeof import("./compaction.test-support.js").summarizeWithFallback;
beforeAll(async () => {
vi.resetModules();
({ summarizeWithFallback } = await import("./compaction.js"));
({ summarizeWithFallback } = await import("./compaction.test-support.js"));
});
describe("summarizeChunks partial summary preservation (#82952)", () => {
@@ -1,6 +1,6 @@
// Verifies summary instruction policy for preserving opaque identifiers.
import { describe, expect, it } from "vitest";
import { buildCompactionSummarizationInstructions } from "./compaction.js";
import { buildCompactionSummarizationInstructions } from "./compaction.test-support.js";
describe("compaction identifier policy", () => {
it("defaults to strict identifier preservation", () => {
@@ -17,8 +17,8 @@ const mockGenerateSummary = vi.mocked(agentSessions.generateSummary);
type SummarizeInStagesInput = Parameters<typeof import("./compaction.js").summarizeInStages>[0];
const MESSAGE_TIME_BASE_MS = Date.UTC(2026, 0, 1);
const { buildCompactionSummarizationInstructions, summarizeInStages } =
await import("./compaction.js");
const { buildCompactionSummarizationInstructions } = await import("./compaction.test-support.js");
const { summarizeInStages } = await import("./compaction.js");
function makeMessage(index: number, size = 1200): AgentMessage {
return {
@@ -3,7 +3,7 @@ import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions";
import type { UserMessage } from "openclaw/plugin-sdk/llm";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { summarizeWithFallback } from "./compaction.js";
import { summarizeWithFallback } from "./compaction.test-support.js";
const agentSessionMocks = vi.hoisted(() => ({
generateSummary: vi.fn(),
+47
View File
@@ -0,0 +1,47 @@
import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions";
import type { CompactionSummarizationInstructions } from "./compaction.js";
import "./compaction.js";
type SummarizeWithFallbackParams = {
messages: AgentMessage[];
model: NonNullable<ExtensionContext["model"]>;
apiKey: string;
headers?: Record<string, string>;
signal: AbortSignal;
reserveTokens: number;
maxChunkTokens: number;
contextWindow: number;
customInstructions?: string;
summarizationInstructions?: CompactionSummarizationInstructions;
previousSummary?: string;
};
type CompactionTestApi = {
buildCompactionSummarizationInstructions(
customInstructions?: string,
instructions?: CompactionSummarizationInstructions,
): string | undefined;
summarizeWithFallback(params: SummarizeWithFallbackParams): Promise<string>;
};
function getTestApi(): CompactionTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.compactionTestApi")
];
if (!api) {
throw new Error("compaction test API is unavailable");
}
return api as CompactionTestApi;
}
export function buildCompactionSummarizationInstructions(
customInstructions?: string,
instructions?: CompactionSummarizationInstructions,
): string | undefined {
return getTestApi().buildCompactionSummarizationInstructions(customInstructions, instructions);
}
export async function summarizeWithFallback(params: SummarizeWithFallbackParams): Promise<string> {
return await getTestApi().summarizeWithFallback(params);
}
@@ -19,7 +19,7 @@ vi.mock("./sessions/index.js", async () => {
});
let isOversizedForSummary: typeof import("./compaction.js").isOversizedForSummary;
let summarizeWithFallback: typeof import("./compaction.js").summarizeWithFallback;
let summarizeWithFallback: typeof import("./compaction.test-support.js").summarizeWithFallback;
function makeAssistantToolCall(timestamp: number): AssistantMessage {
return makeAgentAssistantMessage({
@@ -46,7 +46,8 @@ function makeToolResultWithDetails(timestamp: number): ToolResultMessage<{ raw:
describe("compaction toolResult details stripping", () => {
beforeAll(async () => {
({ isOversizedForSummary, summarizeWithFallback } = await import("./compaction.js"));
({ isOversizedForSummary } = await import("./compaction.js"));
({ summarizeWithFallback } = await import("./compaction.test-support.js"));
});
beforeEach(() => {
+9 -2
View File
@@ -104,7 +104,7 @@ function resolveIdentifierPreservationInstructions(
}
/** Combines identifier-preservation and caller-provided compaction instructions. */
export function buildCompactionSummarizationInstructions(
function buildCompactionSummarizationInstructions(
customInstructions?: string,
instructions?: CompactionSummarizationInstructions,
): string | undefined {
@@ -258,7 +258,7 @@ function generateSummary(
* Summarize with progressive fallback for handling oversized messages.
* If full summarization fails, tries partial summarization excluding oversized messages.
*/
export async function summarizeWithFallback(params: {
async function summarizeWithFallback(params: {
messages: AgentMessage[];
model: NonNullable<ExtensionContext["model"]>;
apiKey: string;
@@ -453,3 +453,10 @@ export function resolveContextWindowTokens(model?: ExtensionContext["model"]): n
(model as { contextTokens?: number } | undefined)?.contextTokens ?? model?.contextWindow;
return Math.max(1, Math.floor(effective ?? DEFAULT_CONTEXT_TOKENS));
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.compactionTestApi")] = {
buildCompactionSummarizationInstructions,
summarizeWithFallback,
};
}
-24
View File
@@ -63,25 +63,6 @@ function getPackageDir(): string {
return currentDir;
}
function getPackageSourceOrDistDir(): string {
const packageDir = getPackageDir();
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
return join(packageDir, srcOrDist);
}
/**
* Get path to built-in themes directory (shipped with package)
* - For Bun binary: theme/ next to executable
* - For Node.js (dist/): dist/agents/modes/interactive/theme/
* - For tsx (src/): src/agents/modes/interactive/theme/
*/
export function getThemesDir(): string {
if (isBunBinary) {
return join(getPackageDir(), "theme");
}
return join(getPackageSourceOrDistDir(), "agents", "modes", "interactive", "theme");
}
/** Get path to package.json */
function getPackageJsonPath(): string {
return join(getPackageDir(), "package.json");
@@ -147,11 +128,6 @@ export function getAgentDir(): string {
return join(homedir(), CONFIG_DIR_NAME, "agent");
}
/** Get path to user's custom themes directory */
export function getCustomThemesDir(): string {
return join(getAgentDir(), "themes");
}
/** Get path to managed binaries directory (fd, rg) */
export function getBinDir(): string {
return join(getAgentDir(), "bin");
@@ -8,7 +8,7 @@ import {
resolveMoonshotThinkingType,
} from "../llm/providers/stream-wrappers/moonshot-thinking.js";
import { runExtraParamsPayloadCase } from "./embedded-agent-runner-extraparams.test-support.js";
import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.js";
import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.test-support.js";
beforeEach(() => {
// Moonshot thinking support lives in its provider wrapper, wired through the
@@ -7,10 +7,8 @@ import {
isProxyReasoningUnsupported,
} from "../llm/providers/stream-wrappers/proxy.js";
import { runExtraParamsPayloadCase } from "./embedded-agent-runner-extraparams.test-support.js";
import {
applyExtraParamsToAgent,
testing as extraParamsTesting,
} from "./embedded-agent-runner/extra-params.js";
import { applyExtraParamsToAgent } from "./embedded-agent-runner/extra-params.js";
import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.test-support.js";
beforeEach(() => {
// OpenRouter behavior is supplied through the provider-runtime seam so tests
@@ -3,7 +3,7 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model, SimpleStreamOptions } from "openclaw/plugin-sdk/llm";
import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.js";
import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.test-support.js";
vi.mock("../plugins/provider-hook-runtime.js", () => ({
clearProviderRuntimePluginCacheForTest: vi.fn(),
@@ -2,7 +2,8 @@
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js";
import { testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js";
import { applyExtraParamsToAgent } from "./extra-params.js";
import { testing as extraParamsTesting } from "./extra-params.test-support.js";
import { log } from "./logger.js";
import { resolveCacheRetention } from "./prompt-cache-retention.js";
@@ -2,8 +2,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js";
import type { Model } from "../../llm/types.js";
import { testing as extraParamsTesting } from "./extra-params.js";
import { runExtraParamsCase } from "./extra-params.test-support.js";
import { runExtraParamsCase, testing as extraParamsTesting } from "./extra-params.test-support.js";
vi.mock("../../llm/stream.js", () => createLlmStreamSimpleMock());
@@ -3,12 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js";
import type { Model } from "../../llm/types.js";
import {
testing as extraParamsTesting,
resolvePreparedExtraParams,
resolveAgentTransportOverride,
resolveExplicitSettingsTransport,
} from "./extra-params.js";
import { runExtraParamsCase } from "./extra-params.test-support.js";
import { runExtraParamsCase, testing as extraParamsTesting } from "./extra-params.test-support.js";
vi.mock("../../llm/stream.js", () => createLlmStreamSimpleMock());
@@ -3,11 +3,11 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js";
import {
testing as extraParamsTesting,
applyExtraParamsToAgent,
resolveExtraParams,
resolvePreparedExtraParams,
} from "./extra-params.js";
import { testing as extraParamsTesting } from "./extra-params.test-support.js";
vi.mock("./logger.js", () => ({
// Sampling tests assert call options only; silence warning/debug output from
@@ -1,10 +1,40 @@
// Shared harness for extra-params wrapper tests.
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { Context, Model, SimpleStreamOptions } from "../../llm/types.js";
import type {
prepareProviderExtraParams,
resolveProviderExtraParamsForTransport,
wrapProviderStreamFn,
} from "../../plugins/provider-hook-runtime.js";
import type { StreamFn } from "../runtime/index.js";
import { testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js";
import { applyExtraParamsToAgent } from "./extra-params.js";
import type { ProviderThinkLevel } from "./utils.js";
type ExtraParamsTestApi = {
setProviderRuntimeDepsForTest(
deps:
| Partial<{
prepareProviderExtraParams: typeof prepareProviderExtraParams;
resolveProviderExtraParamsForTransport: typeof resolveProviderExtraParamsForTransport;
wrapProviderStreamFn: typeof wrapProviderStreamFn;
}>
| undefined,
): void;
resetProviderRuntimeDepsForTest(): void;
};
function getTestApi(): ExtraParamsTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.extraParamsTestApi")
];
if (!api) {
throw new Error("extra params test API is unavailable");
}
return api as ExtraParamsTestApi;
}
export const testing = getTestApi();
type ExtraParamsCapture<TPayload extends Record<string, unknown>> = {
headers?: Record<string, string>;
options?: SimpleStreamOptions;
@@ -60,7 +90,7 @@ export function runExtraParamsCase<
const agent = { streamFn: baseStreamFn };
if (params.mockProviderRuntime === true) {
extraParamsTesting.setProviderRuntimeDepsForTest({
testing.setProviderRuntimeDepsForTest({
prepareProviderExtraParams: () => undefined,
resolveProviderExtraParamsForTransport: () => undefined,
wrapProviderStreamFn: () => undefined,
@@ -79,7 +109,7 @@ export function runExtraParamsCase<
);
} finally {
if (params.mockProviderRuntime === true) {
extraParamsTesting.resetProviderRuntimeDepsForTest();
testing.resetProviderRuntimeDepsForTest();
}
}
@@ -58,7 +58,7 @@ const providerRuntimeDeps = {
let preparedExtraParamsCache = new WeakMap<OpenClawConfig, Map<string, Record<string, unknown>>>();
const REQUEST_SCOPED_EXTRA_PARAM_KEYS = new Set(["response_format", "responseFormat", "stop"]);
export const testing = {
const testing = {
setProviderRuntimeDepsForTest(
deps: Partial<typeof defaultProviderRuntimeDeps> | undefined,
): void {
@@ -80,6 +80,10 @@ export const testing = {
},
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.extraParamsTestApi")] = testing;
}
/**
* Resolve provider-specific extra params from model config.
* Used to pass through stream params like temperature/maxTokens.
@@ -7,7 +7,7 @@ import type { Model, SimpleStreamOptions } from "../../llm/types.js";
vi.mock("../../llm/stream.js", () => createLlmStreamSimpleMock());
let runExtraParamsCase: typeof import("./extra-params.test-support.js").runExtraParamsCase;
let extraParamsTesting: typeof import("./extra-params.js").testing;
let extraParamsTesting: typeof import("./extra-params.test-support.js").testing;
type ToolStreamCase = {
applyProvider: string;
@@ -32,8 +32,8 @@ function runToolStreamCase(params: ToolStreamCase) {
describe("extra-params: provider tool_stream support", () => {
beforeEach(async () => {
({ testing: extraParamsTesting } = await import("./extra-params.js"));
({ runExtraParamsCase } = await import("./extra-params.test-support.js"));
({ runExtraParamsCase, testing: extraParamsTesting } =
await import("./extra-params.test-support.js"));
extraParamsTesting.setProviderRuntimeDepsForTest({
prepareProviderExtraParams: (params) => {
// Z.AI and xAI require streaming tool-call deltas unless config
@@ -1,30 +1,30 @@
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { describe, expect, it } from "vitest";
import { DEFAULT_AGENT_TIMEOUT_MS } from "../timeout.js";
import { testing } from "./run.js";
import {
EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
resolveEmbeddedRunLaneTimeoutMs,
} from "./run/lane-runtime.js";
describe("resolveEmbeddedRunLaneTimeoutMs", () => {
it("adds queue grace to explicit run timeouts", () => {
expect(testing.resolveEmbeddedRunLaneTimeoutMs(60_000)).toBe(
60_000 + testing.EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
expect(resolveEmbeddedRunLaneTimeoutMs(60_000)).toBe(
60_000 + EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
);
expect(testing.resolveEmbeddedRunLaneTimeoutMs(60_000.9)).toBe(
60_000 + testing.EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
expect(resolveEmbeddedRunLaneTimeoutMs(60_000.9)).toBe(
60_000 + EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
);
expect(testing.resolveEmbeddedRunLaneTimeoutMs(DEFAULT_AGENT_TIMEOUT_MS + 60_000)).toBe(
DEFAULT_AGENT_TIMEOUT_MS + 60_000 + testing.EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
expect(resolveEmbeddedRunLaneTimeoutMs(DEFAULT_AGENT_TIMEOUT_MS + 60_000)).toBe(
DEFAULT_AGENT_TIMEOUT_MS + 60_000 + EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
);
});
it("keeps the lane watchdog active when the run timeout is disabled", () => {
const defaultLaneTimeoutMs =
DEFAULT_AGENT_TIMEOUT_MS + testing.EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS;
const defaultLaneTimeoutMs = DEFAULT_AGENT_TIMEOUT_MS + EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS;
expect(testing.resolveEmbeddedRunLaneTimeoutMs(0)).toBe(defaultLaneTimeoutMs);
expect(testing.resolveEmbeddedRunLaneTimeoutMs(-1)).toBe(defaultLaneTimeoutMs);
expect(testing.resolveEmbeddedRunLaneTimeoutMs(Number.NaN)).toBe(defaultLaneTimeoutMs);
expect(testing.resolveEmbeddedRunLaneTimeoutMs(MAX_TIMER_TIMEOUT_MS)).toBe(
defaultLaneTimeoutMs,
);
expect(resolveEmbeddedRunLaneTimeoutMs(0)).toBe(defaultLaneTimeoutMs);
expect(resolveEmbeddedRunLaneTimeoutMs(-1)).toBe(defaultLaneTimeoutMs);
expect(resolveEmbeddedRunLaneTimeoutMs(Number.NaN)).toBe(defaultLaneTimeoutMs);
expect(resolveEmbeddedRunLaneTimeoutMs(MAX_TIMER_TIMEOUT_MS)).toBe(defaultLaneTimeoutMs);
});
});
-10
View File
@@ -1,14 +1,4 @@
/**
* Public embedded-agent run entrypoint.
*/
import {
EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
resolveEmbeddedRunLaneTimeoutMs,
} from "./run/lane-runtime.js";
export { runEmbeddedAgent } from "./run-orchestrator.js";
export const testing = {
EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
resolveEmbeddedRunLaneTimeoutMs,
};
@@ -0,0 +1,17 @@
import "./runs.js";
type EmbeddedRunsTestApi = {
resetActiveEmbeddedRuns(): void;
};
function getTestApi(): EmbeddedRunsTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.embeddedRunsTestApi")
];
if (!api) {
throw new Error("embedded runs test API is unavailable");
}
return api as EmbeddedRunsTestApi;
}
export const testing = getTestApi();
@@ -6,10 +6,10 @@ import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
testing as replyRunTesting,
createReplyOperation,
isReplyRunActiveForSessionId,
} from "../../auto-reply/reply/reply-run-registry.js";
import { testing as replyRunTesting } from "../../auto-reply/reply/reply-run-registry.test-support.js";
import { setDiagnosticsEnabledForProcess } from "../../infra/diagnostic-events.js";
import { resetDiagnosticRunActivityForTest } from "../../logging/diagnostic-run-activity.js";
import { markDiagnosticToolStartedForTest } from "../../logging/diagnostic-run-activity.test-support.js";
@@ -22,7 +22,6 @@ import { createUserTurnTranscriptRecorder } from "../../sessions/user-turn-trans
import { createTestUserTurnTranscriptTarget } from "../../sessions/user-turn-transcript.test-support.js";
import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js";
import {
testing,
abortAndDrainEmbeddedAgentRun,
abortEmbeddedAgentRun,
clearActiveEmbeddedRun,
@@ -45,6 +44,7 @@ import {
waitForActiveEmbeddedRuns,
waitForEmbeddedAgentRunEnd,
} from "./runs.js";
import { testing } from "./runs.test-support.js";
type RunHandle = Parameters<typeof setActiveEmbeddedRun>[1];
@@ -920,8 +920,7 @@ describe("embedded-agent runner run registry", () => {
);
const handle = createRunHandle();
runsA.testing.resetActiveEmbeddedRuns();
runsB.testing.resetActiveEmbeddedRuns();
testing.resetActiveEmbeddedRuns();
try {
runsA.setActiveEmbeddedRun("session-shared", handle);
@@ -930,8 +929,7 @@ describe("embedded-agent runner run registry", () => {
runsB.clearActiveEmbeddedRun("session-shared", handle);
expect(runsA.isEmbeddedAgentRunActive("session-shared")).toBe(false);
} finally {
runsA.testing.resetActiveEmbeddedRuns();
runsB.testing.resetActiveEmbeddedRuns();
testing.resetActiveEmbeddedRuns();
}
});
+6 -1
View File
@@ -927,7 +927,7 @@ function forceClearEmbeddedAgentRun(
return forceClearReplyRunBySessionId(sessionId, cause) || cleared;
}
export const testing = {
const testing = {
resetActiveEmbeddedRuns() {
for (const waiters of EMBEDDED_RUN_WAITERS.values()) {
for (const waiter of waiters) {
@@ -947,4 +947,9 @@ export const testing = {
ABANDONED_EMBEDDED_RUN_SESSION_IDS_BY_FILE.clear();
},
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.embeddedRunsTestApi")] =
testing;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -0,0 +1,32 @@
import fs from "node:fs/promises";
import "./session-manager-cache.js";
type SessionManagerCache = {
clear(): void;
isSessionManagerCached(sessionFile: string): boolean;
keys(): string[];
prewarmSessionFile(sessionFile: string): Promise<void>;
trackSessionManagerAccess(sessionFile: string): void;
};
type SessionManagerCacheTestApi = {
createSessionManagerCache(options?: {
clock?: () => number;
fsModule?: Pick<typeof fs, "open">;
ttlMs?: number | (() => number);
}): SessionManagerCache;
};
function getTestApi(): SessionManagerCacheTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.sessionManagerCacheTestApi")
];
if (!api) {
throw new Error("session manager cache test API is unavailable");
}
return api as SessionManagerCacheTestApi;
}
export const createSessionManagerCache: SessionManagerCacheTestApi["createSessionManagerCache"] = (
options,
) => getTestApi().createSessionManagerCache(options);
@@ -1,7 +1,7 @@
// Session manager cache tests cover TTL pruning and explicit cache-disable
// behavior for transcript-backed session managers.
import { describe, expect, it } from "vitest";
import { createSessionManagerCache } from "./session-manager-cache.js";
import { createSessionManagerCache } from "./session-manager-cache.test-support.js";
describe("session manager cache", () => {
it("prunes expired entries during later cache activity even without revisiting them", () => {
@@ -25,7 +25,7 @@ type SessionManagerCache = {
trackSessionManagerAccess: (sessionFile: string) => void;
};
export function createSessionManagerCache(options?: {
function createSessionManagerCache(options?: {
clock?: () => number;
fsModule?: Pick<typeof fs, "open">;
ttlMs?: number | (() => number);
@@ -87,3 +87,8 @@ export function trackSessionManagerAccess(sessionFile: string): void {
export async function prewarmSessionFile(sessionFile: string): Promise<void> {
await sessionManagerCache.prewarmSessionFile(sessionFile);
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.sessionManagerCacheTestApi")] =
{ createSessionManagerCache };
}
@@ -0,0 +1,63 @@
import type { ReplyDirectiveParseResult } from "../auto-reply/reply/reply-directives.js";
import type { AssistantPhase } from "../shared/chat-message-content.js";
import "./embedded-agent-subscribe.handlers.messages.js";
import type { EmbeddedAgentSubscribeState } from "./embedded-agent-subscribe.handlers.types.js";
type AssistantStreamDataParams = {
text?: string;
delta?: string;
replace?: boolean;
mediaUrls?: string[];
mediaUrl?: string;
phase?: AssistantPhase;
itemId?: string;
};
type AssistantStreamData = {
text: string;
delta: string;
replace?: true;
mediaUrls?: string[];
phase?: AssistantPhase;
itemId?: string;
};
type EmbeddedSubscribeMessagesTestApi = {
buildAssistantStreamData(params: AssistantStreamDataParams): AssistantStreamData;
recordPendingAssistantReplyDirectives(
state: Pick<EmbeddedAgentSubscribeState, "pendingAssistantReplyDirectives">,
parsed: ReplyDirectiveParseResult | null | undefined,
): void;
resolveSilentReplyFallbackText(params: {
text: unknown;
messagingToolSentTexts: string[];
}): string;
};
function getTestApi(): EmbeddedSubscribeMessagesTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.embeddedSubscribeMessagesTestApi")
];
if (!api) {
throw new Error("embedded subscribe messages test API is unavailable");
}
return api as EmbeddedSubscribeMessagesTestApi;
}
export function buildAssistantStreamData(params: AssistantStreamDataParams): AssistantStreamData {
return getTestApi().buildAssistantStreamData(params);
}
export function recordPendingAssistantReplyDirectives(
state: Pick<EmbeddedAgentSubscribeState, "pendingAssistantReplyDirectives">,
parsed: ReplyDirectiveParseResult | null | undefined,
): void {
getTestApi().recordPendingAssistantReplyDirectives(state, parsed);
}
export function resolveSilentReplyFallbackText(params: {
text: unknown;
messagingToolSentTexts: string[];
}): string {
return getTestApi().resolveSilentReplyFallbackText(params);
}
@@ -4,7 +4,6 @@ import { describe, expect, it, vi } from "vitest";
import { createInlineCodeState } from "../../packages/markdown-core/src/code-spans.js";
import { createStreamingDirectiveAccumulator } from "../auto-reply/reply/streaming-directives.js";
import {
buildAssistantStreamData,
consumePendingAssistantReplyDirectivesIntoReply,
consumePendingToolMediaIntoReply,
consumePendingToolMediaReply,
@@ -12,9 +11,12 @@ import {
handleMessageUpdate,
hasAssistantVisibleReply,
readPendingToolMediaReply,
} from "./embedded-agent-subscribe.handlers.messages.js";
import {
buildAssistantStreamData,
recordPendingAssistantReplyDirectives,
resolveSilentReplyFallbackText,
} from "./embedded-agent-subscribe.handlers.messages.js";
} from "./embedded-agent-subscribe.handlers.messages.test-support.js";
import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js";
import {
createOpenAiResponsesPartial,
@@ -409,7 +409,7 @@ function copyPartialBlockState(
}
/** Replaces a silent-reply token with the latest sent messaging-tool text when available. */
export function resolveSilentReplyFallbackText(params: {
function resolveSilentReplyFallbackText(params: {
text: unknown;
messagingToolSentTexts: string[];
}): string {
@@ -612,7 +612,7 @@ function resolveStreamingReplyText(params: {
}
/** Records parsed reply directives until a sendable reply payload is built. */
export function recordPendingAssistantReplyDirectives(
function recordPendingAssistantReplyDirectives(
state: Pick<EmbeddedAgentSubscribeState, "pendingAssistantReplyDirectives">,
parsed: ReplyDirectiveParseResult | null | undefined,
) {
@@ -666,7 +666,7 @@ export function hasAssistantVisibleReply(params: {
}
/** Builds normalized stream payload data for assistant visible output. */
export function buildAssistantStreamData(params: {
function buildAssistantStreamData(params: {
text?: string;
delta?: string;
replace?: boolean;
@@ -1133,6 +1133,16 @@ export function handleMessageUpdate(
}
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.embeddedSubscribeMessagesTestApi")
] = {
buildAssistantStreamData,
recordPendingAssistantReplyDirectives,
resolveSilentReplyFallbackText,
};
}
/** Handles assistant message-end finalization, block flush, and usage commit. */
export function handleMessageEnd(
ctx: EmbeddedAgentSubscribeContext,
@@ -12,8 +12,11 @@ import {
buildBlockedToolResult,
recordAdjustedParamsForToolCall,
recordStructuredReplayTrustForToolCall,
testing as beforeToolCallTesting,
} from "./agent-tools.before-tool-call.js";
import {
adjustedParamsByToolCallId,
buildAdjustedParamsKey,
} from "./agent-tools.before-tool-call.state.js";
import type { MessagingToolSend } from "./embedded-agent-messaging.types.js";
import { buildEmbeddedRunPayloads } from "./embedded-agent-runner/run/payloads.js";
import {
@@ -30,6 +33,8 @@ type ToolExecutionStartEvent = Extract<AgentEvent, { type: "tool_execution_start
type ToolExecutionEndEvent = Extract<AgentEvent, { type: "tool_execution_end" }>;
type PayloadToolMetas = Parameters<typeof buildEmbeddedRunPayloads>[0]["toolMetas"];
const beforeToolCallTesting = { adjustedParamsByToolCallId, buildAdjustedParamsKey };
function createTestContext(): {
ctx: ToolHandlerContext;
warn: ReturnType<typeof vi.fn>;
@@ -4,8 +4,8 @@ import { describe, expect, it } from "vitest";
import {
extractToolResultMediaArtifact,
filterToolResultMediaUrls,
isToolResultMediaTrusted,
} from "./embedded-agent-subscribe.tools.js";
import { isToolResultMediaTrusted } from "./embedded-agent-subscribe.tools.test-support.js";
describe("extractToolResultMediaArtifact", () => {
it("returns undefined for null/undefined", () => {
@@ -0,0 +1,27 @@
import "./embedded-agent-subscribe.tools.js";
type EmbeddedSubscribeToolsTestApi = {
isToolResultMediaTrusted(
toolName?: string,
result?: unknown,
trustedLocalMediaToolNames?: ReadonlySet<string>,
): boolean;
};
function getTestApi(): EmbeddedSubscribeToolsTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.embeddedSubscribeToolsTestApi")
];
if (!api) {
throw new Error("embedded subscribe tools test API is unavailable");
}
return api as EmbeddedSubscribeToolsTestApi;
}
export function isToolResultMediaTrusted(
toolName?: string,
result?: unknown,
trustedLocalMediaToolNames?: ReadonlySet<string>,
): boolean {
return getTestApi().isToolResultMediaTrusted(toolName, result, trustedLocalMediaToolNames);
}
+7 -1
View File
@@ -643,7 +643,7 @@ function isExternalToolResult(result: unknown): boolean {
return typeof details.mcpServer === "string" || typeof details.mcpTool === "string";
}
export function isToolResultMediaTrusted(
function isToolResultMediaTrusted(
toolName?: string,
result?: unknown,
trustedLocalMediaToolNames?: ReadonlySet<string>,
@@ -658,6 +658,12 @@ export function isToolResultMediaTrusted(
return isCoreToolResultMediaTrustedName(toolName);
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.embeddedSubscribeToolsTestApi")
] = { isToolResultMediaTrusted };
}
function isTrustedOwnedTtsLocalMedia(
toolName: string | undefined,
result: unknown,
@@ -2,7 +2,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
awaitAgentHarnessAgentEndHook,
clearAgentHarnessFinalizeRetryBudget,
runAgentHarnessAgentEndHook,
runAgentHarnessBeforeAgentFinalizeHook,
runAgentHarnessLlmInputHook,
@@ -30,7 +29,7 @@ const EVENT = {
describe("agent harness lifecycle hook helpers", () => {
afterEach(() => {
clearAgentHarnessFinalizeRetryBudget();
Reflect.deleteProperty(globalThis, Symbol.for("openclaw.pluginFinalizeRetryBudget"));
});
it("ignores legacy hook runners that advertise llm_input without a runner method", () => {
@@ -126,83 +125,6 @@ describe("agent harness lifecycle hook helpers", () => {
).resolves.toEqual({ action: "continue" });
});
it("clears finalize retry budgets by run id", async () => {
const hookRunner = {
hasHooks: () => true,
runBeforeAgentFinalize: vi.fn().mockResolvedValue({
action: "revise",
retry: {
instruction: "revise once",
idempotencyKey: "stable",
maxAttempts: 1,
},
}),
};
// Retry budgets are per run. A finalize hook may ask for exactly one
// revision, and clearing the run id should make that run eligible again.
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: EVENT,
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "revise", reason: "revise once" });
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: EVENT,
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "continue" });
clearAgentHarnessFinalizeRetryBudget({ runId: "run-1" });
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: EVENT,
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "revise", reason: "revise once" });
});
it("does not clear finalize retry budgets for runs that only share a prefix", async () => {
const hookRunner = {
hasHooks: () => true,
runBeforeAgentFinalize: vi.fn().mockResolvedValue({
action: "revise",
retry: {
instruction: "revise child once",
idempotencyKey: "stable",
maxAttempts: 1,
},
}),
};
const childEvent = {
...EVENT,
runId: "run:child",
};
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: childEvent,
ctx: { runId: "run:child", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "revise", reason: "revise child once" });
clearAgentHarnessFinalizeRetryBudget({ runId: "run" });
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: childEvent,
ctx: { runId: "run:child", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "continue" });
});
it("keys finalize retry budgets by context run id when the event omits run id", async () => {
const hookRunner = {
hasHooks: () => true,
@@ -235,16 +157,6 @@ describe("agent harness lifecycle hook helpers", () => {
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "continue" });
clearAgentHarnessFinalizeRetryBudget({ runId: "run-from-context" });
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: eventWithoutRunId,
ctx: { runId: "run-from-context", sessionKey: "agent:main:shared-session" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "revise", reason: "revise from context run" });
});
it("preserves merged revise reasons when retry metadata is present", async () => {
@@ -64,16 +64,6 @@ function buildFinalizeRetryInstructionKey(instruction: string): string {
return `instruction:${createHash("sha256").update(instruction).digest("hex")}`;
}
/** Clears before-finalize retry budgets globally or for one run. */
export function clearAgentHarnessFinalizeRetryBudget(params?: { runId?: string }): void {
const budget = getFinalizeRetryBudget();
if (!params?.runId) {
budget.clear();
return;
}
budget.delete(params.runId);
}
/** Dispatches best-effort LLM input hooks for a harness attempt. */
export function runAgentHarnessLlmInputHook(params: {
event: PluginHookLlmInputEvent;
@@ -3,12 +3,12 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { runWithAgentRingZeroTools } from "../agent-tools.ring-zero-context.js";
import { createStubTool } from "../test-helpers/agent-tool-stubs.js";
import {
testing,
TOOL_CALL_RAW_TOOL_NAME,
TOOL_DESCRIBE_RAW_TOOL_NAME,
TOOL_SEARCH_CODE_MODE_TOOL_NAME,
TOOL_SEARCH_RAW_TOOL_NAME,
} from "../tool-search.js";
import { testing } from "../tool-search.test-support.js";
import { createAgentHarnessToolSurfaceRuntime } from "./tool-surface-bridge.js";
function tools(names: string[]) {
+6 -7
View File
@@ -131,7 +131,7 @@ let mockedResolveAuthProfileOrder: ReturnType<
typeof vi.mocked<AuthProfilesOrderModule["resolveAuthProfileOrder"]>
>;
let runWithModelFallback: ModelFallbackModule["runWithModelFallback"];
let modelFallbackTesting: ModelFallbackModule["testing"];
let resolveCooldownDecision: (typeof import("./model-fallback.test-support.js"))["resolveCooldownDecision"];
let probeThrottleInternals: ModelFallbackModule["probeThrottleInternals"];
let resetLogger: LoggerModule["resetLogger"];
let setLoggerOverride: LoggerModule["setLoggerOverride"];
@@ -147,6 +147,7 @@ async function loadModelFallbackProbeModules() {
const authProfilesOrderModule = await import("./auth-profiles/order.js");
const loggerModule = await import("../logging/logger.js");
const modelFallbackModule = await import("./model-fallback.js");
const modelFallbackTestSupport = await import("./model-fallback.test-support.js");
mockedEnsureAuthProfileStore = vi.mocked(authProfilesStoreModule.ensureAuthProfileStore);
mockedHasAnyAuthProfileStoreSource = vi.mocked(
authProfilesSourceCheckModule.hasAnyAuthProfileStoreSource,
@@ -158,7 +159,7 @@ async function loadModelFallbackProbeModules() {
);
mockedResolveAuthProfileOrder = vi.mocked(authProfilesOrderModule.resolveAuthProfileOrder);
runWithModelFallback = modelFallbackModule.runWithModelFallback;
modelFallbackTesting = modelFallbackModule.testing;
resolveCooldownDecision = modelFallbackTestSupport.resolveCooldownDecision;
probeThrottleInternals = modelFallbackModule.probeThrottleInternals;
resetLogger = loggerModule.resetLogger;
setLoggerOverride = loggerModule.setLoggerOverride;
@@ -292,7 +293,7 @@ describe("runWithModelFallback probe logic", () => {
if (params.usageStats) {
authStore.usageStats = params.usageStats;
}
return modelFallbackTesting.resolveCooldownDecision({
return resolveCooldownDecision({
candidate: OPENAI_PROBE_CANDIDATE,
isPrimary: params.isPrimary ?? true,
requestedModel: params.requestedModel ?? true,
@@ -302,16 +303,14 @@ describe("runWithModelFallback probe logic", () => {
authRuntime: {
getSoonestCooldownExpiry: mockedGetSoonestCooldownExpiry,
resolveProfilesUnavailableReason: mockedResolveProfilesUnavailableReason,
} as unknown as Parameters<
typeof modelFallbackTesting.resolveCooldownDecision
>[0]["authRuntime"],
} as unknown as Parameters<typeof resolveCooldownDecision>[0]["authRuntime"],
authStore,
profileIds: ["openai-profile-1"],
});
}
function expectOpenAiProbeSuspension(
decision: ReturnType<ModelFallbackModule["testing"]["resolveCooldownDecision"]>,
decision: ReturnType<typeof resolveCooldownDecision>,
reason: "rate_limit" | "billing",
) {
expect(decision).toEqual({
+47
View File
@@ -0,0 +1,47 @@
import type { AuthProfileStore } from "./auth-profiles/types.js";
import type { FailoverReason } from "./embedded-agent-helpers/types.js";
import type { ModelCandidate } from "./model-fallback.types.js";
import "./model-fallback.js";
type CooldownDecision =
| { type: "skip"; reason: FailoverReason; error: string }
| { type: "attempt"; reason: FailoverReason; markProbe: boolean }
| { type: "suspend_lanes"; reason: FailoverReason; leaderCandidate?: ModelCandidate };
type ModelFallbackTestApi = {
resolveCooldownDecision(params: {
candidate: ModelCandidate;
isPrimary: boolean;
requestedModel: boolean;
hasFallbackCandidates: boolean;
now: number;
probeThrottleKey: string;
authRuntime: typeof import("./model-fallback-auth.runtime.js");
authStore: AuthProfileStore;
profileIds: string[];
}): CooldownDecision;
shouldDiscardDeferredSessionSuspension(params: {
error: unknown;
abortSignal?: AbortSignal;
}): boolean;
};
function getTestApi(): ModelFallbackTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.modelFallbackTestApi")
];
if (!api) {
throw new Error("model fallback test API is unavailable");
}
return api as ModelFallbackTestApi;
}
export const resolveCooldownDecision: ModelFallbackTestApi["resolveCooldownDecision"] = (params) =>
getTestApi().resolveCooldownDecision(params);
export function shouldDiscardDeferredSessionSuspension(params: {
error: unknown;
abortSignal?: AbortSignal;
}): boolean {
return getTestApi().shouldDiscardDeferredSessionSuspension(params);
}
+9 -1
View File
@@ -30,18 +30,26 @@ import type { AgentHarness } from "./harness/types.js";
import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
import {
isFallbackSummaryError,
testing,
resolveModelCandidateChain,
runWithImageModelFallback,
runWithModelFallback as runWithModelFallbackBase,
} from "./model-fallback.js";
import { shouldDiscardDeferredSessionSuspension } from "./model-fallback.test-support.js";
import {
createAgentRunDirectAbortError,
createAgentRunRestartAbortError,
resolveAgentRunErrorLifecycleFields,
} from "./run-termination.js";
import { resolveSessionSuspensionReason } from "./session-suspension.js";
import { SessionWriteLockTimeoutError } from "./session-write-lock-error.js";
import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js";
const testing = {
resolveFallbackCandidates: resolveModelCandidateChain,
resolveSessionSuspensionReason,
shouldDiscardDeferredSessionSuspension,
};
type ProviderModelNormalizationParams = { provider: string; context: { modelId: string } };
function makeCommandLaneTaskTimeoutError(lane: string, timeoutMs: number): Error {
+7 -8
View File
@@ -889,14 +889,6 @@ export function resolveImageFallbackDefaultProvider(cfg: OpenClawConfig | undefi
return DEFAULT_PROVIDER;
}
export const testing = {
resolveFallbackCandidates: resolveModelCandidateChain,
resolveImageFallbackCandidates,
resolveCooldownDecision,
resolveSessionSuspensionReason,
shouldDiscardDeferredSessionSuspension,
} as const;
export function resolveModelCandidateChain(
params: {
cfg: OpenClawConfig | undefined;
@@ -1412,6 +1404,13 @@ function shouldDiscardDeferredSessionSuspension(params: {
);
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.modelFallbackTestApi")] = {
resolveCooldownDecision,
shouldDiscardDeferredSessionSuspension,
};
}
export async function runWithModelFallback<T>(
params: RunWithModelFallbackParams<T>,
): Promise<ModelFallbackRunResult<T>> {
-163
View File
@@ -4,14 +4,11 @@
* Validates theme JSON, resolves color variables, watches custom theme files, and exposes terminal styling helpers.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { getCapabilities } from "@earendil-works/pi-tui";
import chalk from "chalk";
import { type Static, Type } from "typebox";
import { Compile } from "typebox/compile";
import { getCustomThemesDir, getThemesDir } from "../../../config.js";
import type { SourceInfo } from "../../../sessions/source-info.js";
import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js";
import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.js";
// ============================================================================
@@ -456,21 +453,6 @@ export class Theme {
// Theme Loading
// ============================================================================
let BUILTIN_THEMES: Record<string, ThemeJson> | undefined;
function getBuiltinThemes(): Record<string, ThemeJson> {
if (!BUILTIN_THEMES) {
const themesDir = getThemesDir();
const darkPath = path.join(themesDir, "dark.json");
const lightPath = path.join(themesDir, "light.json");
BUILTIN_THEMES = {
dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson,
light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson,
};
}
return BUILTIN_THEMES;
}
function parseThemeJson(label: string, json: unknown): ThemeJson {
if (!validateThemeJson.Check(json)) {
const errors = Array.from(validateThemeJson.Errors(json));
@@ -522,31 +504,6 @@ function parseThemeJsonContent(label: string, content: string): ThemeJson {
return parseThemeJson(label, json);
}
function loadThemeJson(name: string): ThemeJson {
const builtinThemes = getBuiltinThemes();
if (name in builtinThemes) {
const builtinTheme = builtinThemes[name];
if (builtinTheme) {
return builtinTheme;
}
}
const registeredTheme = registeredThemes.get(name);
if (registeredTheme?.sourcePath) {
const content = fs.readFileSync(registeredTheme.sourcePath, "utf-8");
return parseThemeJsonContent(registeredTheme.sourcePath, content);
}
if (registeredTheme) {
throw new Error(`Theme "${name}" does not have a source path for export`);
}
const customThemesDir = getCustomThemesDir();
const themePath = path.join(customThemesDir, `${name}.json`);
if (!fs.existsSync(themePath)) {
throw new Error(`Theme not found: ${name}`);
}
const content = fs.readFileSync(themePath, "utf-8");
return parseThemeJsonContent(name, content);
}
function createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string): Theme {
const colorMode = mode ?? (getCapabilities().trueColor ? "truecolor" : "256color");
const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
@@ -579,15 +536,6 @@ export function loadThemeFromPath(themePath: string, mode?: ColorMode): Theme {
return createTheme(themeJson, mode, themePath);
}
function loadTheme(name: string, mode?: ColorMode): Theme {
const registeredTheme = registeredThemes.get(name);
if (registeredTheme) {
return registeredTheme;
}
const themeJson = loadThemeJson(name);
return createTheme(themeJson, mode);
}
// ============================================================================
// Global Theme Instance
// ============================================================================
@@ -607,116 +555,6 @@ export const theme: Theme = new Proxy({} as Theme, {
},
});
function setGlobalTheme(t: Theme): void {
(globalThis as Record<symbol, Theme>)[THEME_KEY] = t;
}
let currentThemeName: string | undefined;
let themeWatcher: fs.FSWatcher | undefined;
let themeReloadTimer: NodeJS.Timeout | undefined;
const registeredThemes = new Map<string, Theme>();
export function setTheme(
name: string,
enableWatcher = false,
): { success: boolean; error?: string } {
currentThemeName = name;
try {
setGlobalTheme(loadTheme(name));
if (enableWatcher) {
startThemeWatcher();
}
return { success: true };
} catch (error) {
// Theme is invalid - fall back to dark theme
currentThemeName = "dark";
setGlobalTheme(loadTheme("dark"));
// Don't start watcher for fallback theme
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function startThemeWatcher(): void {
stopThemeWatcher();
// Only watch if it's a custom theme (not built-in)
if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
return;
}
const customThemesDir = getCustomThemesDir();
const watchedThemeName = currentThemeName;
const watchedFileName = `${watchedThemeName}.json`;
const themeFile = path.join(customThemesDir, watchedFileName);
// Only watch if the file exists
if (!fs.existsSync(themeFile)) {
return;
}
const scheduleReload = () => {
if (themeReloadTimer) {
clearTimeout(themeReloadTimer);
}
themeReloadTimer = setTimeout(() => {
themeReloadTimer = undefined;
// Ignore stale timers after switching themes or stopping the watcher
if (currentThemeName !== watchedThemeName) {
return;
}
// Keep the last successfully loaded theme active if the file is temporarily missing
if (!fs.existsSync(themeFile)) {
return;
}
try {
// Reload the theme from disk and refresh the registry cache
const reloadedTheme = loadThemeFromPath(themeFile);
registeredThemes.set(watchedThemeName, reloadedTheme);
setGlobalTheme(reloadedTheme);
} catch {
// Ignore errors (file might be in invalid state while being edited)
}
}, 100);
};
themeWatcher =
watchWithErrorHandler(
customThemesDir,
(_eventType, filename) => {
if (currentThemeName !== watchedThemeName) {
return;
}
if (!filename) {
scheduleReload();
return;
}
if (filename !== watchedFileName) {
return;
}
scheduleReload();
},
() => {
closeWatcher(themeWatcher);
themeWatcher = undefined;
},
) ?? undefined;
}
function stopThemeWatcher(): void {
if (themeReloadTimer) {
clearTimeout(themeReloadTimer);
themeReloadTimer = undefined;
}
closeWatcher(themeWatcher);
themeWatcher = undefined;
}
// ============================================================================
// HTML Export Helpers
// ============================================================================
@@ -855,4 +693,3 @@ export function getLanguageFromPath(filePath: string): string | undefined {
return extToLang[ext];
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -0,0 +1,36 @@
import type { detectOpenAICompletionsCompat } from "./openai-completions-compat.js";
import type { ProviderEndpointClass } from "./provider-attribution.js";
import "./openai-completions-compat.js";
type OpenAICompletionsCompatDefaultsInput = {
provider?: string;
endpointClass: ProviderEndpointClass;
knownProviderFamily: string;
supportsNativeStreamingUsageCompat?: boolean;
supportsOpenAICompletionsStreamingUsageCompat?: boolean;
usesExplicitProxyLikeEndpoint?: boolean;
};
type OpenAICompletionsCompatDefaults = ReturnType<typeof detectOpenAICompletionsCompat>["defaults"];
type OpenAICompletionsCompatTestApi = {
resolveOpenAICompletionsCompatDefaults(
input: OpenAICompletionsCompatDefaultsInput,
): OpenAICompletionsCompatDefaults;
};
function getTestApi(): OpenAICompletionsCompatTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.openAICompletionsCompatTestApi")
];
if (!api) {
throw new Error("OpenAI completions compat test API is unavailable");
}
return api as OpenAICompletionsCompatTestApi;
}
export function resolveOpenAICompletionsCompatDefaults(
input: OpenAICompletionsCompatDefaultsInput,
): OpenAICompletionsCompatDefaults {
return getTestApi().resolveOpenAICompletionsCompatDefaults(input);
}
+2 -4
View File
@@ -1,9 +1,7 @@
// Verifies OpenAI-compatible endpoint defaults for streaming usage and reasoning payloads.
import { describe, expect, it } from "vitest";
import {
detectOpenAICompletionsCompat,
resolveOpenAICompletionsCompatDefaults,
} from "./openai-completions-compat.js";
import { detectOpenAICompletionsCompat } from "./openai-completions-compat.js";
import { resolveOpenAICompletionsCompatDefaults } from "./openai-completions-compat.test-support.js";
describe("resolveOpenAICompletionsCompatDefaults", () => {
it("keeps streaming usage enabled for provider-declared compatible endpoints", () => {
+7 -1
View File
@@ -40,7 +40,7 @@ function isDefaultRouteProvider(provider: string | undefined, ...ids: string[])
}
/** Resolves default request flags for an OpenAI-compatible completions endpoint. */
export function resolveOpenAICompletionsCompatDefaults(
function resolveOpenAICompletionsCompatDefaults(
input: OpenAICompletionsCompatDefaultsInput,
): OpenAICompletionsCompatDefaults {
const {
@@ -169,3 +169,9 @@ export function detectOpenAICompletionsCompat(
}),
};
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.openAICompletionsCompatTestApi")
] = { resolveOpenAICompletionsCompatDefaults };
}
+4 -6
View File
@@ -38,16 +38,14 @@ vi.mock("../config/config.js", () => ({
import "./test-helpers/fast-openclaw-tools-sessions.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import {
testing as embeddedRunsTesting,
setActiveEmbeddedRun,
} from "./embedded-agent-runner/runs.js";
import { setActiveEmbeddedRun } from "./embedded-agent-runner/runs.js";
import { testing as embeddedRunsTesting } from "./embedded-agent-runner/runs.test-support.js";
import { testing as agentStepTesting } from "./tools/agent-step.test-support.js";
import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
import { createSessionsListTool } from "./tools/sessions-list-tool.js";
import { testing as sessionsResolutionTesting } from "./tools/sessions-resolution.js";
import { testing as sessionsResolutionTesting } from "./tools/sessions-resolution.test-support.js";
import { createSessionsSearchTool } from "./tools/sessions-search-tool.js";
import { testing as sessionsSendA2ATesting } from "./tools/sessions-send-tool.a2a.js";
import { testing as sessionsSendA2ATesting } from "./tools/sessions-send-tool.a2a.test-support.js";
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
const TEST_CONFIG = {
@@ -1,6 +1,6 @@
// Shared subagent tool test harness for gateway/config/queue dependency overrides.
import { vi } from "vitest";
import { testing as queueCleanupTesting } from "../auto-reply/reply/queue/cleanup.js";
import { testing as queueCleanupTesting } from "../auto-reply/reply/queue/cleanup.test-support.js";
import type { CallGatewayOptions } from "../gateway/call.js";
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
import { testing as subagentAnnounceTesting } from "./subagent-announce.js";
@@ -0,0 +1,30 @@
import "./session-suspension.js";
type SessionSuspensionTestApi = {
resetSessionSuspensionStateForTest(): void;
seedClearedLaneResumeForTest(
laneId: string,
cleared: { resumeConcurrency: number; resumeAtMs: number },
): void;
};
function getTestApi(): SessionSuspensionTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.sessionSuspensionTestApi")
];
if (!api) {
throw new Error("session suspension test API is unavailable");
}
return api as SessionSuspensionTestApi;
}
export function resetSessionSuspensionStateForTest(): void {
getTestApi().resetSessionSuspensionStateForTest();
}
export function seedClearedLaneResumeForTest(
laneId: string,
cleared: { resumeConcurrency: number; resumeAtMs: number },
): void {
getTestApi().seedClearedLaneResumeForTest(laneId, cleared);
}
+12 -10
View File
@@ -45,8 +45,9 @@ describe("session suspension", () => {
vi.clearAllTimers();
}
vi.useRealTimers();
const { testing } = await import("./session-suspension.js");
testing.resetSessionSuspensionStateForTest();
const { resetSessionSuspensionStateForTest } =
await import("./session-suspension.test-support.js");
resetSessionSuspensionStateForTest();
sessionAccessorMocks.patchSessionEntry.mockClear();
commandQueueMocks.setCommandLaneConcurrency.mockClear();
});
@@ -234,11 +235,12 @@ describe("session suspension", () => {
it("clamps rescheduled cleanup timers after wall-clock rollback", async () => {
vi.useFakeTimers();
vi.setSystemTime(1_000);
const { enableSessionSuspensionTimersForGatewayStart, testing } =
const { enableSessionSuspensionTimersForGatewayStart } =
await import("./session-suspension.js");
const { seedClearedLaneResumeForTest } = await import("./session-suspension.test-support.js");
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
const customLaneId = "plugin:voice:room-3";
testing.seedClearedLaneResumeForTest(customLaneId, {
seedClearedLaneResumeForTest(customLaneId, {
resumeConcurrency: 1,
resumeAtMs: 1_000 + MAX_TIMER_TIMEOUT_MS + 1_000,
});
@@ -386,12 +388,12 @@ describe("session suspension", () => {
});
it("maps failover reasons to persisted suspension reasons", async () => {
const { testing } = await import("./session-suspension.js");
const { resolveSessionSuspensionReason } = await import("./session-suspension.js");
expect(testing.resolveSessionSuspensionReason("rate_limit")).toBe("quota_exhausted");
expect(testing.resolveSessionSuspensionReason("billing")).toBe("manual");
expect(testing.resolveSessionSuspensionReason("overloaded")).toBe("circuit_open");
expect(testing.resolveSessionSuspensionReason("timeout")).toBe("circuit_open");
expect(testing.resolveSessionSuspensionReason("auth")).toBe("circuit_open");
expect(resolveSessionSuspensionReason("rate_limit")).toBe("quota_exhausted");
expect(resolveSessionSuspensionReason("billing")).toBe("manual");
expect(resolveSessionSuspensionReason("overloaded")).toBe("circuit_open");
expect(resolveSessionSuspensionReason("timeout")).toBe("circuit_open");
expect(resolveSessionSuspensionReason("auth")).toBe("circuit_open");
});
});
+28 -24
View File
@@ -413,27 +413,31 @@ async function suspendSessionQueued(params: SessionSuspensionParams, queuedGener
releasePendingWrite();
}
export const testing = {
resetSessionSuspensionStateForTest: () => {
const state = getSessionSuspensionState();
for (const entry of state.laneResumeTimers.values()) {
clearTimeout(entry.timer);
}
state.laneResumeTimers.clear();
state.clearedLaneResumes.clear();
state.pendingSuspensionWrites.clear();
state.suspensionWriteChain = Promise.resolve();
state.cleanupGeneration = 0;
state.cleanupActive = false;
},
seedClearedLaneResumeForTest: (
laneId: string,
cleared: { resumeConcurrency: number; resumeAtMs: number },
) => {
const state = getSessionSuspensionState();
state.cleanupActive = true;
state.clearedLaneResumes.set(laneId, cleared);
},
resolveLaneResumeConcurrency,
resolveSessionSuspensionReason,
} as const;
function resetSessionSuspensionStateForTest(): void {
const state = getSessionSuspensionState();
for (const entry of state.laneResumeTimers.values()) {
clearTimeout(entry.timer);
}
state.laneResumeTimers.clear();
state.clearedLaneResumes.clear();
state.pendingSuspensionWrites.clear();
state.suspensionWriteChain = Promise.resolve();
state.cleanupGeneration = 0;
state.cleanupActive = false;
}
function seedClearedLaneResumeForTest(
laneId: string,
cleared: { resumeConcurrency: number; resumeAtMs: number },
): void {
const state = getSessionSuspensionState();
state.cleanupActive = true;
state.clearedLaneResumes.set(laneId, cleared);
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.sessionSuspensionTestApi")] = {
resetSessionSuspensionStateForTest,
seedClearedLaneResumeForTest,
};
}
@@ -0,0 +1,14 @@
import "./bash.js";
type BashToolTestApi = {
resolveBashTimeoutMs(timeoutSeconds: unknown): number | undefined;
};
function getTestApi(): BashToolTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.bashToolTestApi")
] as BashToolTestApi;
}
export const resolveBashTimeoutMs: BashToolTestApi["resolveBashTimeoutMs"] = (timeoutSeconds) =>
getTestApi().resolveBashTimeoutMs(timeoutSeconds);
+2 -1
View File
@@ -4,7 +4,8 @@ import path from "node:path";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { describe, expect, it } from "vitest";
import type { BashOperations } from "./bash-operations.js";
import { createBashTool, createLocalBashOperations, resolveBashTimeoutMs } from "./bash.js";
import { createBashTool, createLocalBashOperations } from "./bash.js";
import { resolveBashTimeoutMs } from "./bash.test-support.js";
describe("bash tool timeout helpers", () => {
it("converts positive timeout seconds to timer-safe milliseconds", () => {
+7 -1
View File
@@ -28,7 +28,7 @@ const bashSchema = Type.Object({
command: Type.String({ description: "Bash command." }),
timeout: Type.Optional(Type.Number({ description: "Optional timeout seconds; default none." })),
});
export function resolveBashTimeoutMs(timeoutSeconds: unknown): number | undefined {
function resolveBashTimeoutMs(timeoutSeconds: unknown): number | undefined {
if (
typeof timeoutSeconds !== "number" ||
!Number.isFinite(timeoutSeconds) ||
@@ -39,6 +39,12 @@ export function resolveBashTimeoutMs(timeoutSeconds: unknown): number | undefine
return resolveTimerTimeoutMs(timeoutSeconds * 1000, 1);
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.bashToolTestApi")] = {
resolveBashTimeoutMs,
};
}
/**
* Create bash operations using OpenClaw runtime's built-in local shell execution backend.
*
+14 -18
View File
@@ -40,20 +40,22 @@ vi.mock("node:child_process", async () => {
const spawnMock = vi.mocked(spawn);
let toolSearch: typeof import("./tool-search.js");
let testing: (typeof import("./tool-search.test-support.js"))["testing"];
describe("tool-search code-mode stream errors", () => {
beforeAll(async () => {
toolSearch = await import("./tool-search.js");
testing = (await import("./tool-search.test-support.js")).testing;
});
afterEach(() => {
toolSearch.testing.setToolSearchCodeModeSupportedForTest(undefined);
toolSearch.testing.setToolSearchMinCodeTimeoutMsForTest(undefined);
testing.setToolSearchCodeModeSupportedForTest(undefined);
testing.setToolSearchMinCodeTimeoutMsForTest(undefined);
});
it("rejects stderr errors and leaves the unused stdout unpiped", async () => {
toolSearch.testing.setToolSearchCodeModeSupportedForTest(true);
toolSearch.testing.setToolSearchMinCodeTimeoutMsForTest(1000);
testing.setToolSearchCodeModeSupportedForTest(true);
testing.setToolSearchMinCodeTimeoutMsForTest(1000);
let spawnedChild: MockSpawnChild | undefined;
spawnMock.mockImplementationOnce(
@@ -67,15 +69,12 @@ describe("tool-search code-mode stream errors", () => {
},
);
const runtime = new toolSearch.ToolSearchRuntime(
{},
toolSearch.testing.resolveToolSearchConfig({}),
);
const runtime = new toolSearch.ToolSearchRuntime({}, toolSearch.resolveToolSearchConfig({}));
await expect(
toolSearch.testing.runCodeModeChild({
testing.runCodeModeChild({
code: "return 1;",
config: toolSearch.testing.resolveToolSearchConfig({}),
config: toolSearch.resolveToolSearchConfig({}),
logs: [],
parentToolCallId: "call-stderr-error",
runtime,
@@ -89,8 +88,8 @@ describe("tool-search code-mode stream errors", () => {
});
it("keeps stderr tail in exit error messages valid at UTF-16 boundaries", async () => {
toolSearch.testing.setToolSearchCodeModeSupportedForTest(true);
toolSearch.testing.setToolSearchMinCodeTimeoutMsForTest(1000);
testing.setToolSearchCodeModeSupportedForTest(true);
testing.setToolSearchMinCodeTimeoutMsForTest(1000);
spawnMock.mockImplementationOnce(
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
@@ -105,16 +104,13 @@ describe("tool-search code-mode stream errors", () => {
},
);
const runtime = new toolSearch.ToolSearchRuntime(
{},
toolSearch.testing.resolveToolSearchConfig({}),
);
const runtime = new toolSearch.ToolSearchRuntime({}, toolSearch.resolveToolSearchConfig({}));
let caught: Error | undefined;
try {
await toolSearch.testing.runCodeModeChild({
await testing.runCodeModeChild({
code: "return 1;",
config: toolSearch.testing.resolveToolSearchConfig({}),
config: toolSearch.resolveToolSearchConfig({}),
logs: [],
parentToolCallId: "call-stderr-utf16",
runtime,
+47
View File
@@ -0,0 +1,47 @@
import type { ToolSearchCatalogEntry, ToolSearchConfig, ToolSearchRuntime } from "./tool-search.js";
import "./tool-search.js";
type ToolSearchCatalogSession = {
entries: ToolSearchCatalogEntry[];
searchCount: number;
describeCount: number;
callCount: number;
};
type ToolSearchTestApi = {
sessionCatalogs: Map<string, ToolSearchCatalogSession>;
maxToolSchemaDirectoryPromptChars: number;
setToolSearchCodeModeSupportedForTest(value: boolean | undefined): void;
setToolSearchMinCodeTimeoutMsForTest(value: number | undefined): void;
appendToolSearchCodeStderrTail(current: string, chunk: string): string;
runCodeModeChild(params: {
code: string;
config: ToolSearchConfig;
logs: unknown[];
parentToolCallId: string;
runtime: ToolSearchRuntime;
signal?: AbortSignal;
}): Promise<unknown>;
};
function getTestApi(): ToolSearchTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.toolSearchTestApi")
] as ToolSearchTestApi;
}
export const testing: ToolSearchTestApi = {
get sessionCatalogs() {
return getTestApi().sessionCatalogs;
},
get maxToolSchemaDirectoryPromptChars() {
return getTestApi().maxToolSchemaDirectoryPromptChars;
},
setToolSearchCodeModeSupportedForTest: (value) =>
getTestApi().setToolSearchCodeModeSupportedForTest(value),
setToolSearchMinCodeTimeoutMsForTest: (value) =>
getTestApi().setToolSearchMinCodeTimeoutMsForTest(value),
appendToolSearchCodeStderrTail: (current, chunk) =>
getTestApi().appendToolSearchCodeStderrTail(current, chunk),
runCodeModeChild: (params) => getTestApi().runCodeModeChild(params),
};
+4 -3
View File
@@ -11,7 +11,6 @@ import {
} from "./agent-tools.before-tool-call.js";
import { SESSION_TOOL_STDERR_TAIL_BYTES } from "./sessions/tools/limits.js";
import {
testing,
addClientToolsToToolSearchCatalog,
applyToolSearchCatalog,
applyToolSchemaDirectoryCatalog,
@@ -22,12 +21,14 @@ import {
estimateToolSchemaDirectoryToolNames,
projectToolSearchTargetTranscriptMessages,
registerHeadlessToolSearchCatalog,
resolveToolSearchConfig,
resolveToolSearchCatalogTool,
TOOL_CALL_RAW_TOOL_NAME,
TOOL_DESCRIBE_RAW_TOOL_NAME,
TOOL_SEARCH_CODE_MODE_TOOL_NAME,
TOOL_SEARCH_RAW_TOOL_NAME,
} from "./tool-search.js";
import { testing } from "./tool-search.test-support.js";
import { jsonResult, type AnyAgentTool } from "./tools/common.js";
function fakeTool(name: string, description: string): AnyAgentTool {
@@ -171,7 +172,7 @@ describe("Tool Search", () => {
});
it("enables object config when a mode is set", () => {
const resolved = testing.resolveToolSearchConfig({
const resolved = resolveToolSearchConfig({
tools: {
toolSearch: {
mode: "directory",
@@ -186,7 +187,7 @@ describe("Tool Search", () => {
testing.setToolSearchCodeModeSupportedForTest(false);
try {
const config = { tools: { toolSearch: true } } as never;
const resolved = testing.resolveToolSearchConfig(config);
const resolved = resolveToolSearchConfig(config);
const compacted = applyToolSearchCatalog({
tools: [
fakeTool(TOOL_SEARCH_CODE_MODE_TOOL_NAME, "code mode"),
+6 -2
View File
@@ -143,7 +143,7 @@ type ToolSearchDirectoryIntent = {
type ToolDirectoryFamily = "memory" | "web";
export type ToolSearchCatalogSession = {
type ToolSearchCatalogSession = {
entries: ToolSearchCatalogEntry[];
searchCount: number;
describeCount: number;
@@ -2393,7 +2393,7 @@ export function createToolSearchTools(ctx: ToolSearchToolContext): AnyAgentTool[
];
}
export const testing = {
const testing = {
sessionCatalogs,
reusableCatalogSnapshots,
maxToolSchemaDirectoryPromptChars: MAX_TOOL_SCHEMA_DIRECTORY_PROMPT_CHARS,
@@ -2413,4 +2413,8 @@ export const testing = {
appendToolSearchCodeStderrTail,
runCodeModeChild,
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.toolSearchTestApi")] = testing;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -0,0 +1,22 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import type { ToolModelConfig } from "./model-config.helpers.js";
import "./image-generate-tool.js";
type ImageGenerateToolTestApi = {
resolveImageGenerationModelConfigForTool(params: {
cfg?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
authStore?: AuthProfileStore;
}): ToolModelConfig | null;
};
function getTestApi(): ImageGenerateToolTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.imageGenerateToolTestApi")
] as ImageGenerateToolTestApi;
}
export const resolveImageGenerationModelConfigForTool: ImageGenerateToolTestApi["resolveImageGenerationModelConfigForTool"] =
(params) => getTestApi().resolveImageGenerationModelConfigForTool(params);
+4 -3
View File
@@ -33,7 +33,7 @@ let mediaStore: typeof import("../../media/store.js");
let webMedia: typeof import("../../media/web-media.js");
let resetRecentMediaGenerationDuplicateGuardsForTests: typeof import("../media-generation-task-status-shared.test-support.js").resetRecentMediaGenerationDuplicateGuardsForTests;
let createImageGenerateTool: typeof import("./image-generate-tool.js").createImageGenerateTool;
let resolveImageGenerationModelConfigForTool: typeof import("./image-generate-tool.js").resolveImageGenerationModelConfigForTool;
let resolveImageGenerationModelConfigForTool: typeof import("./image-generate-tool.test-support.js").resolveImageGenerationModelConfigForTool;
const GENERATION_PROVIDER_ENV_VARS = [
"BYTEPLUS_API_KEY",
@@ -337,8 +337,9 @@ describe("createImageGenerateTool", () => {
webMedia = await import("../../media/web-media.js");
({ resetRecentMediaGenerationDuplicateGuardsForTests } =
await import("../media-generation-task-status-shared.test-support.js"));
({ createImageGenerateTool, resolveImageGenerationModelConfigForTool } =
await import("./image-generate-tool.js"));
({ createImageGenerateTool } = await import("./image-generate-tool.js"));
({ resolveImageGenerationModelConfigForTool } =
await import("./image-generate-tool.test-support.js"));
});
beforeEach(() => {
+7 -1
View File
@@ -245,7 +245,7 @@ const ImageGenerateToolSchema = Type.Object({
),
});
export function resolveImageGenerationModelConfigForTool(params: {
function resolveImageGenerationModelConfigForTool(params: {
cfg?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
@@ -261,6 +261,12 @@ export function resolveImageGenerationModelConfigForTool(params: {
});
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.imageGenerateToolTestApi")] = {
resolveImageGenerationModelConfigForTool,
};
}
function hasExplicitImageGenerationModelConfig(cfg?: OpenClawConfig): boolean {
return hasToolModelConfig(coerceToolModelConfig(cfg?.agents?.defaults?.imageGenerationModel));
}
@@ -9,7 +9,8 @@ import type { ModelDefinitionConfig } from "../../config/types.models.js";
import type { ImageDescriptionRequest } from "../../plugin-sdk/media-understanding.js";
import { getApiKeyForModel, hasUsableCustomProviderApiKey } from "../model-auth.js";
import { resolveImageToolFactoryAvailable } from "../openclaw-tools.media-factory-plan.js";
import { createImageTool, resolveImageModelConfigForTool, testing } from "./image-tool.js";
import { createImageTool } from "./image-tool.js";
import { resolveImageModelConfigForTool, testing } from "./image-tool.test-support.js";
import { hasProviderAuthForTool } from "./model-config.helpers.js";
const USER_PROVIDER = "hatchery-qwen3.6-plus";
@@ -21,7 +21,8 @@ import {
} from "../../plugin-sdk/test-env.js";
import { isLiveTestEnabled } from "../live-test-helpers.js";
import { isLiveAuthDrift } from "../live-test-provider-drift.js";
import { createImageTool, testing } from "./image-tool.js";
import { createImageTool } from "./image-tool.js";
import { testing } from "./image-tool.test-support.js";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY?.trim() ?? "";
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY?.trim() ?? "";
@@ -0,0 +1,96 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type {
resolveAutoMediaKeyProviders,
resolveDefaultMediaModel,
} from "../../media-understanding/defaults.js";
import type {
buildMediaUnderstandingRegistry,
getMediaUnderstandingProvider,
} from "../../media-understanding/provider-registry.js";
import type { ImageCompressionPolicy, WebMediaResult } from "../../media/web-media.js";
import type {
describeImageWithModel,
describeImagesWithModel,
} from "../../plugin-sdk/media-understanding.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import type { resolveBundledStaticCatalogModel } from "../embedded-agent-runner/model.static-catalog.js";
import type {
coerceImageAssistantText,
decodeDataUrl,
hasImageReasoningOnlyResponse,
ImageModelConfig,
} from "./image-tool.helpers.js";
import "./image-tool.js";
type ResolveModelAsync = (typeof import("../embedded-agent-runner/model.js"))["resolveModelAsync"];
type ImageToolLoadWebMediaOptions = {
maxBytes?: number;
sandboxValidated?: boolean;
readFile?: (filePath: string) => Promise<Buffer>;
imageCompression?: ImageCompressionPolicy;
localRoots?: readonly string[] | "any";
inboundRoots?: readonly string[];
ssrfPolicy?: ReturnType<
(typeof import("./media-tool-shared.js"))["resolveRemoteMediaSsrfPolicy"]
>;
readIdleTimeoutMs?: number;
};
type ImageWebMediaRuntime = {
loadWebMedia(mediaUrl: string, options?: ImageToolLoadWebMediaOptions): Promise<WebMediaResult>;
optimizeImageBufferForWebMedia: (typeof import("../../media/web-media.js"))["optimizeImageBufferForWebMedia"];
};
type ResolveImageCompressionPolicy = (params: {
cfg?: OpenClawConfig;
imageModelConfig?: ImageModelConfig | null;
modelOverride?: string;
imageCount: number;
agentDir?: string;
workspaceDir?: string;
}) => Promise<ImageCompressionPolicy>;
type ImageToolProviderDeps = {
buildProviderRegistry: typeof buildMediaUnderstandingRegistry;
getMediaUnderstandingProvider: typeof getMediaUnderstandingProvider;
describeImageWithModel: typeof describeImageWithModel;
describeImagesWithModel: typeof describeImagesWithModel;
resolveAutoMediaKeyProviders: typeof resolveAutoMediaKeyProviders;
resolveDefaultMediaModel: typeof resolveDefaultMediaModel;
resolveBundledStaticCatalogModel: typeof resolveBundledStaticCatalogModel;
resolveModelAsync: ResolveModelAsync;
resolveImageCompressionPolicy: ResolveImageCompressionPolicy;
loadImageWebMediaRuntime: () => Promise<ImageWebMediaRuntime>;
};
type ImageToolTestApi = {
decodeDataUrl: typeof decodeDataUrl;
coerceImageAssistantText: typeof coerceImageAssistantText;
hasImageReasoningOnlyResponse: typeof hasImageReasoningOnlyResponse;
resolveImageToolMaxTokens(
modelMaxTokens: number | undefined,
requestedMaxTokens?: number,
): number;
resolveImageCompressionPolicy: ResolveImageCompressionPolicy;
setProviderDepsForTest(overrides?: Partial<ImageToolProviderDeps>): void;
resolveImageModelConfigForTool(params: {
cfg?: OpenClawConfig;
agentDir: string;
workspaceDir?: string;
authStore?: AuthProfileStore;
}): ImageModelConfig | null;
};
function getTestApi(): ImageToolTestApi {
const api = (globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.imageToolTestApi")];
if (!api) {
throw new Error("image tool test API is unavailable");
}
return api as ImageToolTestApi;
}
export const testing = getTestApi();
export const resolveImageModelConfigForTool: ImageToolTestApi["resolveImageModelConfigForTool"] = (
params,
) => testing.resolveImageModelConfigForTool(params);
+2 -1
View File
@@ -22,7 +22,8 @@ import { minimaxUnderstandImage } from "../minimax-vlm.js";
import { createHostSandboxFsBridge } from "../test-helpers/host-sandbox-fs-bridge.js";
import { createUnsafeMountedSandbox } from "../test-helpers/unsafe-mounted-sandbox.js";
import { makeZeroUsageSnapshot } from "../usage.js";
import { testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js";
import { createImageTool } from "./image-tool.js";
import { testing, resolveImageModelConfigForTool } from "./image-tool.test-support.js";
import { resolveMediaToolInboundRoots } from "./media-tool-shared.js";
function jsonRoundTrip<T>(value: T): T {
+9 -2
View File
@@ -180,7 +180,7 @@ function isCanonicalCandidateShadowedByExecutionAlias(
);
}
export const testing = {
const testing = {
decodeDataUrl,
coerceImageAssistantText,
hasImageReasoningOnlyResponse,
@@ -240,7 +240,7 @@ function resolveImageToolMaxTokens(modelMaxTokens: number | undefined, requested
* - same provider (best effort)
* - fall back to OpenAI/Anthropic when available
*/
export function resolveImageModelConfigForTool(params: {
function resolveImageModelConfigForTool(params: {
cfg?: OpenClawConfig;
agentDir: string;
workspaceDir?: string;
@@ -366,6 +366,13 @@ export function resolveImageModelConfigForTool(params: {
});
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.imageToolTestApi")] = {
...testing,
resolveImageModelConfigForTool,
};
}
function resolveImageModelConfigForOverride(params: {
cfg?: OpenClawConfig;
modelOverride?: string;
@@ -0,0 +1,23 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import "./model-config.helpers.js";
type ModelConfigHelpersTestApi = {
hasDirectProviderApiKeyAuthForTool(params: {
provider: string;
cfg?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
authStore?: AuthProfileStore;
modelApi?: string;
}): boolean;
};
function getTestApi(): ModelConfigHelpersTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.modelConfigHelpersTestApi")
] as ModelConfigHelpersTestApi;
}
export const hasDirectProviderApiKeyAuthForTool: ModelConfigHelpersTestApi["hasDirectProviderApiKeyAuthForTool"] =
(params) => getTestApi().hasDirectProviderApiKeyAuthForTool(params);
@@ -4,10 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { AuthProfileCredential, AuthProfileStore } from "../auth-profiles/types.js";
import {
hasDirectProviderApiKeyAuthForTool,
hasProviderAuthForTool,
resolveOpenAiImageMediaCandidate,
} from "./model-config.helpers.js";
import { hasDirectProviderApiKeyAuthForTool } from "./model-config.helpers.test-support.js";
vi.mock("../auth-profiles/external-cli-sync.js", () => ({
resolveExternalCliAuthProfiles: () => [],
+7 -1
View File
@@ -240,7 +240,7 @@ function hasAuthProfileTypeForProvider(params: {
}
/** Returns whether a provider has direct API-key-capable auth for model-backed tools. */
export function hasDirectProviderApiKeyAuthForTool(params: {
function hasDirectProviderApiKeyAuthForTool(params: {
provider: string;
cfg?: OpenClawConfig;
workspaceDir?: string;
@@ -272,6 +272,12 @@ export function hasDirectProviderApiKeyAuthForTool(params: {
});
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.modelConfigHelpersTestApi")] = {
hasDirectProviderApiKeyAuthForTool,
};
}
function hasCanonicalOpenAiCodexAuthSignal(params: {
cfg?: OpenClawConfig;
agentDir?: string;
+1 -1
View File
@@ -11,7 +11,7 @@ import {
resolveSessionToolsVisibility,
} from "../../plugin-sdk/session-visibility.js";
import { resolveSandboxedSessionToolContext } from "./sessions-access.js";
import { testing as sessionsResolutionTesting } from "./sessions-resolution.js";
import { testing as sessionsResolutionTesting } from "./sessions-resolution.test-support.js";
describe("resolveSessionToolsVisibility", () => {
it("defaults to tree when unset or invalid", () => {
@@ -0,0 +1,16 @@
import type { callGateway } from "../../gateway/call.js";
import "./sessions-resolution.js";
type SessionsResolutionTestApi = {
testing: {
setDepsForTest(overrides?: Partial<{ callGateway: typeof callGateway }>): void;
};
};
function getTestApi(): SessionsResolutionTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.sessionsResolutionTestApi")
] as SessionsResolutionTestApi;
}
export const testing = getTestApi().testing;
+7 -1
View File
@@ -471,7 +471,7 @@ export async function resolveVisibleSessionReference(params: {
return { ok: true, key: resolvedKey, displayKey };
}
export const testing = {
const testing = {
setDepsForTest(overrides?: Partial<{ callGateway: GatewayCaller }>) {
sessionsResolutionDeps = overrides
? {
@@ -484,3 +484,9 @@ export const testing = {
);
},
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.sessionsResolutionTestApi")] = {
testing,
};
}
@@ -0,0 +1,18 @@
import type { CallGatewayOptions } from "../../gateway/call.js";
import "./sessions-send-tool.a2a.js";
type GatewayCaller = <T = unknown>(opts: CallGatewayOptions) => Promise<T>;
type SessionsSendA2ATestApi = {
testing: {
setDepsForTest(overrides?: Partial<{ callGateway: GatewayCaller }>): void;
};
};
function getTestApi(): SessionsSendA2ATestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.sessionsSendA2ATestApi")
] as SessionsSendA2ATestApi;
}
export const testing = getTestApi().testing;
@@ -7,7 +7,8 @@ import { createSessionConversationTestRegistry } from "../../test-utils/session-
import { readLatestAssistantReplySnapshot, waitForAgentRun } from "../run-wait.js";
import { runAgentStep } from "./agent-step.js";
import type { SessionListRow } from "./sessions-helpers.js";
import { runSessionsSendA2AFlow, testing } from "./sessions-send-tool.a2a.js";
import { runSessionsSendA2AFlow } from "./sessions-send-tool.a2a.js";
import { testing } from "./sessions-send-tool.a2a.test-support.js";
const callGatewayMock = vi.hoisted(() => vi.fn());
+7 -1
View File
@@ -258,7 +258,7 @@ export async function runSessionsSendA2AFlow(params: {
}
}
export const testing = {
const testing = {
setDepsForTest(overrides?: Partial<{ callGateway: GatewayCaller }>) {
sessionsSendA2ADeps = overrides
? {
@@ -268,3 +268,9 @@ export const testing = {
: defaultSessionsSendA2ADeps;
},
};
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.sessionsSendA2ATestApi")] = {
testing,
};
}
+1 -1
View File
@@ -11,7 +11,7 @@ import { isAgentSessionModelPatchOrigin } from "../../gateway/session-model-patc
import { GATEWAY_OWNER_ONLY_CORE_TOOLS } from "../../security/dangerous-tools.js";
import { withTempDir } from "../../test-helpers/temp-dir.js";
import { createAgentPatchedSessionModelRunGuard } from "../session-model-auto-revert.js";
import { testing as sessionsResolutionTesting } from "./sessions-resolution.js";
import { testing as sessionsResolutionTesting } from "./sessions-resolution.test-support.js";
import { createSessionsTool } from "./sessions-tool.js";
describe("sessions tool", () => {
@@ -0,0 +1,22 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import type { ToolModelConfig } from "./model-config.helpers.js";
import "./video-generate-tool.js";
type VideoGenerateToolTestApi = {
resolveVideoGenerationModelConfigForTool(params: {
cfg?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
authStore?: AuthProfileStore;
}): ToolModelConfig | null;
};
function getTestApi(): VideoGenerateToolTestApi {
return (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("openclaw.videoGenerateToolTestApi")
] as VideoGenerateToolTestApi;
}
export const resolveVideoGenerationModelConfigForTool: VideoGenerateToolTestApi["resolveVideoGenerationModelConfigForTool"] =
(params) => getTestApi().resolveVideoGenerationModelConfigForTool(params);
+2 -4
View File
@@ -17,10 +17,8 @@ import * as videoGenerationRuntime from "../../video-generation/runtime.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import { resetRecentMediaGenerationDuplicateGuardsForTests } from "../media-generation-task-status-shared.test-support.js";
import * as videoGenerateBackground from "./video-generate-background.js";
import {
createVideoGenerateTool,
resolveVideoGenerationModelConfigForTool,
} from "./video-generate-tool.js";
import { createVideoGenerateTool } from "./video-generate-tool.js";
import { resolveVideoGenerationModelConfigForTool } from "./video-generate-tool.test-support.js";
const taskRuntimeInternalMocks = vi.hoisted(() => {
const mocks = {

Some files were not shown because too many files have changed in this diff Show More