mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: () => [],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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
Reference in New Issue
Block a user