Files
openclaw/src/auto-reply/reply.directive.directive-behavior.e2e-mocks.ts
Peter Steinberger 7a8eee4a36 perf(agents): keep turn-path model catalog reads off the full live build (#120834)
* perf(agents): keep turn-path model catalog reads off the full live build

First agent turns (embedded and cron) resolved thinking capability through
loadPreparedModelCatalogSnapshot without readOnly, which materialized the
full live model-runtime catalog: ambient synthetic-auth discovery fanned out
to every registered provider and loaded plugin discovery modules through
jiti source transform (3,172 TS modules, 36s event-loop block, +600MB heap,
58.7s model-selection on a cold gateway).

- add loadProviderScopedThinkingCatalog: manifest metadata first, then a
  provider-scoped read-only static catalog, then scoped live discovery only
  for runtime-discovery providers (preserves #116584 Ollama semantics)
- route scopedLiveProviderDiscovery through the scoped read-only loader
- scope live-mode ambient synthetic-auth refs to the requested providers
- bound the last-resort synthetic-auth sweep to discovery entry modules
- memoize per-turn plugin skill dir resolution/republish (single-slot,
  lifecycle-cleared; was a full walk + symlink republish every turn)

Cold first turn 72.7s -> ~22s wall (remaining cost is provider prefill of
the ~19.5k-token default prompt); model-selection 58,726ms -> 124ms.

* test(agents): align model-catalog.runtime mocks with scoped thinking catalog seam

Explicit vi.mock factories must export every binding prod touches; the new
loadProviderScopedThinkingCatalog export is now mocked everywhere the module
is stubbed, and the live-model-switch Ollama hydration test asserts the new
provider-scoped seam instead of the retired unscoped snapshot call shape.

* test(agents): export scoped thinking catalog from every prepared-catalog mock; split synthetic-auth helpers

- add loadProviderScopedThinkingCatalog to all explicit prepared-model-catalog
  and model-catalog.runtime mock factories (vi.mock factories must export every
  binding prod touches)
- move synthetic-auth ref scoping/resolution into
  prepared-model-runtime.synthetic-auth.ts; keeps facts under the max-lines cap

* test(agents): prove scoped thinking hydration for runtime-only models

Boundary proof for the ClawSweeper review gap: the three-tier helper stops at
manifest or scoped-static when they resolve, and runs provider-scoped live
discovery (no broad fanout) only for runtime-only models; cron selection
hydrates through the same scoped helper and skips it entirely for thinking=off.

* test(agents): accept rest args in scoped thinking catalog mocks
2026-08-08 22:48:40 -07:00

151 lines
6.1 KiB
TypeScript

/** Shared E2E mocks for directive behavior tests that exercise reply-agent dispatch. */
import { vi, type Mock } from "vitest";
export const runEmbeddedAgentMock: Mock = vi.fn();
export const compactEmbeddedAgentSessionMock: Mock = vi.fn();
export const loadModelCatalogMock: Mock = vi.fn();
export const resolveCommandSecretRefsViaGatewayMock: Mock = vi.fn();
export const clearSessionAuthProfileOverrideMock: Mock = vi.fn();
export const resolveSessionAuthProfileOverrideMock: Mock = vi.fn();
function objectRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
}
function normalizeReplyAgentPayload(payload: Record<string, unknown>, params: unknown) {
const text = typeof payload.text === "string" ? payload.text : undefined;
if (!text) {
return payload;
}
const explicitReplyMatch = text.match(/\[\[\s*reply_to\s*:\s*([^\]]+?)\s*\]\]/i);
const explicitReplyToId = explicitReplyMatch?.[1]?.trim();
const replyToCurrentPattern = /\[\[\s*reply_to_current\s*\]\]/gi;
const hasReplyToCurrent = replyToCurrentPattern.test(text);
const currentMessageId = objectRecord(objectRecord(params)?.sessionCtx)?.MessageSid;
// Directive tests encode reply targets in text markers so mocked agents can stay lightweight.
const cleanedText = text
.replace(replyToCurrentPattern, "")
.replace(/\[\[\s*reply_to\s*:\s*([^\]]+?)\s*\]\]/gi, "")
.trim();
return {
...payload,
text: cleanedText,
...(explicitReplyToId
? { replyToId: explicitReplyToId }
: hasReplyToCurrent && typeof currentMessageId === "string"
? { replyToId: currentMessageId, replyToCurrent: true }
: {}),
};
}
async function runMockedReplyAgent(runParams: unknown, params: unknown) {
const result = await runEmbeddedAgentMock(runParams);
const payloadsRaw = objectRecord(result)?.payloads;
const payloads = Array.isArray(payloadsRaw)
? payloadsRaw.flatMap((payload) => {
const record = objectRecord(payload);
return record ? [record] : [];
})
: [];
const normalized = payloads.map((payload) => normalizeReplyAgentPayload(payload, params));
if (normalized.length === 0) {
return undefined;
}
return normalized.length === 1 ? normalized[0] : normalized;
}
/** Runs the mocked reply agent using the follow-up run payload from directive tests. */
export async function runDirectiveBehaviorReplyAgent(params: unknown) {
const runParams = objectRecord(objectRecord(params)?.followupRun)?.run ?? {};
return await runMockedReplyAgent(runParams, params);
}
export const runReplyAgentMock: Mock = vi.fn(runDirectiveBehaviorReplyAgent);
/** Runs the mocked prepared-reply path with the resolved model and elevation settings. */
export async function runDirectiveBehaviorPreparedReply(params: unknown) {
const input = objectRecord(params) ?? {};
const runParams = {
provider: input.provider,
model: input.model,
thinkLevel: input.resolvedThinkLevel,
reasoningLevel: input.resolvedReasoningLevel,
bashElevated: {
enabled: input.elevatedEnabled === true,
allowed: input.elevatedAllowed === true,
defaultLevel:
typeof input.resolvedElevatedLevel === "string" ? input.resolvedElevatedLevel : "off",
fullAccessAvailable: true,
},
};
return await runMockedReplyAgent(runParams, params);
}
export const runPreparedReplyMock: Mock = vi.fn(runDirectiveBehaviorPreparedReply);
vi.mock("../agents/embedded-agent.js", () => ({
abortEmbeddedAgentRun: vi.fn().mockReturnValue(false),
compactEmbeddedAgentSession: (...args: unknown[]) => compactEmbeddedAgentSessionMock(...args),
runEmbeddedAgent: (...args: unknown[]) => runEmbeddedAgentMock(...args),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false),
isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false),
}));
vi.mock("../agents/embedded-agent.runtime.js", () => ({
abortEmbeddedAgentRun: vi.fn().mockReturnValue(false),
compactEmbeddedAgentSession: (...args: unknown[]) => compactEmbeddedAgentSessionMock(...args),
runEmbeddedAgent: (...args: unknown[]) => runEmbeddedAgentMock(...args),
resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false),
isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false),
waitForEmbeddedAgentRunEnd: vi.fn().mockResolvedValue(true),
}));
vi.mock("../agents/prepared-model-catalog.js", () => ({
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
loadPreparedModelCatalog: loadModelCatalogMock,
}));
vi.mock("../agents/thinking-runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/thinking-runtime.js")>();
return {
...actual,
// These tests cover directive acknowledgements and persistence, not harness selection.
// Keep each directive from loading unrelated provider-route metadata through auto selection.
resolveEffectiveAgentRuntime: () => "openclaw",
};
});
vi.mock("../cli/command-secret-gateway.js", () => ({
resolveCommandSecretRefsViaGateway: (...args: unknown[]) =>
resolveCommandSecretRefsViaGatewayMock(...args),
}));
vi.mock("../agents/auth-profiles/session-override.js", () => ({
clearSessionAuthProfileOverride: (...args: unknown[]) =>
clearSessionAuthProfileOverrideMock(...args),
resolveSessionAuthProfileOverride: (...args: unknown[]) =>
resolveSessionAuthProfileOverrideMock(...args),
}));
vi.mock("../plugins/hook-runner-global.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/hook-runner-global.js")>();
return {
...actual,
getGlobalHookRunner: () => undefined,
initializeGlobalHookRunner: vi.fn(),
resetGlobalHookRunner: vi.fn(),
};
});
vi.mock("./reply/agent-runner.runtime.js", () => ({
runReplyAgent: (...args: unknown[]) => runReplyAgentMock(...args),
}));
vi.mock("./reply/get-reply-run.js", () => ({
runPreparedReply: (...args: unknown[]) => runPreparedReplyMock(...args),
}));