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

2139 lines
75 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
buildResponseFilter,
composeToolSchema,
extractPathParams,
generateDefinitionsJson,
generateQueryWrapperDefinitionsJson,
generateQueryWrapperFile,
generateToolCode,
} from '../../scripts/generate-tools'
import type { OpenApiSpec, ResolvedOperation } from '../../scripts/generate-tools'
import { QueryWrapperToolConfigSchema, ToolConfigSchema } from '../../scripts/yaml-config-schema'
import type { EnabledQueryWrapperToolConfig, EnabledToolConfig, ToolConfig } from '../../scripts/yaml-config-schema'
function makeSpec(overrides: Partial<OpenApiSpec> = {}): OpenApiSpec {
return {
paths: {},
components: { schemas: {} },
...overrides,
}
}
function makeResolved(overrides: Partial<ResolvedOperation> = {}): ResolvedOperation {
return {
method: 'GET',
path: '/v1/projects/{project_id}/things/',
operation: {
operationId: 'things_list',
parameters: [],
},
...overrides,
}
}
const defaultCategory = {
category: 'Things',
feature: 'things',
url_prefix: '/things',
tools: {},
}
// Tests don't exercise schema_ref param_overrides, so return an empty JsonSchemaRoot stub.
const stubGetQuerySchema = (): { definitions: Record<string, never> } => ({ definitions: {} })
describe('extractPathParams', () => {
const cases = [
{
name: 'returns empty for path without params',
url: '/v1/projects/things/',
expected: [],
},
{
name: 'excludes project_id',
url: '/v1/projects/{project_id}/things/',
expected: [],
},
{
name: 'extracts non-project_id params',
url: '/v1/projects/{project_id}/things/{id}/',
expected: ['id'],
},
{
name: 'extracts multiple params',
url: '/v1/projects/{project_id}/things/{thing_id}/sub/{sub_id}/',
expected: ['thing_id', 'sub_id'],
},
]
it.each(cases)('$name', ({ url, expected }) => {
expect(extractPathParams(url)).toEqual(expected)
})
})
describe('composeToolSchema', () => {
it('returns empty toolInputsImports when no param_overrides have input_schema', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved()
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.toolInputsImports).toEqual([])
})
it('returns empty toolInputsImports when param_overrides only have descriptions', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
param_overrides: {
name: { description: 'A custom description' },
},
}
const resolved = makeResolved()
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.toolInputsImports).toEqual([])
})
it('collects toolInputsImports from param_overrides with input_schema', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
param_overrides: {
steps: { input_schema: 'StepsSchema' },
filters: { input_schema: 'FiltersSchema' },
name: { description: 'Just a description' },
},
}
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
steps: { type: 'object' },
filters: { type: 'object' },
name: { type: 'string' },
},
},
},
},
},
},
})
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.toolInputsImports).toContain('StepsSchema')
expect(result.toolInputsImports).toContain('FiltersSchema')
expect(result.toolInputsImports).toHaveLength(2)
})
it('applies .extend() for param_overrides with input_schema', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
param_overrides: {
steps: { input_schema: 'StepsSchema' },
},
}
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
steps: { type: 'object' },
name: { type: 'string' },
},
},
},
},
},
},
})
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.schemaExpr).toContain('.extend({ steps: StepsSchema })')
})
})
describe('generateToolCode with input_schema', () => {
it('uses custom schema import when input_schema is set', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
input_schema: 'ThingCreateSchema',
scopes: ['thing:write'],
annotations: { readOnly: false, destructive: false, idempotent: false },
}
const resolved = makeResolved({ method: 'POST' })
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.toolInputsImports).toEqual(['ThingCreateSchema'])
expect(result.orvalImports).toEqual([])
expect(result.code).toMatchSnapshot()
})
it('generates body forwarding for POST with input_schema', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
input_schema: 'ThingCreateSchema',
}
const resolved = makeResolved({ method: 'POST' })
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('const parsedParams = ThingsCreateSchema.parse(params)')
expect(result.code).toContain('body: parsedParams')
})
it('generates query forwarding for GET with input_schema', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
}
const resolved = makeResolved({ method: 'GET' })
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('const parsedParams = ThingsListSchema.parse(params)')
expect(result.code).toContain('query: parsedParams')
})
it('uses response_type override for custom input schema tools', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
response_type: "Omit<Schemas.ThingList, 'results'> & { results: unknown[] }",
}
const resolved = makeResolved({ method: 'GET' })
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain(
"const thingsList = (): ToolBase<typeof ThingsListSchema, Omit<Schemas.ThingList, 'results'> & { results: unknown[] }>"
)
expect(result.code).toContain(
"const result = await context.api.request<Omit<Schemas.ThingList, 'results'> & { results: unknown[] }>({"
)
})
it('destructures path params for input_schema with path params', () => {
const config: ToolConfig = {
operation: 'things_update',
enabled: true,
input_schema: 'ThingUpdateSchema',
}
const resolved = makeResolved({
method: 'PATCH',
path: '/v1/projects/{project_id}/things/{id}/',
})
const result = generateToolCode(
'things-update',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('id, ...body')
expect(result.code).toContain('${encodeURIComponent(String(id))}')
})
it('destructures path params into query for GET with path params', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
input_schema: 'ThingGetSchema',
}
const resolved = makeResolved({
method: 'GET',
path: '/v1/projects/{project_id}/things/{id}/',
})
const result = generateToolCode(
'things-retrieve',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('id, ...query')
})
it('applies enrich_url with input_schema', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
input_schema: 'ThingCreateSchema',
enrich_url: '{id}',
}
const resolved = makeResolved({ method: 'POST' })
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toMatchSnapshot()
})
it('applies list enrichment with input_schema', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
list: true,
enrich_url: '{id}',
}
const resolved = makeResolved({ method: 'GET' })
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toMatchSnapshot()
})
it('applies list enrichment without inserting a path slash before a query-string prefix', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
list: true,
enrich_url: '?tab=alerts&alert_id={id}',
}
const resolved = makeResolved({ method: 'GET' })
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('`/things?tab=alerts&alert_id=${item.id}`')
})
it('applies list enrichment without inserting a path slash before a fragment prefix', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
list: true,
enrich_url: '#tab-{id}',
}
const resolved = makeResolved({ method: 'GET' })
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('`/things#tab-${item.id}`')
})
it('extends the custom schema with a selectable `fields` param and narrows the response', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
list: true,
response: { include: ['id', 'name'], selectable: true },
}
const resolved = makeResolved({ method: 'GET' })
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
// The `fields` param must be added to the custom schema, constrained to the allowlist,
// and reject an empty array so it can't silently fall back to the full payload.
expect(result.code).toContain('.extend({ fields: z.array(z.enum([')
expect(result.code).toContain("z.enum(['id', 'name'])")
expect(result.code).toContain('.min(1)')
// And the response filter must honor it, falling back to the full allowlist when omitted.
expect(result.code).toContain("params.fields?.length ? params.fields : ['id', 'name']")
})
it('throws when selectable is set without an include allowlist', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
input_schema: 'ThingListSchema',
list: true,
response: { selectable: true },
}
const resolved = makeResolved({ method: 'GET' })
// selectable has no allowlist to constrain `fields` against — codegen must fail loudly rather
// than silently emit a tool whose selectable flag does nothing.
expect(() =>
generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
).toThrow(/response\.selectable requires a non-empty response\.include allowlist/)
})
})
describe('generateToolCode without input_schema', () => {
it('returns orvalImports and no toolInputsImports', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved({
operation: {
operationId: 'things_list',
parameters: [
{
in: 'path',
name: 'project_id',
required: true,
schema: { type: 'string' },
},
],
},
})
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.toolInputsImports).toEqual([])
})
it('comma-joins explode:false array query params (DRF comma-separated filters)', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved({
operation: {
operationId: 'things_list',
parameters: [
{
in: 'query',
name: 'type',
style: 'form',
explode: false,
schema: { type: 'array', items: { type: 'string' } },
},
],
},
})
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain(
"type: Array.isArray(params.type) ? params.type.join(',') || undefined : params.type,"
)
})
it('forwards array query params without explode:false untouched (json.loads()-style backends)', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved({
operation: {
operationId: 'things_list',
parameters: [
{
in: 'query',
name: 'serviceNames',
schema: { type: 'array', items: { type: 'string' } },
},
],
},
})
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('serviceNames: params.serviceNames,')
expect(result.code).not.toContain('serviceNames.join')
})
it('keeps explode:false params with object items on the JSON path', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved({
operation: {
operationId: 'things_list',
parameters: [
{
in: 'query',
name: 'filters',
style: 'form',
explode: false,
schema: { type: 'array', items: { $ref: '#/components/schemas/Filter' } },
},
],
},
})
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('filters: params.filters,')
expect(result.code).not.toContain('filters.join')
})
it('collects toolInputsImports from param_overrides', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
param_overrides: {
steps: { input_schema: 'StepsSchema' },
},
}
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
steps: { type: 'object' },
name: { type: 'string' },
},
},
},
},
},
},
})
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.toolInputsImports).toContain('StepsSchema')
expect(result.code).toContain('.extend({ steps: StepsSchema })')
})
})
describe('inject_body', () => {
const injectBodyResolved = (): ResolvedOperation =>
makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
name: { type: 'string' },
},
},
},
},
},
},
})
it('emits hardcoded body assignments for inject_body entries', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
inject_body: { created_via: 'mcp' },
}
const result = generateToolCode(
'things-create',
config,
injectBodyResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain(`body["created_via"] = "mcp"`)
})
it('emits inject_body after dynamic body builder so it overrides caller input', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
inject_body: { created_via: 'mcp' },
}
const result = generateToolCode(
'things-create',
config,
injectBodyResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
const nameIdx = result.code.indexOf(`body["name"]`)
const injectIdx = result.code.indexOf(`body["created_via"]`)
expect(nameIdx).toBeGreaterThan(-1)
expect(injectIdx).toBeGreaterThan(nameIdx)
})
it('initializes body even when inject_body is the only source', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
exclude_params: ['name'],
inject_body: { created_via: 'mcp' },
}
const result = generateToolCode(
'things-create',
config,
injectBodyResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('const body: Record<string, unknown> = {}')
expect(result.code).toContain(`body["created_via"] = "mcp"`)
expect(result.code).toContain('body,')
})
it('escapes inject_body keys that contain special characters', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
inject_body: { "weird'key": 'safe' },
}
const result = generateToolCode(
'things-create',
config,
injectBodyResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
// JSON.stringify escapes the single quote so the generated TS stays valid.
expect(result.code).toContain(`body["weird'key"] = "safe"`)
})
})
describe('anyOf / oneOf body schemas (discriminated unions)', () => {
// Polymorphic Python serializers (e.g. file-download-batch-exports) emit
// request bodies as `anyOf` of per-variant object schemas. Without union
// handling, `bodyFieldNames` stays empty and the generated handler POSTs
// with no body — the server rejects the request as "field required".
const unionResolved = (): ResolvedOperation =>
makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
anyOf: [
{
type: 'object',
properties: {
kind: { type: 'string', enum: ['a'] },
shared_field: { type: 'string' },
only_in_a: { type: 'string' },
},
required: ['kind', 'shared_field'],
},
{
type: 'object',
properties: {
kind: { type: 'string', enum: ['b'] },
shared_field: { type: 'string' },
only_in_b: { type: 'number' },
},
required: ['kind', 'shared_field', 'only_in_b'],
},
],
},
},
},
},
},
})
it('emits body assembly for every field, guarding variant-specific access with `in`', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
}
const result = generateToolCode(
'things-create',
config,
unionResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
// Body is initialized and forwarded.
expect(result.code).toContain('const body: Record<string, unknown> = {}')
expect(result.code).toContain('body,')
// Every field across all variants is assembled into the body.
expect(result.code).toContain(`body["kind"] = params.kind`)
expect(result.code).toContain(`body["shared_field"] = params.shared_field`)
expect(result.code).toContain(`body["only_in_a"] = params.only_in_a`)
expect(result.code).toContain(`body["only_in_b"] = params.only_in_b`)
// Fields present in every variant don't need the `in` guard.
expect(result.code).toContain('if (params.kind !== undefined)')
expect(result.code).toContain('if (params.shared_field !== undefined)')
// Fields only in some variants must be guarded with `'X' in params` so
// accessing them on the inferred union type still type-checks.
expect(result.code).toContain(`if ('only_in_a' in params && params.only_in_a !== undefined)`)
expect(result.code).toContain(`if ('only_in_b' in params && params.only_in_b !== undefined)`)
})
it('flattens allOf composition inside a union variant', () => {
// Mirrors the common OpenAPI pattern of a variant that extends a base
// schema via allOf (e.g. `$ref` + extra properties). Without allOf
// handling the base-schema fields would be dropped from the body.
const spec = makeSpec({
components: {
schemas: {
BaseFields: {
type: 'object',
properties: {
base_field: { type: 'string' },
},
required: ['base_field'],
},
},
},
})
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
anyOf: [
{
allOf: [
{ $ref: '#/components/schemas/BaseFields' },
{
type: 'object',
properties: {
kind: { type: 'string', enum: ['a'] },
only_in_a: { type: 'string' },
},
required: ['kind'],
},
],
},
{
type: 'object',
properties: {
base_field: { type: 'string' },
kind: { type: 'string', enum: ['b'] },
},
required: ['base_field', 'kind'],
},
],
},
},
},
},
},
})
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
}
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
spec,
new Set<string>(),
stubGetQuerySchema
)
// allOf-composed fields reach the body builder, treated as shared
// (present in every variant) rather than variant-specific.
expect(result.code).toContain(`body["base_field"] = params.base_field`)
expect(result.code).toContain('if (params.base_field !== undefined)')
expect(result.code).toContain('if (params.kind !== undefined)')
// The variant-only field still gets `in`-guarded.
expect(result.code).toContain(`if ('only_in_a' in params && params.only_in_a !== undefined)`)
})
it('resolves $ref-based union variants from components.schemas', () => {
const spec = makeSpec({
components: {
schemas: {
VariantA: {
type: 'object',
properties: { kind: { type: 'string' }, payload_a: { type: 'string' } },
required: ['kind'],
},
VariantB: {
type: 'object',
properties: { kind: { type: 'string' }, payload_b: { type: 'string' } },
required: ['kind'],
},
},
},
})
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{ $ref: '#/components/schemas/VariantA' },
{ $ref: '#/components/schemas/VariantB' },
],
},
},
},
},
},
})
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
}
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
spec,
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain(`body["payload_a"] = params.payload_a`)
expect(result.code).toContain(`body["payload_b"] = params.payload_b`)
})
})
describe('rename_params', () => {
it('swaps field names in schema expression and tracks renames', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
rename_params: { $unset: 'property_key' },
}
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
$unset: { type: 'string' },
},
},
},
},
},
},
})
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.schemaExpr).toContain(".omit({ '$unset': true })")
expect(result.schemaExpr).toContain(".extend({ property_key: ThingsCreateBody.shape['$unset'] })")
expect(result.bodyFieldNames).toContain('property_key')
expect(result.bodyFieldNames).not.toContain('$unset')
expect(result.renamedFields).toEqual({ property_key: '$unset' })
})
it('generates handler that maps alias to original body key', () => {
const config: ToolConfig = {
operation: 'things_create',
enabled: true,
rename_params: { $unset: 'property_key' },
}
const resolved = makeResolved({
method: 'POST',
operation: {
operationId: 'things_create',
parameters: [],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
$unset: { type: 'string' },
},
},
},
},
},
},
})
const result = generateToolCode(
'things-create',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('params.property_key !== undefined')
expect(result.code).toContain('body["$unset"] = params.property_key')
expect(result.code).not.toContain('params.$unset')
})
})
describe('param_overrides aliases', () => {
it('wraps the composed schema with normalizeParamAliases and imports the helper', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
param_overrides: {
id: { aliases: ['thingId', 'thing_id'] },
},
}
const resolved = makeResolved({
method: 'GET',
path: '/v1/projects/{project_id}/things/{id}/',
operation: {
operationId: 'things_retrieve',
parameters: [
{ name: 'project_id', in: 'path', required: true, schema: { type: 'string' } },
{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
],
},
})
const result = generateToolCode(
'things-get',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.castHelperImports.has('normalizeParamAliases')).toBe(true)
expect(result.code).toContain(
"const ThingsGetSchema = z.preprocess(normalizeParamAliases({ id: ['thingId', 'thing_id'] }), ThingsRetrieveParams.omit({ project_id: true }))"
)
})
})
describe('x-accepts-stringified-json query params', () => {
function resolvedWith(parameters: NonNullable<ResolvedOperation['operation']['parameters']>): ResolvedOperation {
return makeResolved({
operation: {
operationId: 'things_list',
parameters,
},
})
}
it('widens schema for params marked x-accepts-stringified-json', () => {
const config: ToolConfig = { operation: 'things_list', enabled: true }
const resolved = resolvedWith([
{
in: 'query',
name: 'filters_override',
schema: { type: 'string' },
description: 'Filters override.',
'x-accepts-stringified-json': true,
},
])
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.schemaExpr).toContain(
"filters_override: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe('Filters override.')"
)
})
it('does not widen sibling params that lack the extension', () => {
const config: ToolConfig = { operation: 'things_list', enabled: true }
const resolved = resolvedWith([
{
in: 'query',
name: 'filters_override',
schema: { type: 'string' },
'x-accepts-stringified-json': true,
},
{
in: 'query',
name: 'plain_string',
schema: { type: 'string' },
},
])
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.schemaExpr).toContain('filters_override: z.union([z.string()')
expect(result.schemaExpr).not.toContain('plain_string: z.union(')
})
it('does not widen params named *_override without the extension', () => {
const config: ToolConfig = { operation: 'things_list', enabled: true }
const resolved = resolvedWith([
{
in: 'query',
name: 'filters_override',
schema: { type: 'string' },
// no x-accepts-stringified-json — magic naming alone must not trigger widening
},
])
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.schemaExpr).not.toContain('z.union([z.string()')
})
it('skips widening when the YAML config also defines a param_override for the same field', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
param_overrides: {
filters_override: { description: 'Custom YAML description' },
},
}
const resolved = resolvedWith([
{
in: 'query',
name: 'filters_override',
schema: { type: 'string' },
description: 'OpenAPI description',
'x-accepts-stringified-json': true,
},
])
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
// YAML wins — describe() comes through, no union extension afterwards.
expect(result.schemaExpr).toContain('Custom YAML description')
expect(result.schemaExpr).not.toContain('z.union([z.string()')
})
it('respects exclude_params — excluded fields are not widened', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
exclude_params: ['filters_override'],
}
const resolved = resolvedWith([
{
in: 'query',
name: 'filters_override',
schema: { type: 'string' },
'x-accepts-stringified-json': true,
},
{
in: 'query',
name: 'variables_override',
schema: { type: 'string' },
'x-accepts-stringified-json': true,
},
])
const result = composeToolSchema(config, resolved, makeSpec(), stubGetQuerySchema)
expect(result.schemaExpr).not.toContain('filters_override: z.union(')
expect(result.schemaExpr).toContain('variables_override: z.union([z.string()')
})
})
describe('QueryWrapperToolConfigSchema validation', () => {
it.each([true, false] as const)('accepts use_optimized_output: %s', (value) => {
const result = QueryWrapperToolConfigSchema.safeParse({
schema_ref: 'AssistantTrendsQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
use_optimized_output: value,
})
expect(result.success).toBe(true)
})
it('rejects non-boolean use_optimized_output values', () => {
const result = QueryWrapperToolConfigSchema.safeParse({
schema_ref: 'AssistantTrendsQuery',
enabled: true,
use_optimized_output: 'optimized',
})
expect(result.success).toBe(false)
})
it('accepts system_prompt_hint on query wrappers', () => {
const result = QueryWrapperToolConfigSchema.safeParse({
schema_ref: 'AssistantTrendsQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
system_prompt_hint: 'Time series, aggregations, formulas',
})
expect(result.success).toBe(true)
})
it('accepts system_prompt_hint on standard tools', () => {
const result = ToolConfigSchema.safeParse({
operation: 'logs_query_create',
enabled: true,
system_prompt_hint: 'Log filtering by severity/service/attribute',
})
expect(result.success).toBe(true)
})
})
describe('system_prompt_hint flows into tool definitions', () => {
it('propagates system_prompt_hint from wrapper YAML into the generated definition', () => {
const wrapperConfig: EnabledQueryWrapperToolConfig = {
schema_ref: 'AssistantTrendsQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
system_prompt_hint: 'Time series, aggregations, formulas',
}
const definitions = generateQueryWrapperDefinitionsJson(
{ category: 'Query wrappers', feature: 'insights', wrappers: {} },
[['query-trends', wrapperConfig]],
'/tmp'
) as Record<string, { system_prompt_hint?: string }>
expect(definitions['query-trends']?.system_prompt_hint).toBe('Time series, aggregations, formulas')
})
it('omits system_prompt_hint from the wrapper definition when not set', () => {
const wrapperConfig: EnabledQueryWrapperToolConfig = {
schema_ref: 'AssistantTrendsQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
}
const definitions = generateQueryWrapperDefinitionsJson(
{ category: 'Query wrappers', feature: 'insights', wrappers: {} },
[['query-trends', wrapperConfig]],
'/tmp'
)
expect((definitions['query-trends'] as Record<string, unknown>).system_prompt_hint).toBeUndefined()
})
it('propagates system_prompt_hint from standard tool YAML into the generated definition', () => {
const toolConfig: EnabledToolConfig = {
operation: 'logs_query_create',
enabled: true,
scopes: ['logs:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
system_prompt_hint: 'Log filtering by severity/service/attribute',
}
const resolved: ResolvedOperation = {
method: 'POST',
path: '/v1/projects/{project_id}/logs/query/',
operation: { operationId: 'logs_query_create', description: 'Query logs' },
}
const definitions = generateDefinitionsJson([
{
config: { category: 'Logs', feature: 'logs', url_prefix: '/logs', tools: {} },
enabledTools: [['query-logs', toolConfig, resolved]],
enabledWrappers: [],
yamlDir: '/tmp',
},
]) as Record<string, { system_prompt_hint?: string }>
expect(definitions['query-logs']?.system_prompt_hint).toBe('Log filtering by severity/service/attribute')
})
})
describe('generateQueryWrapperFile with use_optimized_output', () => {
const minimalQuerySchema = {
definitions: {
AssistantTestQuery: {
type: 'object' as const,
properties: {
kind: { const: 'TestQuery', type: 'string' as const },
},
required: ['kind'],
},
},
}
it('emits outputFormat: optimized when use_optimized_output is true', () => {
const { code } = generateQueryWrapperFile(
{
category: 'Test',
feature: 'test',
wrappers: {
'query-test': {
schema_ref: 'AssistantTestQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
use_optimized_output: true,
},
},
},
'test.yaml',
minimalQuerySchema
)
expect(code).toContain("outputFormat: 'optimized'")
})
it.each([false, undefined] as const)('emits outputFormat: json when use_optimized_output is %s', (value) => {
const { code } = generateQueryWrapperFile(
{
category: 'Test',
feature: 'test',
wrappers: {
'query-test': {
schema_ref: 'AssistantTestQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
...(value === undefined ? {} : { use_optimized_output: value }),
},
},
},
'test.yaml',
minimalQuerySchema
)
expect(code).toContain("outputFormat: 'json'")
})
it('extends the wrapper schema with an output_format input when use_optimized_output is true', () => {
const { code } = generateQueryWrapperFile(
{
category: 'Test',
feature: 'test',
wrappers: {
'query-test': {
schema_ref: 'AssistantTestQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
use_optimized_output: true,
},
},
},
'test.yaml',
minimalQuerySchema
)
expect(code).toContain('QueryTestSchema = AssistantTestQuery.extend({')
expect(code).toContain('output_format: z')
expect(code).toContain(".enum(['optimized', 'json'])")
expect(code).toContain(".default('optimized')")
})
it('does not expose an output_format input when use_optimized_output is false', () => {
const { code } = generateQueryWrapperFile(
{
category: 'Test',
feature: 'test',
wrappers: {
'query-test': {
schema_ref: 'AssistantTestQuery',
enabled: true,
scopes: ['query:read'],
annotations: { readOnly: true, destructive: false, idempotent: true },
},
},
},
'test.yaml',
minimalQuerySchema
)
expect(code).not.toContain('output_format: z')
})
})
describe('ToolConfigSchema validation', () => {
const validBase = {
operation: 'things_create',
enabled: true,
input_schema: 'ThingCreateSchema',
}
const conflictCases = [
{
name: 'rejects input_schema with include_params',
extra: { include_params: ['name'] },
},
{
name: 'rejects input_schema with exclude_params',
extra: { exclude_params: ['name'] },
},
{
name: 'rejects input_schema with param_overrides',
extra: { param_overrides: { name: { description: 'x' } } },
},
]
it.each(conflictCases)('$name', ({ extra }) => {
const result = ToolConfigSchema.safeParse({ ...validBase, ...extra })
expect(result.success).toBe(false)
})
it('allows input_schema without include_params, exclude_params, or param_overrides', () => {
const result = ToolConfigSchema.safeParse(validBase)
expect(result.success).toBe(true)
})
it('accepts response_type override', () => {
const result = ToolConfigSchema.safeParse({
operation: 'things_list',
enabled: true,
response_type: "Omit<Schemas.ThingList, 'results'>",
})
expect(result.success).toBe(true)
})
it('rejects response.include with response.exclude', () => {
const result = ToolConfigSchema.safeParse({
operation: 'things_list',
enabled: true,
response: { include: ['id'], exclude: ['name'] },
})
expect(result.success).toBe(false)
})
it('accepts response.include alone', () => {
const result = ToolConfigSchema.safeParse({
operation: 'things_list',
enabled: true,
response: { include: ['id', 'name'] },
})
expect(result.success).toBe(true)
})
it('accepts response.exclude alone', () => {
const result = ToolConfigSchema.safeParse({
operation: 'things_list',
enabled: true,
response: { exclude: ['filters', 'created_by'] },
})
expect(result.success).toBe(true)
})
})
// ------------------------------------------------------------------
// buildResponseFilter
// ------------------------------------------------------------------
describe('buildResponseFilter', () => {
it('returns empty code when no response filtering configured', () => {
const config: ToolConfig = { operation: 'things_list', enabled: true }
const result = buildResponseFilter(config)
expect(result.code).toBe('')
expect(result.helperImport).toBeNull()
})
it('generates pickResponseFields for detail endpoint with response.include', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
response: { include: ['id', 'name', 'status'] },
}
const result = buildResponseFilter(config)
expect(result.code).toContain('pickResponseFields(result, ')
expect(result.code).toContain("'id', 'name', 'status'")
expect(result.helperImport).toBe('pickResponseFields')
})
it('generates omitResponseFields for detail endpoint with response.exclude', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
response: { exclude: ['filters', 'created_by'] },
}
const result = buildResponseFilter(config)
expect(result.code).toContain('omitResponseFields(result, ')
expect(result.code).toContain("'filters', 'created_by'")
expect(result.helperImport).toBe('omitResponseFields')
})
it('maps pickResponseFields over results for list endpoint with response.include', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
list: true,
response: { include: ['id', 'key'] },
}
const result = buildResponseFilter(config)
expect(result.code).toContain('(result.results ?? []).map')
expect(result.code).toContain('pickResponseFields(item, ')
expect(result.helperImport).toBe('pickResponseFields')
})
it('maps omitResponseFields over results for list endpoint with response.exclude', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
list: true,
response: { exclude: ['large_blob'] },
}
const result = buildResponseFilter(config)
expect(result.code).toContain('(result.results ?? []).map')
expect(result.code).toContain('omitResponseFields(item, ')
expect(result.helperImport).toBe('omitResponseFields')
})
it('preserves wildcard dot-path patterns in generated code', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
response: { exclude: ['filters.groups.*.properties', 'created_by'] },
}
const result = buildResponseFilter(config)
expect(result.code).toContain("'filters.groups.*.properties'")
expect(result.code).toContain("'created_by'")
})
})
// ------------------------------------------------------------------
// generateToolCode — response filtering
// ------------------------------------------------------------------
describe('generateToolCode with response filtering', () => {
it('generates pickResponseFields and uses filtered var for enrichment', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
response: { include: ['id', 'name'] },
enrich_url: '{id}',
}
const resolved = makeResolved({
method: 'GET',
path: '/v1/projects/{project_id}/things/{id}/',
})
const result = generateToolCode(
'things-get',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('pickResponseFields(result, ')
expect(result.code).toContain('const filtered = ')
expect(result.code).toContain('withInsightsUrl(context, filtered,')
expect(result.toolUtilsValueImports).toEqual(new Set(['pickResponseFields']))
})
it('generates omitResponseFields for detail endpoint', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
response: { exclude: ['filters'] },
}
const resolved = makeResolved({
method: 'GET',
path: '/v1/projects/{project_id}/things/{id}/',
})
const result = generateToolCode(
'things-get',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('omitResponseFields(result, ')
expect(result.code).toContain('return filtered')
expect(result.toolUtilsValueImports).toEqual(new Set(['omitResponseFields']))
})
it('returns no tool utility imports when none are needed', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved()
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.toolUtilsValueImports).toEqual(new Set())
})
it('generates response filtering for list endpoint with enrichment', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
list: true,
enrich_url: '{id}',
response: { exclude: ['large_field'] },
}
const resolved = makeResolved()
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('(result.results ?? []).map((item: any) => omitResponseFields(item, ')
expect(result.code).toContain('...filtered,')
expect(result.code).toContain('(filtered.results ?? []).map')
expect(result.toolUtilsValueImports).toEqual(new Set(['omitResponseFields']))
})
})
describe('generateToolCode with informational response wrapping', () => {
it('wraps a filtered and enriched list after all response transformations', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
list: true,
enrich_url: '{id}',
response: {
exclude: ['large_field'],
informational_wrapper: {
tag: 'thing-references',
purpose: 'Use it only to identify relevant things.',
},
},
}
const resolved = makeResolved()
const result = generateToolCode(
'things-get',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('ToolBase<typeof ThingsGetSchema, WithInformationalResponse<unknown>>')
const filteringIndex = result.code.indexOf('const filtered =')
const wrappingIndex = result.code.indexOf('withInformationalResponse(')
expect(filteringIndex).toBeGreaterThan(-1)
expect(wrappingIndex).toBeGreaterThan(filteringIndex)
expect(result.code).toContain(
'withInformationalResponse(await withInsightsUrl(context, {\n ...filtered,'
)
expect(result.code).toContain('"thing-references", "Use it only to identify relevant things.")')
expect(result.needsWithInformationalResponse).toBe(true)
expect(result.toolUtilsValueImports).toEqual(new Set(['omitResponseFields', 'withInformationalResponse']))
})
})
describe('path parameter encoding', () => {
it('wraps project_id with encodeURIComponent in generated paths', () => {
const config: ToolConfig = {
operation: 'things_list',
enabled: true,
}
const resolved = makeResolved()
const result = generateToolCode(
'things-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('${encodeURIComponent(String(projectId))}')
expect(result.code).not.toMatch(/\$\{projectId\}/)
})
it('wraps organization_id with encodeURIComponent in generated paths', () => {
const config: ToolConfig = {
operation: 'members_list',
enabled: true,
}
const resolved = makeResolved({
path: '/v1/organizations/{organization_id}/members/',
operation: {
operationId: 'members_list',
parameters: [{ name: 'organization_id', in: 'path', required: true, schema: { type: 'string' } }],
},
})
const result = generateToolCode(
'org-members-list',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('${encodeURIComponent(String(orgId))}')
expect(result.code).not.toMatch(/\$\{orgId\}/)
})
it('wraps user-provided path params with encodeURIComponent', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
}
const resolved = makeResolved({
method: 'GET',
path: '/v1/projects/{project_id}/things/{id}/',
operation: {
operationId: 'things_retrieve',
parameters: [
{ name: 'project_id', in: 'path', required: true, schema: { type: 'string' } },
{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
],
},
})
const result = generateToolCode(
'things-get',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('${encodeURIComponent(String(params.id))}')
expect(result.code).not.toMatch(/\$\{params\.id\}/)
})
it('wraps fallback-resolved params with encodeURIComponent', () => {
const config: ToolConfig = {
operation: 'things_retrieve',
enabled: true,
param_overrides: {
id: { optional: true, fallback: 'orgId', description: 'Optional ID' },
},
}
const resolved = makeResolved({
method: 'GET',
path: '/v1/projects/{project_id}/things/{id}/',
operation: {
operationId: 'things_retrieve',
parameters: [
{ name: 'project_id', in: 'path', required: true, schema: { type: 'string' } },
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
],
},
})
const result = generateToolCode(
'things-get',
config,
resolved,
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
// Fallback params use local variable (no params. prefix) but still get encoded
expect(result.code).toContain('${encodeURIComponent(String(id))}')
expect(result.code).not.toMatch(/\$\{id\}[^)]/)
})
})
// ------------------------------------------------------------------
// confirmed_action — schema validation
// ------------------------------------------------------------------
describe('ToolConfigSchema confirmed_action', () => {
const base = {
operation: 'organizations_partial_update',
enabled: true,
} as const
it('accepts a minimal confirmed_action block', () => {
const result = ToolConfigSchema.safeParse({
...base,
confirmed_action: { message: 'Confirm action on {orgId}?' },
})
expect(result.success).toBe(true)
})
it('accepts an action_label override', () => {
const result = ToolConfigSchema.safeParse({
...base,
confirmed_action: { message: 'Confirm', action_label: 'enforce 2FA' },
})
expect(result.success).toBe(true)
})
it('rejects missing message', () => {
const result = ToolConfigSchema.safeParse({
...base,
confirmed_action: { action_label: 'foo' },
})
expect(result.success).toBe(false)
})
it('rejects confirmed_action combined with ui_app (silent UI-app drop)', () => {
const result = ToolConfigSchema.safeParse({
...base,
ui_app: 'org-2fa',
confirmed_action: { message: 'x' },
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0]!.message).toContain('ui_app')
}
})
it('rejects confirmed_action combined with a whole-tool input_schema', () => {
const result = ToolConfigSchema.safeParse({
...base,
input_schema: 'OrganizationUpdateSchema',
confirmed_action: { message: 'x' },
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0]!.message).toContain('input_schema')
}
})
it('rejects unknown keys inside the confirmed_action object', () => {
const result = ToolConfigSchema.safeParse({
...base,
confirmed_action: { message: 'x', bogus: true },
})
expect(result.success).toBe(false)
})
})
// ------------------------------------------------------------------
// generateToolCode — confirmed_action codegen
// ------------------------------------------------------------------
describe('generateToolCode with confirmed_action', () => {
function makeConfirmedConfig(): ToolConfig {
return {
operation: 'organizations_partial_update',
enabled: true,
title: 'Enforce 2FA',
confirmed_action: {
message: 'About to enable enforce 2FA on organization {id}.',
action_label: 'enforce 2FA',
},
}
}
function makePatchResolved(): ResolvedOperation {
return {
method: 'PATCH',
path: '/v1/organizations/{id}/',
operation: { operationId: 'organizations_partial_update', parameters: [] },
}
}
it('emits TWO factories (prepare and execute), not the base factory', () => {
const result = generateToolCode(
'organization-enforce-2fa-update',
makeConfirmedConfig(),
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('organizationEnforce2faUpdatePrepare')
expect(result.code).toContain('organizationEnforce2faUpdateExecute')
// No base factory of the original name should be emitted.
expect(result.code).not.toMatch(/const organizationEnforce2faUpdate\s*=/)
})
it('emits a strict confirmation-only schema for the execute variant', () => {
const result = generateToolCode(
'organization-enforce-2fa-update',
makeConfirmedConfig(),
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('OrganizationEnforce2faUpdateSchemaExecute')
expect(result.code).toContain('z.strictObject({')
expect(result.code).not.toContain('OrganizationEnforce2faUpdateSchema.extend({')
expect(result.code).toContain('confirmation_hash')
expect(result.code).toContain('confirmation:')
})
it('wires prepare into prepareConfirmedAction with the messageTemplate', () => {
const result = generateToolCode(
'organization-enforce-2fa-update',
makeConfirmedConfig(),
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('await prepareConfirmedAction')
expect(result.code).toContain('purpose: "organization-enforce-2fa-update"')
expect(result.code).toContain('actionLabel: "enforce 2FA"')
expect(result.code).toContain('messageTemplate: "About to enable enforce 2FA on organization {id}."')
})
it('wires execute into executeConfirmedAction and falls through to the original API call', () => {
const result = generateToolCode(
'organization-enforce-2fa-update',
makeConfirmedConfig(),
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('await executeConfirmedAction')
expect(result.code).toContain('if (!__guard.ok)')
// After the guard, the handler runs the original API request.
expect(result.code).toContain('await context.api.request')
expect(result.code).toContain("method: 'PATCH'")
})
it('uses only verifiedArgs as the downstream action params', () => {
const result = generateToolCode(
'organization-enforce-2fa-update',
makeConfirmedConfig(),
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('const params = __guard.verifiedArgs')
expect(result.code).toContain('incomingArgs: confirmationParams')
})
it('uses the tool title as the fallback action_label when none is set', () => {
const config: ToolConfig = {
...makeConfirmedConfig(),
title: 'Enforce 2FA',
confirmed_action: { message: 'msg' },
}
const result = generateToolCode(
'organization-enforce-2fa-update',
config,
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('actionLabel: "Enforce 2FA"')
})
it('binds the active project scope into prepare and re-checks it in execute (cross-project replay guard)', () => {
// A project-scoped confirmed action must sign the active project at
// prepare time (boundScope) and re-verify it at execute time
// (expectedScope). Without this, a confirmation prepared while one
// project was active could be replayed against another after
// switch-project.
const result = generateToolCode(
'metric-approve',
makeConfirmedConfig(),
makeResolved({
method: 'POST',
path: '/v1/projects/{project_id}/metrics/{name}/approve/',
operation: {
operationId: 'organizations_partial_update',
parameters: [
{ in: 'path', name: 'project_id', required: true, schema: { type: 'string' } },
{ in: 'path', name: 'name', required: true, schema: { type: 'string' } },
],
},
}),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('boundScope: { projectId: String(__scopeProjectId) }')
expect(result.code).toContain('expectedScope: { projectId: String(__scopeProjectId) }')
})
it('reuses the scope-checked project id to build the execute path (no post-guard re-read)', () => {
// The path must be built from the same project id that was verified
// against expectedScope. Re-reading state after the guard would let a
// concurrent switch-project retarget the request at a different
// project than the confirmation was bound to.
const result = generateToolCode(
'metric-approve',
makeConfirmedConfig(),
makeResolved({
method: 'POST',
path: '/v1/projects/{project_id}/metrics/{name}/approve/',
operation: {
operationId: 'organizations_partial_update',
parameters: [
{ in: 'path', name: 'project_id', required: true, schema: { type: 'string' } },
{ in: 'path', name: 'name', required: true, schema: { type: 'string' } },
],
},
}),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('const projectId = __scopeProjectId')
expect(result.code).not.toContain(
'const params = __guard.verifiedArgs\n const projectId = await context.stateManager.getProjectId()'
)
})
it('resolves an omitted state-fallback id and signs it into the confirmed args (cross-org replay guard)', () => {
// When the target id is optional with a state fallback, prepare must
// resolve the active org/project to a concrete value and sign it, so a
// switch-organization between prepare and execute can't retarget the
// confirmed action at a different entity where the user is also an admin.
const config: ToolConfig = {
...makeConfirmedConfig(),
param_overrides: {
id: { optional: true, fallback: 'orgId', description: 'Organization ID.' },
},
}
const result = generateToolCode(
'organization-enforce-2fa-update',
config,
makePatchResolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
expect(result.code).toContain('const id = params.id ?? await context.stateManager.getOrgID()')
expect(result.code).toContain('args: { ...params, id }')
// The unresolved args object must not be what gets signed.
expect(result.code).not.toContain('args: params,')
})
})
describe('optional param with state fallback', () => {
const resolved = (): ResolvedOperation =>
makeResolved({
path: '/v1/organizations/{organization_id}/things/{id}/',
operation: {
operationId: 'things_retrieve',
parameters: [
{ in: 'path', name: 'organization_id', required: true, schema: { type: 'string' } },
{ in: 'path', name: 'id', required: true, schema: { type: 'integer' } },
],
},
})
const config = (): ToolConfig => ({
operation: 'things_retrieve',
enabled: true,
param_overrides: {
id: {
description: 'Thing ID. If omitted, uses the active project.',
optional: true,
fallback: 'projectId',
cast: 'string-int',
},
},
})
it('resolves the omitted param from state in the handler', () => {
const result = generateToolCode(
'things-retrieve',
config(),
resolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
const collapsed = result.code.replace(/\s+/g, ' ')
expect(collapsed).toContain('const id = params.id ?? await context.stateManager.getProjectId()')
})
it('surfaces the field as optional despite the cast wrapper (z.preprocess strips inner optionality)', () => {
const result = generateToolCode(
'things-retrieve',
config(),
resolved(),
defaultCategory,
makeSpec(),
new Set<string>(),
stubGetQuerySchema
)
// The outer `.optional()` after the preprocess is what makes the agent-facing
// JSON Schema treat the param as optional. Without it, the tool would still
// demand the id the fallback exists to supply.
const collapsed = result.code.replace(/\s+/g, ' ')
expect(collapsed).toContain('.optional()).optional()')
})
})