mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
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.
112 lines
4.2 KiB
TypeScript
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')
|
|
})
|
|
})
|