Files
mcp/test/tools/fetch-search.test.ts
zeekay c724ae360a Let the tests run again
Every suite had stopped executing. ts-jest peers on `typescript >=4.3 <7`
and this repo builds against 7, so all twenty files died inside ConfigSet
with "Cannot read properties of undefined (reading 'fileExists')" — twenty
failures, zero tests, and nothing to read them by because the push gate
deliberately runs typecheck and build rather than jest.

swc transforms instead: it strips types rather than asking the compiler
to, so it has no TypeScript version to disagree with, and `tsc --noEmit`
already answers the question ts-jest was answering twice.

Running as real ESM then exposed what the CommonJS fallback had hidden.
jest.mock does not hoist there, so a static import binds the real module
and the suite fails on `mockReturnValue is not a function` — an absent
mock reported as a broken one. The search, autogui and ui suites now
register through unstable_mockModule and import after it. One of them had
never mocked fs at all: it cast fs.promises to a mocked type and called
mockImplementation on the real module.

384 tests, twenty suites, all passing.
2026-09-01 09:42:54 -07:00

109 lines
4.1 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { fetchTool } from '../../src/tools/unified/fetch.js';
/**
* The regression these pin is BLINDNESS RENDERED AS EMPTINESS.
*
* `fetch:search` used to scrape html.duckduckgo.com and pull results out with a
* regex. Measured 2026-08-20 that page answers HTTP 202 (an anti-bot challenge)
* and carries no `result__title`, so the regex matched nothing and the tool
* reported `{results: [], count: 0}` — the same answer it gives for a query the
* web genuinely has nothing for. A caller could not tell the two apart, which is
* the whole reason it went unnoticed.
*
* So the assertions are about the SHAPE OF A FAILURE, not about search quality:
* when we cannot search, the tool must say so.
*/
function body(res: any) {
return JSON.parse(res.content[0].text);
}
const KEYS = ['HANZO_API_KEY', 'API_KEY', 'API_TOKEN', 'HANZO_TOKEN', 'API_URL'];
let saved: Record<string, string | undefined>;
beforeEach(() => {
saved = Object.fromEntries(KEYS.map((k) => [k, process.env[k]]));
for (const k of KEYS) delete process.env[k];
});
afterEach(() => {
for (const k of KEYS) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
});
describe('fetch:search', () => {
it('refuses loudly with no credential, instead of reporting zero results', async () => {
const res: any = await fetchTool.handler({ action: 'search', query: 'anything' });
expect(res.isError).toBe(true);
const b = body(res);
expect(b.ok).toBe(false);
expect(b.error.code).toBe('NO_CREDENTIAL');
// The failure must never be mistakable for a successful empty search.
expect(b.data).toBeNull();
});
it('still requires a query', async () => {
process.env.HANZO_API_KEY = 'sk-test';
const res: any = await fetchTool.handler({ action: 'search' });
expect(res.isError).toBe(true);
expect(body(res).error.code).toBe('INVALID_PARAMS');
});
it("passes cloud's own refusal through verbatim rather than flattening it", async () => {
process.env.HANZO_API_KEY = 'sk-test';
process.env.API_URL = 'https://api.example.invalid';
const spy = jest.spyOn(globalThis, 'fetch' as any).mockResolvedValue({
ok: false,
status: 401,
text: async () => '{"status":401,"error":"sign in to search the web"}',
} as any);
try {
const res: any = await fetchTool.handler({ action: 'search', query: 'q' });
expect(res.isError).toBe(true);
const b = body(res);
expect(b.error.code).toBe('SEARCH_FAILED');
expect(b.error.message).toContain('401');
expect(b.error.message).toContain('sign in to search the web');
} finally {
spy.mockRestore();
}
});
it('asks OUR api, and maps its envelope', async () => {
process.env.HANZO_API_KEY = 'sk-test';
process.env.API_URL = 'https://api.example.invalid';
const seen: any = {};
const spy = jest.spyOn(globalThis, 'fetch' as any).mockImplementation(async (url: any, init: any) => {
seen.url = String(url);
seen.auth = init?.headers?.Authorization;
seen.body = JSON.parse(init.body);
return {
ok: true,
status: 200,
text: async () =>
JSON.stringify({
query: 'q',
number_of_results: 2,
engines: ['bing', 'mojeek'],
results: [{ url: 'https://a.example', title: 'A', content: 'snippet a' }],
}),
} as any;
});
try {
const res: any = await fetchTool.handler({ action: 'search', query: 'q' });
expect(seen.url).toBe('https://api.example.invalid/v1/websearch');
expect(seen.auth).toBe('Bearer sk-test');
expect(seen.body).toEqual({ q: 'q' });
const b = body(res);
expect(b.ok).toBe(true);
expect(b.data.results).toEqual([{ url: 'https://a.example', title: 'A', snippet: 'snippet a' }]);
// Which engines answered: an empty result WITH engines is a real empty,
// one with none is a search that never happened.
expect(b.data.engines).toEqual(['bing', 'mojeek']);
} finally {
spy.mockRestore();
}
});
});