Files
zeekay 40d120dbf7 refactor(api): say /v1/ in the Python callers
Every first-party Python caller and test now names the mount the API actually
answers on. Mechanical: URL literals only, and only where the /api/ was ours --
a preceding word character means it was a module path like insights/api/ or a
third-party host like slack.com/api/, and those are untouched.

Left saying /api/, each for a reason rather than for safety:
- products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints
- services/llm-gateway deploys separately and is under a code freeze, so moving
  its two calls would break it during the skew
- the mcp_store and Slack/Teams redirect_uris are registered with third parties
  and must byte-match what was registered
- the Wizard and Raycast client-metadata paths are OAuth client_ids
- /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the
  ingress routes to the Rust fleets, held by released SDK versions

Test URLconfs moved with the requests that call them, and the Datastore
user-selection fixtures now carry the leading slash that request.path has.
2026-08-08 18:38:44 -07:00

92 lines
4.1 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { strToU8, zipSync } from 'fflate'
import { http, HttpResponse, type JsonBodyType, type RequestHandler, type StrictRequest } from 'msw'
const FIXTURES_DIR = dirname(fileURLToPath(import.meta.url))
const fixture = (name: string): JsonBodyType =>
JSON.parse(readFileSync(resolve(FIXTURES_DIR, name), 'utf-8')) as JsonBodyType
// Real Insights API responses captured from a freshly-seeded local dev server
// (`./bin/start`) and sanitized — emails, distinct ids, project tokens, and
// org/team uuids replaced with stable test values.
const usersMe = fixture('api_users__me.json')
const personalApiKeyCurrent = fixture('api_personal_api_keys__current.json')
const project = fixture('api_projects_id.json')
const organization = fixture('api_organizations_id.json')
const groupTypes = fixture('api_projects_id_groups_types.json')
// `MCP.detectRegion` calls users/@me on both us.hanzo.ai and eu.hanzo.ai
// in parallel. Match any host with a leading `*` wildcard.
export const handlers: RequestHandler[] = [
// OAuth introspection. MCP only falls back here when /personal_api_keys/@current
// returns 401/403 — local dev rejects introspect for personal keys, so we
// synthesize a healthy session-token response instead of capturing one.
http.post('*/oauth/introspect', () =>
HttpResponse.json({
active: true,
scope: '',
scoped_teams: [],
scoped_organizations: [],
})
),
http.get('*/v1/users/@me', () => HttpResponse.json(usersMe)),
http.get('*/v1/users/@me/', () => HttpResponse.json(usersMe)),
http.get('*/v1/personal_api_keys/@current', () => HttpResponse.json(personalApiKeyCurrent)),
http.get('*/v1/personal_api_keys/@current/', () => HttpResponse.json(personalApiKeyCurrent)),
http.get('*/v1/organizations/:orgId', () => HttpResponse.json(organization)),
http.get('*/v1/organizations/:orgId/', () => HttpResponse.json(organization)),
http.get('*/v1/projects/:projectId/groups_types', () => HttpResponse.json(groupTypes)),
http.get('*/v1/projects/:projectId/groups_types/', () => HttpResponse.json(groupTypes)),
http.get('*/v1/projects/:projectId', () => HttpResponse.json(project)),
http.get('*/v1/projects/:projectId/', () => HttpResponse.json(project)),
]
const contextMillManifest = {
version: '1.0.0',
resources: [
{
id: 'test-guide',
name: 'Insights Getting Started',
uri: 'insights://guide/getting-started',
resource: {
mimeType: 'text/plain',
description: 'A guide to getting started with Insights',
text: 'Welcome to Insights. This is a test resource.',
},
},
],
}
const contextMillZip = zipSync({
'manifest.json': strToU8(JSON.stringify(contextMillManifest)),
})
export const contextMillHandler = http.get(
'https://github.com/Insights/context-mill/releases/latest/download/skills-mcp-resources.zip',
() => new HttpResponse(contextMillZip, { headers: { 'Content-Type': 'application/zip' } })
)
/**
* Dispatch a workerd outbound request to the matching MSW handler. Mirrors
* MSW's internal handler-resolution loop without relying on global fetch
* interception (which doesn't work inside the workerd runtime). The first
* handler whose `run()` returns a response wins; everything else 404s.
*/
export async function dispatchHandlers(request: Request): Promise<Response> {
// workerd's Request is structurally compatible with MSW's StrictRequest;
// the cast just appeases the cf-properties shape mismatch.
const msReq = request.clone() as unknown as StrictRequest<JsonBodyType>
for (const handler of handlers) {
// `run` is the same entrypoint MSW's setupServer uses internally.
const result = await handler.run({ request: msReq, requestId: crypto.randomUUID() })
if (result?.response) {
return result.response
}
}
return new Response(null, { status: 404 })
}