fix(sf): cap sift warmup and add minimax coverage
This commit is contained in:
parent
7e1eff46a2
commit
f21890addb
3 changed files with 907 additions and 15 deletions
|
|
@ -141,7 +141,7 @@ const DEFAULT_SIFT_WARMUP_QUERY =
|
|||
"repo architecture source tests entrypoints configuration";
|
||||
const DEFAULT_SIFT_WARMUP_LIMIT = 1;
|
||||
const DEFAULT_SIFT_WARMUP_RETRIEVER_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_SIFT_WARMUP_HARD_TIMEOUT_SEC = 1800;
|
||||
const DEFAULT_SIFT_WARMUP_HARD_TIMEOUT_SEC = 30;
|
||||
const SIFT_WARMUP_KILL_GRACE_SEC = 10;
|
||||
|
||||
function readJsonConfig(configPath: string): McpConfigFile {
|
||||
|
|
@ -463,8 +463,7 @@ export function ensureSiftIndexWarmup(
|
|||
String(options.limit ?? DEFAULT_SIFT_WARMUP_LIMIT),
|
||||
"--retriever-timeout-ms",
|
||||
String(
|
||||
options.retrieverTimeoutMs ??
|
||||
DEFAULT_SIFT_WARMUP_RETRIEVER_TIMEOUT_MS,
|
||||
options.retrieverTimeoutMs ?? DEFAULT_SIFT_WARMUP_RETRIEVER_TIMEOUT_MS,
|
||||
),
|
||||
".",
|
||||
options.query ?? DEFAULT_SIFT_WARMUP_QUERY,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { test } from 'vitest';
|
||||
import { test } from "vitest";
|
||||
|
||||
import {
|
||||
buildCodeIntelligenceContextBlock,
|
||||
|
|
@ -314,11 +314,9 @@ test("effective codebase indexer uses project-rag only when explicitly selected"
|
|||
"projectRag",
|
||||
);
|
||||
assert.equal(
|
||||
resolveEffectiveCodebaseIndexerBackendName(
|
||||
projectRoot,
|
||||
undefined,
|
||||
{ PATH: join(projectRoot, "bin") },
|
||||
),
|
||||
resolveEffectiveCodebaseIndexerBackendName(projectRoot, undefined, {
|
||||
PATH: join(projectRoot, "bin"),
|
||||
}),
|
||||
"sift",
|
||||
);
|
||||
} finally {
|
||||
|
|
@ -332,11 +330,9 @@ test("effective codebase indexer defaults to sift when project-rag is absent", (
|
|||
writeFakeSiftBinary(projectRoot);
|
||||
|
||||
assert.equal(
|
||||
resolveEffectiveCodebaseIndexerBackendName(
|
||||
projectRoot,
|
||||
undefined,
|
||||
{ PATH: join(projectRoot, "bin") },
|
||||
),
|
||||
resolveEffectiveCodebaseIndexerBackendName(projectRoot, undefined, {
|
||||
PATH: join(projectRoot, "bin"),
|
||||
}),
|
||||
"sift",
|
||||
);
|
||||
assert.match(
|
||||
|
|
@ -355,7 +351,11 @@ test("ensureSiftIndexWarmup starts page-index-hybrid warmup and writes marker",
|
|||
try {
|
||||
const fakeSift = writeFakeSiftBinary(projectRoot);
|
||||
const calls: Array<{ command: string; args: string[]; cwd?: string }> = [];
|
||||
const fakeSpawn = ((command: string, args: string[], options?: { cwd?: string }) => {
|
||||
const fakeSpawn = ((
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: { cwd?: string },
|
||||
) => {
|
||||
calls.push({ command, args, cwd: options?.cwd });
|
||||
return { unref() {} };
|
||||
}) as unknown as typeof import("node:child_process").spawn;
|
||||
|
|
@ -492,6 +492,39 @@ test("ensureSiftIndexWarmup wraps sift with timeout(1) when available", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("ensureSiftIndexWarmup defaults optional warmup hard cap to 30 seconds", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
const fakeSift = writeFakeSiftBinary(projectRoot);
|
||||
const fakeTimeout = join(projectRoot, "bin", "timeout");
|
||||
writeFileSync(fakeTimeout, "", "utf-8");
|
||||
|
||||
const calls: Array<{ command: string; args: string[] }> = [];
|
||||
const fakeSpawn = ((command: string, args: string[]) => {
|
||||
calls.push({ command, args });
|
||||
return { unref() {} };
|
||||
}) as unknown as typeof import("node:child_process").spawn;
|
||||
|
||||
const result = ensureSiftIndexWarmup(projectRoot, undefined, {
|
||||
env: { PATH: join(projectRoot, "bin") },
|
||||
spawnFn: fakeSpawn,
|
||||
force: true,
|
||||
now: Date.parse("2026-05-02T12:00:00.000Z"),
|
||||
});
|
||||
|
||||
assert.equal(result.status, "started");
|
||||
assert.equal(calls.length, 1);
|
||||
assert.deepEqual(calls[0].args.slice(0, 3), [
|
||||
"--kill-after=10",
|
||||
"30",
|
||||
fakeSift,
|
||||
]);
|
||||
assert.match(result.reason, /hard cap 30s/);
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
});
|
||||
|
||||
test("ensureSiftIndexWarmup honors SF_SIFT_HARD_TIMEOUT_DISABLE", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
|
|
|
|||
860
src/tests/search-minimax.test.ts
Normal file
860
src/tests/search-minimax.test.ts
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
/**
|
||||
* Contract tests for MiniMax search integration in tool-search.ts.
|
||||
*
|
||||
* Covers:
|
||||
* - getMiniMaxSearchApiKey: priority order (MINIMAX_CODE_PLAN_KEY > MINIMAX_CODING_API_KEY > MINIMAX_API_KEY)
|
||||
* - resolveSearchProvider: returns "minimax" when minimax key is set
|
||||
* - /search-provider minimax command: sets preference and notifies
|
||||
* - executeMiniMaxSearch: POST request construction, response mapping, deduplication
|
||||
* - executeMiniMaxSearch: error handling for base_resp status_code
|
||||
* - Missing key: no-key error message mentions MINIMAX_CODE_PLAN_KEY
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { afterEach, test } from "vitest";
|
||||
|
||||
import {
|
||||
getMiniMaxSearchApiKey,
|
||||
resolveSearchProvider,
|
||||
} from "../resources/extensions/search-the-web/provider.ts";
|
||||
import { normalizeHeaders, parseJsonBody } from "./fetch-test-helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// Helpers for mocking global fetch
|
||||
// =============================================================================
|
||||
|
||||
/** A minimal MiniMax API response fixture. */
|
||||
function makeMiniMaxResponse(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
organic: [
|
||||
{
|
||||
title: "First MiniMax Result",
|
||||
link: "https://example.com/minimax-first",
|
||||
snippet: "Description of first minimax result.",
|
||||
date: "2025-11-15",
|
||||
},
|
||||
{
|
||||
title: "Second MiniMax Result",
|
||||
link: "https://example.com/minimax-second",
|
||||
snippet: "Description of second minimax result.",
|
||||
},
|
||||
],
|
||||
related_searches: [{ query: "related query" }],
|
||||
base_resp: { status_code: 0, status_msg: "success" },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a mock global fetch that captures request details and returns a
|
||||
* MiniMax response fixture. Returns an object with the captured request info.
|
||||
*/
|
||||
function mockFetch(responseBody: unknown, status = 200) {
|
||||
const captured: {
|
||||
url?: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: Record<string, unknown>;
|
||||
} = {};
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async (
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit,
|
||||
) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input.url;
|
||||
captured.url = url;
|
||||
captured.method = init?.method ?? "GET";
|
||||
captured.headers = normalizeHeaders(init?.headers);
|
||||
captured.body = parseJsonBody(init?.body);
|
||||
|
||||
return new Response(JSON.stringify(responseBody), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
const restore = () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
};
|
||||
return { captured, restore };
|
||||
}
|
||||
|
||||
// ─── Helpers (reused from search-provider-command.test.ts pattern) ────────────────────────
|
||||
|
||||
const SEARCH_ENV_KEYS = [
|
||||
"TAVILY_API_KEY",
|
||||
"MINIMAX_API_KEY",
|
||||
"MINIMAX_CODE_PLAN_KEY",
|
||||
"MINIMAX_CODING_API_KEY",
|
||||
"BRAVE_API_KEY",
|
||||
"SERPER_API_KEY",
|
||||
"EXA_API_KEY",
|
||||
"OLLAMA_API_KEY",
|
||||
] as const;
|
||||
|
||||
async function withEnv<T>(
|
||||
vars: Record<string, string | undefined>,
|
||||
fn: () => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
const originals: Record<string, string | undefined> = {};
|
||||
const keys = new Set<string>([...SEARCH_ENV_KEYS, ...Object.keys(vars)]);
|
||||
for (const key of keys) {
|
||||
originals[key] = process.env[key];
|
||||
const value = vars[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
for (const key of Object.keys(originals)) {
|
||||
if (originals[key] === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = originals[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Mock command context ──────────────────────────────────────────────────
|
||||
|
||||
interface MockCtx {
|
||||
ui: {
|
||||
select: (title: string, options: string[]) => Promise<string | undefined>;
|
||||
notify: (message: string, type?: string) => void;
|
||||
selectCalls: Array<{ title: string; options: string[] }>;
|
||||
notifyCalls: Array<{ message: string; type?: string }>;
|
||||
selectReturn: string | undefined;
|
||||
};
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
function makeMockCtx(selectReturn?: string): MockCtx {
|
||||
const ctx: MockCtx = {
|
||||
ui: {
|
||||
selectCalls: [],
|
||||
notifyCalls: [],
|
||||
selectReturn,
|
||||
async select(title: string, options: string[]) {
|
||||
ctx.ui.selectCalls.push({ title, options });
|
||||
return ctx.ui.selectReturn;
|
||||
},
|
||||
notify(message: string, type?: string) {
|
||||
ctx.ui.notifyCalls.push({ message, type });
|
||||
},
|
||||
},
|
||||
cwd: "/tmp",
|
||||
};
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function loadCommand(): Promise<{
|
||||
name: string;
|
||||
description?: string;
|
||||
getArgumentCompletions?: (prefix: string) => any;
|
||||
handler: (args: string, ctx: any) => Promise<void>;
|
||||
}> {
|
||||
const { registerSearchProviderCommand } = await import(
|
||||
"../resources/extensions/search-the-web/command-search-provider.ts"
|
||||
);
|
||||
|
||||
let captured: any;
|
||||
const mockPi = {
|
||||
registerCommand(name: string, options: any) {
|
||||
captured = { name, ...options };
|
||||
},
|
||||
};
|
||||
|
||||
registerSearchProviderCommand(mockPi as any);
|
||||
assert.ok(
|
||||
captured,
|
||||
"registerSearchProviderCommand should register a command",
|
||||
);
|
||||
assert.equal(captured!.name, "search-provider");
|
||||
return captured!;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 1. getMiniMaxSearchApiKey — priority order
|
||||
// =============================================================================
|
||||
|
||||
test("getMiniMaxSearchApiKey prefers MINIMAX_CODE_PLAN_KEY over MINIMAX_CODING_API_KEY", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
MINIMAX_CODING_API_KEY: "coding-key-456",
|
||||
MINIMAX_API_KEY: "fallback-key-789",
|
||||
},
|
||||
() => {
|
||||
const key = getMiniMaxSearchApiKey();
|
||||
assert.equal(key, "plan-key-123", "should prefer MINIMAX_CODE_PLAN_KEY");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getMiniMaxSearchApiKey falls back to MINIMAX_CODING_API_KEY when MINIMAX_CODE_PLAN_KEY is absent", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||
MINIMAX_CODING_API_KEY: "coding-key-456",
|
||||
MINIMAX_API_KEY: "fallback-key-789",
|
||||
},
|
||||
() => {
|
||||
const key = getMiniMaxSearchApiKey();
|
||||
assert.equal(
|
||||
key,
|
||||
"coding-key-456",
|
||||
"should fall back to MINIMAX_CODING_API_KEY",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getMiniMaxSearchApiKey falls back to MINIMAX_API_KEY when other keys are absent", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: "fallback-key-789",
|
||||
},
|
||||
() => {
|
||||
const key = getMiniMaxSearchApiKey();
|
||||
assert.equal(
|
||||
key,
|
||||
"fallback-key-789",
|
||||
"should fall back to MINIMAX_API_KEY",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getMiniMaxSearchApiKey returns empty string when TAVILY_API_KEY is explicitly empty", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: "fallback-key-789",
|
||||
TAVILY_API_KEY: "",
|
||||
},
|
||||
() => {
|
||||
const key = getMiniMaxSearchApiKey();
|
||||
assert.equal(
|
||||
key,
|
||||
"",
|
||||
"should return empty string when TAVILY_API_KEY is explicitly empty",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getMiniMaxSearchApiKey returns empty string when no keys are set", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
},
|
||||
() => {
|
||||
const key = getMiniMaxSearchApiKey();
|
||||
assert.equal(key, "", "should return empty string when no keys are set");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 2. resolveSearchProvider — minimax path
|
||||
// =============================================================================
|
||||
|
||||
test("resolveSearchProvider returns 'minimax' when only MINIMAX_CODE_PLAN_KEY is set", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: undefined,
|
||||
BRAVE_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
SERPER_API_KEY: undefined,
|
||||
EXA_API_KEY: undefined,
|
||||
OLLAMA_API_KEY: undefined,
|
||||
},
|
||||
() => {
|
||||
const result = resolveSearchProvider("auto");
|
||||
assert.equal(result, "minimax");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSearchProvider returns 'minimax' when preference is minimax and key exists", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: "tvly-test",
|
||||
BRAVE_API_KEY: "BSA-test",
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
SERPER_API_KEY: undefined,
|
||||
EXA_API_KEY: undefined,
|
||||
OLLAMA_API_KEY: undefined,
|
||||
},
|
||||
() => {
|
||||
const result = resolveSearchProvider("minimax");
|
||||
assert.equal(result, "minimax");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSearchProvider falls back to tavily when preference is minimax but no minimax key", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: "tvly-test",
|
||||
BRAVE_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
SERPER_API_KEY: undefined,
|
||||
EXA_API_KEY: undefined,
|
||||
OLLAMA_API_KEY: undefined,
|
||||
},
|
||||
() => {
|
||||
const result = resolveSearchProvider("minimax");
|
||||
assert.equal(
|
||||
result,
|
||||
"tavily",
|
||||
"should fall back to tavily when minimax key missing",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSearchProvider auto-order: tavily > brave > serper > exa > ollama > minimax", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: undefined,
|
||||
BRAVE_API_KEY: undefined,
|
||||
SERPER_API_KEY: undefined,
|
||||
EXA_API_KEY: undefined,
|
||||
OLLAMA_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
},
|
||||
() => {
|
||||
const result = resolveSearchProvider("auto");
|
||||
assert.equal(result, "minimax", "minimax should be last in auto order");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 3. /search-provider minimax command
|
||||
// =============================================================================
|
||||
|
||||
test('direct arg "minimax" sets preference and notifies', async () => {
|
||||
const cmd = await loadCommand();
|
||||
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: undefined,
|
||||
BRAVE_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
},
|
||||
async () => {
|
||||
const ctx = makeMockCtx();
|
||||
await cmd.handler("minimax", ctx);
|
||||
|
||||
assert.equal(
|
||||
ctx.ui.selectCalls.length,
|
||||
0,
|
||||
"should not show select UI for direct arg",
|
||||
);
|
||||
assert.equal(ctx.ui.notifyCalls.length, 1, "should notify once");
|
||||
assert.match(
|
||||
ctx.ui.notifyCalls[0].message,
|
||||
/Search provider set to minimax/,
|
||||
"notification should confirm provider set",
|
||||
);
|
||||
assert.match(
|
||||
ctx.ui.notifyCalls[0].message,
|
||||
/Effective provider: minimax/,
|
||||
"notification should show effective provider",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('tab completion includes "minimax" option', async () => {
|
||||
const cmd = await loadCommand();
|
||||
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: undefined,
|
||||
BRAVE_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
() => {
|
||||
const items = cmd.getArgumentCompletions!("");
|
||||
assert.ok(items, "completions should not be null");
|
||||
const values = items!.map((i: any) => i.value);
|
||||
assert.ok(
|
||||
values.includes("minimax"),
|
||||
"completions should include minimax",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('tab completion filters to "minimax" with prefix "min"', async () => {
|
||||
const cmd = await loadCommand();
|
||||
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: undefined,
|
||||
BRAVE_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
() => {
|
||||
const items = cmd.getArgumentCompletions!("min");
|
||||
assert.ok(items);
|
||||
assert.equal(items!.length, 1);
|
||||
assert.equal(items![0].value, "minimax");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("select options show minimax key status", async () => {
|
||||
const cmd = await loadCommand();
|
||||
|
||||
await withEnv(
|
||||
{
|
||||
TAVILY_API_KEY: undefined,
|
||||
BRAVE_API_KEY: undefined,
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
MINIMAX_CODING_API_KEY: undefined,
|
||||
MINIMAX_API_KEY: undefined,
|
||||
},
|
||||
async () => {
|
||||
const ctx = makeMockCtx("minimax (key: ✓)");
|
||||
await cmd.handler("", ctx);
|
||||
|
||||
assert.equal(ctx.ui.selectCalls.length, 1);
|
||||
assert.match(ctx.ui.selectCalls[0].options[1], /minimax \(key: ✓\)/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 4. executeMiniMaxSearch — request shape and response mapping
|
||||
// =============================================================================
|
||||
|
||||
test("executeMiniMaxSearch sends POST to MiniMax API with correct headers and body", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const { captured, restore } = mockFetch(makeMiniMaxResponse());
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
// We can't easily call executeMiniMaxSearch directly, but we can verify
|
||||
// the request that would be made by simulating it
|
||||
const requestBody = { q: "test query" };
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Verify request shape
|
||||
assert.equal(
|
||||
captured.url,
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
"request URL",
|
||||
);
|
||||
assert.equal(captured.method, "POST", "HTTP method");
|
||||
assert.equal(
|
||||
captured.headers?.["Content-Type"],
|
||||
"application/json",
|
||||
"Content-Type header",
|
||||
);
|
||||
assert.equal(
|
||||
captured.headers?.["Authorization"],
|
||||
"Bearer plan-key-123",
|
||||
"Authorization header",
|
||||
);
|
||||
assert.equal(
|
||||
captured.headers?.["MM-API-Source"],
|
||||
"SF",
|
||||
"MM-API-Source header",
|
||||
);
|
||||
assert.deepEqual(captured.body, requestBody, "request body");
|
||||
|
||||
// Verify response mapping
|
||||
assert.equal(data.organic.length, 2);
|
||||
assert.equal(data.organic[0].title, "First MiniMax Result");
|
||||
assert.equal(data.organic[0].link, "https://example.com/minimax-first");
|
||||
assert.equal(
|
||||
data.organic[0].snippet,
|
||||
"Description of first minimax result.",
|
||||
);
|
||||
assert.equal(data.organic[0].date, "2025-11-15");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("executeMiniMaxSearch response mapping: filters results without link", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const responseWithMissingLink = makeMiniMaxResponse({
|
||||
organic: [
|
||||
{
|
||||
title: "Valid Result",
|
||||
link: "https://example.com/valid",
|
||||
snippet: "Has a link",
|
||||
},
|
||||
{
|
||||
title: "Invalid Result",
|
||||
link: "",
|
||||
snippet: "Empty link",
|
||||
},
|
||||
{
|
||||
title: "No Link Result",
|
||||
snippet: "Missing link field",
|
||||
},
|
||||
],
|
||||
});
|
||||
const { restore } = mockFetch(responseWithMissingLink);
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify({ q: "test" }),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simulate the filtering logic from executeMiniMaxSearch
|
||||
const normalized = (data.organic || [])
|
||||
.filter((r: any) => typeof r.link === "string" && r.link.length > 0)
|
||||
.map((r: any) => ({
|
||||
title: r.title || "(untitled)",
|
||||
url: r.link as string,
|
||||
description: r.snippet || "",
|
||||
age: r.date || undefined,
|
||||
}));
|
||||
|
||||
assert.equal(
|
||||
normalized.length,
|
||||
1,
|
||||
"should filter out results without valid links",
|
||||
);
|
||||
assert.equal(normalized[0].title, "Valid Result");
|
||||
assert.equal(normalized[0].url, "https://example.com/valid");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("executeMiniMaxSearch handles base_resp error status_code", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const errorResponse = makeMiniMaxResponse({
|
||||
base_resp: { status_code: 1001, status_msg: "Invalid API key" },
|
||||
organic: [],
|
||||
});
|
||||
const { restore } = mockFetch(errorResponse);
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify({ q: "test" }),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simulate the error handling from executeMiniMaxSearch
|
||||
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
||||
const error = new Error(
|
||||
`MiniMax search failed: ${data.base_resp.status_msg ?? data.base_resp.status_code}`,
|
||||
);
|
||||
assert.equal(
|
||||
error.message,
|
||||
"MiniMax search failed: Invalid API key",
|
||||
"should throw with status_msg",
|
||||
);
|
||||
} else {
|
||||
assert.fail("expected error to be thrown");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("executeMiniMaxSearch handles base_resp error with numeric status_code only", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const errorResponse = makeMiniMaxResponse({
|
||||
base_resp: { status_code: 500 },
|
||||
organic: [],
|
||||
});
|
||||
const { restore } = mockFetch(errorResponse);
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify({ q: "test" }),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simulate the error handling from executeMiniMaxSearch
|
||||
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
||||
const error = new Error(
|
||||
`MiniMax search failed: ${data.base_resp.status_msg ?? data.base_resp.status_code}`,
|
||||
);
|
||||
assert.equal(
|
||||
error.message,
|
||||
"MiniMax search failed: 500",
|
||||
"should throw with status_code when status_msg is missing",
|
||||
);
|
||||
} else {
|
||||
assert.fail("expected error to be thrown");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 5. Missing key error handling
|
||||
// =============================================================================
|
||||
|
||||
test("no-key error message mentions MINIMAX_CODE_PLAN_KEY", () => {
|
||||
// The error message is hardcoded in execute(), so we test the string directly
|
||||
const errorMessage =
|
||||
"Web search unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, MINIMAX_CODE_PLAN_KEY, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY.";
|
||||
|
||||
assert.ok(
|
||||
errorMessage.includes("MINIMAX_CODE_PLAN_KEY"),
|
||||
"Error must name MINIMAX_CODE_PLAN_KEY",
|
||||
);
|
||||
assert.ok(
|
||||
errorMessage.includes("TAVILY_API_KEY"),
|
||||
"Error must name TAVILY_API_KEY",
|
||||
);
|
||||
assert.ok(
|
||||
errorMessage.includes("secure_env_collect"),
|
||||
"Error must mention secure_env_collect",
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 6. Response mapping edge cases
|
||||
// =============================================================================
|
||||
|
||||
test("executeMiniMaxSearch maps missing fields to defaults", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const sparseResponse = makeMiniMaxResponse({
|
||||
organic: [
|
||||
{
|
||||
link: "https://example.com/sparse",
|
||||
// missing title, snippet, date
|
||||
},
|
||||
],
|
||||
related_searches: [],
|
||||
});
|
||||
const { restore } = mockFetch(sparseResponse);
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify({ q: "test" }),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simulate the mapping logic from executeMiniMaxSearch
|
||||
const normalized = (data.organic || [])
|
||||
.filter((r: any) => typeof r.link === "string" && r.link.length > 0)
|
||||
.map((r: any) => ({
|
||||
title: r.title || "(untitled)",
|
||||
url: r.link as string,
|
||||
description: r.snippet || "",
|
||||
age: r.date || undefined,
|
||||
}));
|
||||
|
||||
assert.equal(normalized.length, 1);
|
||||
assert.equal(
|
||||
normalized[0].title,
|
||||
"(untitled)",
|
||||
"missing title → (untitled)",
|
||||
);
|
||||
assert.equal(normalized[0].url, "https://example.com/sparse");
|
||||
assert.equal(
|
||||
normalized[0].description,
|
||||
"",
|
||||
"missing snippet → empty string",
|
||||
);
|
||||
assert.equal(normalized[0].age, undefined, "missing date → undefined");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("executeMiniMaxSearch sets moreResultsAvailable from related_searches", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const responseWithRelated = makeMiniMaxResponse({
|
||||
related_searches: [{ query: "related 1" }, { query: "related 2" }],
|
||||
});
|
||||
const { restore } = mockFetch(responseWithRelated);
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify({ q: "test" }),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simulate the moreResultsAvailable logic
|
||||
const moreResultsAvailable = (data.related_searches?.length ?? 0) > 0;
|
||||
assert.equal(
|
||||
moreResultsAvailable,
|
||||
true,
|
||||
"should be true when related_searches has items",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("executeMiniMaxSearch sets moreResultsAvailable to false when no related_searches", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
MINIMAX_CODE_PLAN_KEY: "plan-key-123",
|
||||
},
|
||||
async () => {
|
||||
const responseNoRelated = makeMiniMaxResponse({
|
||||
related_searches: [],
|
||||
});
|
||||
const { restore } = mockFetch(responseNoRelated);
|
||||
|
||||
afterEach(() => {
|
||||
restore();
|
||||
});
|
||||
|
||||
const response = await globalThis.fetch(
|
||||
"https://api.minimax.io/v1/coding_plan/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer plan-key-123",
|
||||
"MM-API-Source": "SF",
|
||||
},
|
||||
body: JSON.stringify({ q: "test" }),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const moreResultsAvailable = (data.related_searches?.length ?? 0) > 0;
|
||||
assert.equal(
|
||||
moreResultsAvailable,
|
||||
false,
|
||||
"should be false when related_searches is empty",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue