Files
gitmost/apps/server/src/integrations/ai/ai-provider-http.spec.ts
claude code agent 227 da15b55786 refactor(ai): address PR #176 review — finite-timeout wording, env doc, tests, permanent provider-http module
- Wording: every comment now says the stream timeouts are RAISED to a
  generous-but-finite ~15-min silence timeout, not "disabled (0)" (the stale
  comments contradicted the code, which uses AI_STREAM_TIMEOUT_MS, default
  900000ms).
- Architecture (the load-bearing-temporary trap): the streaming fetch reached
  the chat provider only by riding the "temporary DIAGNOSTIC" telemetry, so
  deleting the telemetry by its own label would silently revert the timeout fix.
  Legitimize it: rename ai-http-diagnostics.ts -> ai-provider-http.ts,
  createDiagnosticFetch -> createInstrumentedFetch, field aiDiagnosticFetch ->
  aiProviderFetch, drop the "temporary" labels, and document the chat transport
  (streaming fetch + instrumentation) as one intentional construct.
- Docs: AI_STREAM_TIMEOUT_MS added to .env.example next to AI_EMBEDDING_TIMEOUT_MS.
- Tests:
  - ai-provider-http.spec: createInstrumentedFetch delegates to the injected
    baseFetch with the same input/init, returns the Response untouched, rethrows
    the error, and defaults to global fetch — covering the baseFetch seam.
  - ai-streaming-fetch.spec: the delayed-server test is now LOAD-BEARING — with
    AI_STREAM_TIMEOUT_MS set below the 1.5s server delay the call actually rejects
    (a lost dispatcher -> global 300s default would NOT), proving the configured
    dispatcher is wired; plus the default-timeout happy path.

server tsc clean; ai-streaming-fetch / ai-provider-http / ai.service / mcp-servers
/ ai-error specs green (41).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:31:58 +03:00

41 lines
1.8 KiB
TypeScript

import { createInstrumentedFetch } from './ai-provider-http';
/**
* createInstrumentedFetch must be behavior-neutral: it delegates to the supplied
* baseFetch with the SAME input/init, returns the Response object untouched (so
* the streamed SSE body is never read/cloned), and rethrows the same error. The
* baseFetch injection is the seam that carries the streaming fetch (#175) onto
* the chat provider, so it is tested directly.
*/
describe('createInstrumentedFetch', () => {
it('delegates to the injected baseFetch with the same input/init', async () => {
const fakeResponse = new Response('ok', { status: 200 });
const baseFetch = jest.fn().mockResolvedValue(fakeResponse);
const instrumented = createInstrumentedFetch('test', baseFetch as never);
const init = { method: 'POST', body: '{"q":1}' };
const res = await instrumented('https://example.com/v1/chat', init);
expect(baseFetch).toHaveBeenCalledTimes(1);
expect(baseFetch).toHaveBeenCalledWith('https://example.com/v1/chat', init);
// The Response is returned UNTOUCHED (same reference — never read/cloned).
expect(res).toBe(fakeResponse);
});
it('rethrows the base fetch error unchanged (pre-response failure)', async () => {
const err = Object.assign(new TypeError('fetch failed'), {
cause: { code: 'ECONNRESET' },
});
const baseFetch = jest.fn().mockRejectedValue(err);
const instrumented = createInstrumentedFetch('test', baseFetch as never);
await expect(instrumented('https://example.com/')).rejects.toBe(err);
});
it('defaults to the global fetch when no baseFetch is given', () => {
// Constructing without a baseFetch must not throw — it simply wraps global
// fetch (the non-chat default).
expect(() => createInstrumentedFetch('test')).not.toThrow();
});
});