Files
insights/services/mcp/tests/unit/tool-error-hints.test.ts
zeekay 1c7d419444 refactor(api): finish the caller move, and stop tracking build output
Frontend, plugin server, MCP service, Stripe app, CLI, Playwright and the CI
scripts now say /v1/. Three of these were invisible to a plain search for the
literal, and each would have silently stopped matching:

- api.ts PROJECT_ID_REGEX, which guards against null/undefined project ids
- isExternalLink's scheme test in lib/utils/url.ts
- the exporter's interview-token redaction

Two more that a naive sweep would have missed: our own cloud hosts precede the
path in test assertions and pagination fixtures (us.hanzo.ai/api/…,
testserver/api/…), and stripe-app.json's CSP connect-src had to move or the
deployed app's /v1/ fetches would be blocked by the browser.

Also stops tracking the compiled InsightsQL parser. Building it locally is what
makes the URLconf boot at all, and an earlier commit of mine swept the output
into git along with this worktree's absolute path; .gitignore now keeps it out.

Still at /api/, named rather than assumed: the plugin server's own Express
routes (the ingress splits on that prefix and lives in another repo), the SDK
aliases the ingress routes to the Rust fleets, the cross-region EU->US spend
redirect, and the MCP tool tests that assert paths from generated handlers --
those move with the regeneration, not before it.
2026-08-08 18:58:46 -07:00

112 lines
4.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { handleToolError, InsightsApiError, InsightsValidationError, wrapError } from '@/lib/errors'
import { getToolRecoveryHint } from '@/lib/tool-error-hints'
vi.mock('@/lib/insights', () => ({
getInsightsClient: () => ({ captureException: vi.fn() }),
}))
const LOGS_QUERY_URL = 'https://us.hanzo.ai/v1/projects/2/logs/query/'
describe('getToolRecoveryHint', () => {
it('returns the narrow-and-retry hint for a 5xx on a logs query endpoint', () => {
const hint = getToolRecoveryHint({ url: LOGS_QUERY_URL, status: 500 })
expect(hint).not.toBeUndefined()
expect(hint).toContain('scans too much data')
expect(hint).toContain('logs-count')
expect(hint).toContain('logs-count-ranges')
expect(hint).toContain('serviceNames')
})
it.each([
'https://us.hanzo.ai/v1/projects/2/logs/count/',
'https://us.hanzo.ai/v1/projects/2/logs/count-ranges/',
'https://us.hanzo.ai/v1/projects/2/logs/services/',
'https://us.hanzo.ai/v1/projects/2/logs/sparkline/',
])('also fires for the sibling logs query endpoint %s', (url: string) => {
expect(getToolRecoveryHint({ url, status: 503 })).not.toBeUndefined()
})
it.each([400, 404])('does not fire for %i — those carry an actionable detail already', (status) => {
expect(getToolRecoveryHint({ url: LOGS_QUERY_URL, status })).toBeUndefined()
})
it('does not fire for unrelated endpoints', () => {
expect(
getToolRecoveryHint({ url: 'https://us.hanzo.ai/v1/projects/2/insights/', status: 500 })
).toBeUndefined()
})
it('fires when status is unknown but the URL is a logs query endpoint', () => {
expect(getToolRecoveryHint({ url: LOGS_QUERY_URL })).not.toBeUndefined()
})
})
describe('handleToolError recovery hints', () => {
it('appends the recovery hint to a 5xx logs query failure', () => {
const error = new InsightsApiError({
status: 500,
statusText: 'Internal Server Error',
body: '{"detail":"timeout"}',
url: LOGS_QUERY_URL,
method: 'POST',
})
const result = handleToolError(error, 'query-logs')
const [content] = result.content as Array<{ type: string; text: string }>
expect(content?.text).toContain('[query-logs]')
expect(content?.text).toContain('Status Code: 500')
expect(content?.text).toContain('Narrow the query and retry')
})
it('appends the hint even when the typed error is hidden behind Error.cause', () => {
const original = new InsightsApiError({
status: 503,
statusText: 'Service Unavailable',
body: 'upstream timeout',
url: LOGS_QUERY_URL,
method: 'POST',
})
const wrapped = wrapError('Failed to query logs', original)
const result = handleToolError(wrapped, 'query-logs')
const [content] = result.content as Array<{ type: string; text: string }>
expect(content?.text).toContain('logs-count-ranges')
})
it('does not append a hint for a 4xx (no noise on recoverable input errors)', () => {
const error = new InsightsValidationError({
detail: 'invalid filter',
attr: 'filterGroup',
code: 'invalid',
extra: undefined,
url: LOGS_QUERY_URL,
method: 'POST',
})
const result = handleToolError(error, 'query-logs')
const [content] = result.content as Array<{ type: string; text: string }>
expect(content?.text).not.toContain('Narrow the query and retry')
})
it('does not append a hint for a 5xx on an unrelated tool', () => {
const error = new InsightsApiError({
status: 500,
statusText: 'Internal Server Error',
body: 'boom',
url: 'https://us.hanzo.ai/v1/projects/2/insights/',
method: 'GET',
})
const result = handleToolError(error, 'insights-list')
const [content] = result.content as Array<{ type: string; text: string }>
expect(content?.text).not.toContain('Narrow the query and retry')
})
})