diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.test.ts index 8316346a8..a49aefa46 100644 --- a/src/agents/memory-search.test.ts +++ b/src/agents/memory-search.test.ts @@ -5,7 +5,9 @@ import { resolveMemorySearchConfig } from "./memory-search.js"; const asConfig = (cfg: OpenClawConfig): OpenClawConfig => cfg; describe("memory search config", () => { - function configWithDefaultProvider(provider: "openai" | "local" | "gemini"): OpenClawConfig { + function configWithDefaultProvider( + provider: "openai" | "local" | "gemini" | "mistral", + ): OpenClawConfig { return asConfig({ agents: { defaults: { @@ -147,6 +149,13 @@ describe("memory search config", () => { expectDefaultRemoteBatch(resolved); }); + it("includes remote defaults and model default for mistral without overrides", () => { + const cfg = configWithDefaultProvider("mistral"); + const resolved = resolveMemorySearchConfig(cfg, "main"); + expectDefaultRemoteBatch(resolved); + expect(resolved?.model).toBe("mistral-embed"); + }); + it("defaults session delta thresholds", () => { const cfg = asConfig({ agents: { diff --git a/src/cli/program/register.onboard.test.ts b/src/cli/program/register.onboard.test.ts index 9ea7e87b2..89d6e2433 100644 --- a/src/cli/program/register.onboard.test.ts +++ b/src/cli/program/register.onboard.test.ts @@ -14,7 +14,12 @@ vi.mock("../../commands/auth-choice-options.js", () => ({ })); vi.mock("../../commands/onboard-provider-auth-flags.js", () => ({ - ONBOARD_PROVIDER_AUTH_FLAGS: [] as Array<{ cliOption: string; description: string }>, + ONBOARD_PROVIDER_AUTH_FLAGS: [ + { + cliOption: "--mistral-api-key ", + description: "Mistral API key", + }, + ] as Array<{ cliOption: string; description: string }>, })); vi.mock("../../commands/onboard.js", () => ({ @@ -103,6 +108,16 @@ describe("registerOnboardCommand", () => { ); }); + it("parses --mistral-api-key and forwards mistralApiKey", async () => { + await runCli(["onboard", "--mistral-api-key", "sk-mistral-test"]); + expect(onboardCommandMock).toHaveBeenCalledWith( + expect.objectContaining({ + mistralApiKey: "sk-mistral-test", + }), + runtime, + ); + }); + it("reports errors via runtime on onboard command failures", async () => { onboardCommandMock.mockRejectedValueOnce(new Error("onboard failed")); diff --git a/src/gateway/live-tool-probe-utils.test.ts b/src/gateway/live-tool-probe-utils.test.ts index 623f9d08e..ff2468ece 100644 --- a/src/gateway/live-tool-probe-utils.test.ts +++ b/src/gateway/live-tool-probe-utils.test.ts @@ -45,4 +45,56 @@ describe("live tool probe utils", () => { }), ).toBe(false); }); + + it("retries when tool output is empty and attempts remain", () => { + expect( + shouldRetryToolReadProbe({ + text: " ", + nonceA: "nonce-a", + nonceB: "nonce-b", + provider: "openai", + attempt: 0, + maxAttempts: 3, + }), + ).toBe(true); + }); + + it("retries when output still looks like tool/function scaffolding", () => { + expect( + shouldRetryToolReadProbe({ + text: "Use tool function read[] now.", + nonceA: "nonce-a", + nonceB: "nonce-b", + provider: "openai", + attempt: 0, + maxAttempts: 3, + }), + ).toBe(true); + }); + + it("retries mistral nonce marker echoes without parsed nonce values", () => { + expect( + shouldRetryToolReadProbe({ + text: "nonceA= nonceB=", + nonceA: "nonce-a", + nonceB: "nonce-b", + provider: "mistral", + attempt: 0, + maxAttempts: 3, + }), + ).toBe(true); + }); + + it("does not retry nonce marker echoes for non-mistral providers", () => { + expect( + shouldRetryToolReadProbe({ + text: "nonceA= nonceB=", + nonceA: "nonce-a", + nonceB: "nonce-b", + provider: "openai", + attempt: 0, + maxAttempts: 3, + }), + ).toBe(false); + }); }); diff --git a/src/media-understanding/runner.auto-audio.test.ts b/src/media-understanding/runner.auto-audio.test.ts index 6992f1b79..975f1438b 100644 --- a/src/media-understanding/runner.auto-audio.test.ts +++ b/src/media-understanding/runner.auto-audio.test.ts @@ -109,47 +109,69 @@ describe("runCapability auto audio entries", () => { }); it("uses mistral when only mistral key is configured", async () => { + const priorEnv: Record = { + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + GROQ_API_KEY: process.env.GROQ_API_KEY, + DEEPGRAM_API_KEY: process.env.DEEPGRAM_API_KEY, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, + MISTRAL_API_KEY: process.env.MISTRAL_API_KEY, + }; + delete process.env.OPENAI_API_KEY; + delete process.env.GROQ_API_KEY; + delete process.env.DEEPGRAM_API_KEY; + delete process.env.GEMINI_API_KEY; + process.env.MISTRAL_API_KEY = "mistral-test-key"; let runResult: Awaited> | undefined; - await withAudioFixture("openclaw-auto-audio-mistral", async ({ ctx, media, cache }) => { - const providerRegistry = buildProviderRegistry({ - openai: { - id: "openai", - capabilities: ["audio"], - transcribeAudio: async () => ({ text: "openai", model: "gpt-4o-mini-transcribe" }), - }, - mistral: { - id: "mistral", - capabilities: ["audio"], - transcribeAudio: async (req) => ({ text: "mistral", model: req.model ?? "unknown" }), - }, - }); - const cfg = { - models: { - providers: { - mistral: { - apiKey: "mistral-test-key", - models: [], + try { + await withAudioFixture("openclaw-auto-audio-mistral", async ({ ctx, media, cache }) => { + const providerRegistry = buildProviderRegistry({ + openai: { + id: "openai", + capabilities: ["audio"], + transcribeAudio: async () => ({ text: "openai", model: "gpt-4o-mini-transcribe" }), + }, + mistral: { + id: "mistral", + capabilities: ["audio"], + transcribeAudio: async (req) => ({ text: "mistral", model: req.model ?? "unknown" }), + }, + }); + const cfg = { + models: { + providers: { + mistral: { + apiKey: "mistral-test-key", + models: [], + }, }, }, - }, - tools: { - media: { - audio: { - enabled: true, + tools: { + media: { + audio: { + enabled: true, + }, }, }, - }, - } as unknown as OpenClawConfig; + } as unknown as OpenClawConfig; - runResult = await runCapability({ - capability: "audio", - cfg, - ctx, - attachments: cache, - media, - providerRegistry, + runResult = await runCapability({ + capability: "audio", + cfg, + ctx, + attachments: cache, + media, + providerRegistry, + }); }); - }); + } finally { + for (const [key, value] of Object.entries(priorEnv)) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + } if (!runResult) { throw new Error("Expected auto audio mistral result"); } diff --git a/src/memory/embeddings-mistral.test.ts b/src/memory/embeddings-mistral.test.ts new file mode 100644 index 000000000..7826cd354 --- /dev/null +++ b/src/memory/embeddings-mistral.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_MISTRAL_EMBEDDING_MODEL, normalizeMistralModel } from "./embeddings-mistral.js"; + +describe("normalizeMistralModel", () => { + it("returns the default model for empty values", () => { + expect(normalizeMistralModel("")).toBe(DEFAULT_MISTRAL_EMBEDDING_MODEL); + expect(normalizeMistralModel(" ")).toBe(DEFAULT_MISTRAL_EMBEDDING_MODEL); + }); + + it("strips the mistral/ prefix", () => { + expect(normalizeMistralModel("mistral/mistral-embed")).toBe("mistral-embed"); + expect(normalizeMistralModel(" mistral/custom-embed ")).toBe("custom-embed"); + }); + + it("keeps explicit non-prefixed models", () => { + expect(normalizeMistralModel("mistral-embed")).toBe("mistral-embed"); + expect(normalizeMistralModel("custom-embed-v2")).toBe("custom-embed-v2"); + }); +});