Files
insights/services/mcp/tests/unit/build-tool-result.test.ts
zeekay 159e825022 merge: take upstream master fa1a9a60 onto the converged tree
The five days of upstream evolution since the 2026-08-01 import land through
bin/debrand: new query schema, billing counters, ducklake compaction, the
desktop sandbox default, capture evolution. Ours' invariants hold everywhere
they met it: IAM-native auth only, no ee, no upsell surfaces, one sessions
model, org-is-the-tenant, v1-first serving, fail-closed capture. Crates and
workflows the fork retired stay retired; the Lemon prefix finishes dissolving
into elements.
2026-08-06 05:33:45 -07:00

442 lines
18 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
EXEC_BUILT_PAYLOAD,
STRUCTURED_CONTENT_ONLY_TEXT,
estimateResponseTokens,
markExecPayload,
buildToolResultPayload,
isToolCallPayload,
} from '@/lib/build-tool-result'
import { estimateTokens } from '@/lib/estimate-tokens'
import { INSIGHTS_FORMATTED_RESULTS_OVERRIDE_KEY, INSIGHTS_META_KEY } from '@/tools/types'
import { APP_DATA_META_KEY } from '@/ui-apps/types'
// Simulates a `query-trends` handler return value: a UI-resource tool that
// carries both the raw `results` object and a pre-formatted pipe-delimited table
// surfaced via the override key.
const FORMATTED_TABLE = [
'Date|$pageview',
'2026-04-14|0',
'2026-04-15|0',
'2026-04-16|25',
'2026-04-17|0',
'2026-04-18|0',
'2026-04-19|0',
'2026-04-20|3',
'2026-04-21|0',
].join('\n')
function queryTrendsHandlerResult(withFormatted = true): Record<string, unknown> {
return {
results: [
{
data: [0, 0, 25, 0, 0, 0, 3, 0],
labels: [
'14-Apr-2026',
'15-Apr-2026',
'16-Apr-2026',
'17-Apr-2026',
'18-Apr-2026',
'19-Apr-2026',
'20-Apr-2026',
'21-Apr-2026',
],
count: 28,
label: '$pageview',
},
],
_insightsUrl: 'http://localhost:8010/project/1/insights/new#q=%7B...',
...(withFormatted ? { [INSIGHTS_FORMATTED_RESULTS_OVERRIDE_KEY]: FORMATTED_TABLE } : {}),
}
}
// Same `_meta` shape that `createQueryWrapper` produces for a UI-resource query tool.
const queryTrendsToolMeta = {
ui: { resourceUri: 'ui://insights/query-results.html' },
} as const
describe('buildToolResultPayload — query-trends for Claude Code', () => {
it('returns formatted table as text AND suppresses structuredContent for claude-code', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: { series: [{ event: '$pageview', kind: 'EventsNode' }] },
suppressStructuredContentForFormattedResults: true,
distinctId: 'test-distinct-id',
})
// The model should see the formatted table — not a JSON dump.
expect(payload.content).toEqual([{ type: 'text', text: FORMATTED_TABLE }])
// No structuredContent: Claude Code would otherwise prefer it over text,
// defeating the purpose of the formatted_results override.
expect(payload).not.toHaveProperty('structuredContent')
})
it('keeps structuredContent when suppression is false', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
suppressStructuredContentForFormattedResults: false,
distinctId: 'test-distinct-id',
})
expect(payload.content[0]!.text).toBe(FORMATTED_TABLE)
expect(payload.structuredContent).toMatchObject({
results: expect.any(Array),
_insightsUrl: expect.any(String),
_analytics: { distinctId: 'test-distinct-id', toolName: 'query-trends' },
})
// Override key must not leak into structuredContent.
expect(payload.structuredContent).not.toHaveProperty(INSIGHTS_FORMATTED_RESULTS_OVERRIDE_KEY)
})
it('keeps structuredContent when suppression is omitted', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
distinctId: 'd',
})
expect(payload.content[0]!.text).toBe(FORMATTED_TABLE)
expect(payload.structuredContent).not.toBeUndefined()
})
it('keeps structuredContent when caller explicitly passes output_format=json (even on claude-code)', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: { output_format: 'json' },
suppressStructuredContentForFormattedResults: true,
distinctId: 'd',
})
// Text still carries the formatted override (the override wins unconditionally)...
expect(payload.content[0]!.text).toBe(FORMATTED_TABLE)
// ...but structuredContent is no longer suppressed — the caller opted into JSON.
expect(payload.structuredContent).not.toBeUndefined()
expect(payload.structuredContent).toMatchObject({
results: expect.any(Array),
_insightsUrl: expect.any(String),
})
})
it('does NOT suppress structuredContent when there is no formatted_results override', () => {
// If the backend didn't return formatted_results (unsupported query type, EE unavailable,
// etc.), the wrapper has nothing to put in text besides the TOON-encoded raw object.
// In that case we must keep structuredContent so Claude Code at least sees the data.
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(/* withFormatted */ false),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
suppressStructuredContentForFormattedResults: true,
distinctId: 'd',
})
expect(payload.structuredContent).not.toBeUndefined()
// Text is TOON-encoded rawResult, not JSON, not the formatted table.
expect(payload.content[0]!.text).not.toBe(FORMATTED_TABLE)
expect(payload.content[0]!.text).toContain('_insightsUrl')
})
it('embeds analytics metadata in structuredContent when present (non-suppressed case)', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
suppressStructuredContentForFormattedResults: false,
distinctId: 'user-abc-123',
})
expect(payload.structuredContent).toMatchObject({
_analytics: {
distinctId: 'user-abc-123',
toolName: 'query-trends',
},
})
})
it('omits structuredContent for tools without a UI resource', () => {
// Non-UI tools (e.g. a hypothetical `query-logs` without a UI app) must never emit
// structuredContent regardless of client or formatted_results.
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(),
toolMeta: undefined,
toolName: 'whatever',
params: {},
suppressStructuredContentForFormattedResults: false,
distinctId: 'd',
})
expect(payload).not.toHaveProperty('structuredContent')
expect(payload.content[0]!.text).toBe(FORMATTED_TABLE)
})
})
// Inline-exec UI-app hosts (Insights Desktop, Claude Code, Cowork) go through the exec
// wrapper, which sets `forceUiDataToMeta` + `includeUiResponseMeta`. The app payload
// should only move onto `_meta` when a compact formatted table takes structuredContent's
// place for the model — otherwise it stays in the standard structuredContent field so it
// isn't duplicated under a non-standard `_meta` key.
describe('buildToolResultPayload — inline-exec UI host (forceUiDataToMeta)', () => {
it('suppresses structuredContent and re-homes the payload onto _meta when a formatted table exists', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(/* withFormatted */ true),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
forceUiDataToMeta: true,
includeUiResponseMeta: true,
distinctId: 'd',
})
// Model reads the compact table, not the verbose JSON.
expect(payload.content[0]!.text).toBe(FORMATTED_TABLE)
expect(payload).not.toHaveProperty('structuredContent')
// The UI app hydrates from _meta since structuredContent was dropped.
expect(payload._meta?.[APP_DATA_META_KEY]).toMatchObject({ results: expect.any(Array) })
})
it('keeps the payload in structuredContent and never emits _meta app-data when there is no formatted table', () => {
// Regression guard for the duplicated-results report: without a compact table the
// model reads the data as text anyway, so re-homing it onto `_meta` only duplicates
// the full payload under a non-standard key. Keep it in structuredContent instead.
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(/* withFormatted */ false),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
forceUiDataToMeta: true,
includeUiResponseMeta: true,
distinctId: 'd',
})
expect(payload.structuredContent).toMatchObject({ results: expect.any(Array) })
expect(payload._meta?.[APP_DATA_META_KEY]).toBeUndefined()
// The UI resource URI is still exposed for single-exec clients.
expect(payload._meta?.ui).toEqual({ resourceUri: 'ui://insights/query-results.html' })
})
// Regression guard for the relapse report: after the payload moved back into
// `structuredContent`, the text channel still carried a full TOON copy of it, so an
// inline-exec UI host handed the agent the same rows twice. List and detail shapes
// are the two the report flagged (`experiment-list`, `experiment-get`).
it.each([
[
'list',
{
count: 2,
next: null,
results: [
{ id: 387170, name: 'Onboarding copy', feature_flag_key: 'onboarding-copy' },
{ id: 387171, name: 'Pricing page', feature_flag_key: 'pricing-page' },
],
},
],
[
'detail',
{
id: 387170,
name: 'Onboarding copy',
feature_flag: { key: 'onboarding-copy', filters: { groups: [{ rollout_percentage: 100 }] } },
metrics: [{ kind: 'ExperimentMetric', name: 'Signups' }],
},
],
])('serializes a %s payload exactly once when there is no formatted table', (_shape, handlerResult) => {
const payload = buildToolResultPayload({
handlerResult,
toolMeta: { ui: { resourceUri: 'ui://insights/experiment-list.html' } },
toolName: 'experiment-list',
params: {},
forceUiDataToMeta: true,
includeUiResponseMeta: true,
distinctId: 'd',
})
expect(payload.structuredContent).toMatchObject(handlerResult)
// The text channel points at structuredContent instead of repeating it.
expect(payload.content).toEqual([{ type: 'text', text: STRUCTURED_CONTENT_ONLY_TEXT }])
expect(payload.content[0]!.text).not.toContain('Onboarding copy')
expect(payload._meta?.[APP_DATA_META_KEY]).toBeUndefined()
})
it('counts the structured payload for token estimation when the text is only a pointer', () => {
// The estimate feeds `$mcp_tool_call.output_tokens`; without this the whole
// response would be billed as the one-line pointer.
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(/* withFormatted */ false),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: {},
forceUiDataToMeta: true,
includeUiResponseMeta: true,
distinctId: 'd',
})
expect(estimateResponseTokens(payload)).toBeGreaterThan(estimateTokens(STRUCTURED_CONTENT_ONLY_TEXT))
})
it('keeps the mirrored text when the caller asked for JSON output', () => {
const payload = buildToolResultPayload({
handlerResult: queryTrendsHandlerResult(/* withFormatted */ false),
toolMeta: queryTrendsToolMeta,
toolName: 'query-trends',
params: { output_format: 'json' },
forceUiDataToMeta: true,
includeUiResponseMeta: true,
distinctId: 'd',
})
expect(JSON.parse(payload.content[0]!.text)).toMatchObject({ results: expect.any(Array) })
expect(payload.structuredContent).toMatchObject({ results: expect.any(Array) })
})
})
describe('buildToolResultPayload — non-query use cases', () => {
it('preserves array handler results', () => {
const result = [{ id: 'template-1' }]
Object.defineProperty(result, INSIGHTS_FORMATTED_RESULTS_OVERRIDE_KEY, {
value: 'informational result',
enumerable: false,
})
const payload = buildToolResultPayload({
handlerResult: result,
toolMeta: undefined,
toolName: 'templates-list',
params: {},
})
expect(payload.content).toEqual([{ type: 'text', text: 'informational result' }])
})
it('passes string handler results through verbatim (no character-indexed expansion)', () => {
// Regression guard for the original bug: `execute-sql` and other
// string-returning handlers must not be object-rest-destructured.
const sqlResult = 'You are given a table...\n\nmarker|answer\nfix_applied|42'
const payload = buildToolResultPayload({
handlerResult: sqlResult,
toolMeta: undefined,
toolName: 'execute-sql',
params: {},
suppressStructuredContentForFormattedResults: true,
distinctId: undefined,
})
expect(payload.content[0]!.text).toBe(sqlResult)
expect(payload.content[0]!.text).not.toMatch(/"0":\s*"./)
expect(payload).not.toHaveProperty('structuredContent')
})
it('JSON-encodes rawResult when tool-level outputFormat=json is configured', () => {
// For tools like `query-llm-traces-list` that advertise JSON output at the tool level
// (via `_meta[INSIGHTS_META_KEY].outputFormat === 'json'`), text is JSON, not TOON.
const payload = buildToolResultPayload({
handlerResult: { results: [], _insightsUrl: 'http://...' },
toolMeta: { [INSIGHTS_META_KEY]: { outputFormat: 'json' } },
toolName: 'query-llm-traces-list',
params: {},
suppressStructuredContentForFormattedResults: true,
distinctId: undefined,
})
expect(() => JSON.parse(payload.content[0]!.text)).not.toThrow()
expect(JSON.parse(payload.content[0]!.text)).toEqual({
results: [],
_insightsUrl: 'http://...',
})
})
})
describe('isToolCallPayload — nominal brand', () => {
it('matches only payloads carrying the exec brand', () => {
const branded = markExecPayload({
content: [{ type: 'text', text: 'hi' }],
structuredContent: { foo: 1 },
_meta: { ui: { resourceUri: 'ui://app' } },
})
expect(isToolCallPayload(branded)).toBe(true)
expect(branded[EXEC_BUILT_PAYLOAD]).toBe(true)
})
it('returns false for bare CallToolResult-shaped payloads without the brand', () => {
// Regression guard: `setActive` / `searchDocs` return this shape today, and
// a future tool returning a similar shape must not silently skip the
// buildToolResultPayload pipeline.
expect(
isToolCallPayload({
content: [{ type: 'text', text: 'switched' }],
})
).toBe(false)
expect(
isToolCallPayload({
content: [{ type: 'text', text: 'switched' }],
structuredContent: { foo: 1 },
})
).toBe(false)
expect(
isToolCallPayload({
content: [{ type: 'text', text: 'switched' }],
_meta: { ui: { resourceUri: 'ui://app' } },
})
).toBe(false)
})
it('returns false for non-object values', () => {
expect(isToolCallPayload(undefined)).toBe(false)
expect(isToolCallPayload(null)).toBe(false)
expect(isToolCallPayload('string-result')).toBe(false)
expect(isToolCallPayload(42)).toBe(false)
})
})
describe('buildToolResultPayload — confirmed-action prepare results', () => {
const prepareResult = {
confirmation_hash: 'signed-token-abc',
confirmation_word: 'confirm',
action: 'create loop',
message: "About to create the loop 'Open PR Summary'. Reply 'confirm' to create it.",
next_steps: 'Surface the message above to the user.',
}
it('carries the prepare payload on _meta so UI apps can read the hash', () => {
const payload = buildToolResultPayload({
handlerResult: prepareResult,
toolMeta: undefined,
toolName: 'loops-create-prepare',
params: { name: 'Open PR Summary' },
})
expect(payload._meta?.[APP_DATA_META_KEY]).toMatchObject({
confirmation_hash: 'signed-token-abc',
confirmation_word: 'confirm',
})
expect(payload).not.toHaveProperty('structuredContent')
expect(payload.content[0]!.text).toContain('signed-token-abc')
})
it.each([
['non-prepare object result', { results: [{ id: 1 }] }],
['confirmation_hash without the confirm word', { confirmation_hash: 'abc', confirmation_word: 'yes' }],
['non-string confirmation_hash', { confirmation_hash: 42, confirmation_word: 'confirm' }],
])('does not attach _meta for %s', (_label, handlerResult) => {
const payload = buildToolResultPayload({
handlerResult,
toolMeta: undefined,
toolName: 'whatever',
params: {},
})
expect(payload).not.toHaveProperty('_meta')
})
})