From b515c04b01af3a020e99798a059a56830e88df39 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:27:03 -0700 Subject: [PATCH 1/3] fix(jsm): accept the numeric pagination the JSM tools actually send The JSM tools declare start/limit as type: 'number' and the block coerces Max Results with Number.parseInt, but every /api/tools/jsm/* contract typed them as z.string(). Any JSM read with pagination filled in 400'd before reaching Atlassian, and get_queues 400'd unconditionally because the block always sends includeCount as a boolean. Normalize both shapes at the contract boundary, add the missing Start Index block input, and route Max Results through the existing toOptionalInt helper so a non-numeric entry no longer sends NaN. --- .../blocks/jira_service_management.test.ts | 192 ++++++++++++++++++ .../blocks/blocks/jira_service_management.ts | 75 ++++--- .../lib/api/contracts/selectors/jsm.test.ts | 112 ++++++++++ apps/sim/lib/api/contracts/selectors/jsm.ts | 60 ++++-- 4 files changed, 395 insertions(+), 44 deletions(-) create mode 100644 apps/sim/blocks/blocks/jira_service_management.test.ts create mode 100644 apps/sim/lib/api/contracts/selectors/jsm.test.ts diff --git a/apps/sim/blocks/blocks/jira_service_management.test.ts b/apps/sim/blocks/blocks/jira_service_management.test.ts new file mode 100644 index 00000000000..78290f82e57 --- /dev/null +++ b/apps/sim/blocks/blocks/jira_service_management.test.ts @@ -0,0 +1,192 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { z } from 'zod' +import { + jsmApprovalsBodySchema, + jsmCommentsBodySchema, + jsmCustomersBodySchema, + jsmIssuePaginationBodySchema, + jsmParticipantsBodySchema, + jsmQueuesBodySchema, + jsmRequestsBodySchema, + jsmRequestTypesToolBodySchema, + jsmServiceDeskScopedBodySchema, + jsmServiceDesksBodySchema, +} from '@/lib/api/contracts/selectors/jsm' +import { JiraServiceManagementBlock } from '@/blocks/blocks/jira_service_management' +import { + jsmGetApprovalsTool, + jsmGetCommentsTool, + jsmGetCustomersTool, + jsmGetOrganizationsTool, + jsmGetParticipantsTool, + jsmGetQueuesTool, + jsmGetRequestsTool, + jsmGetRequestTypesTool, + jsmGetServiceDesksTool, + jsmGetSlaTool, + jsmGetTransitionsTool, +} from '@/tools/jsm' +import type { ToolConfig } from '@/tools/types' + +const DOMAIN = 'example.atlassian.net' +/** Injected by the executor from the OAuth credential before the tool's `body` runs. */ +const ACCESS_TOKEN = 'token-123' + +interface PaginatedCase { + operation: string + tool: ToolConfig + schema: z.ZodType + extraInputs: Record +} + +/** + * Every paginated JSM operation, wired to the tool it resolves to and the contract its route + * parses the body with. This walks the real chain — block `tools.config.params` → the tool's + * `request.body` → the route contract — which is exactly where `jsm_get_comments` broke: the + * tools declare `start`/`limit` as `type: 'number'` while the contract demanded strings. + */ +const PAGINATED_CASES: PaginatedCase[] = [ + { + operation: 'get_service_desks', + tool: jsmGetServiceDesksTool, + schema: jsmServiceDesksBodySchema, + extraInputs: {}, + }, + { + operation: 'get_request_types', + tool: jsmGetRequestTypesTool, + schema: jsmRequestTypesToolBodySchema, + extraInputs: { serviceDeskId: '1' }, + }, + { + operation: 'get_requests', + tool: jsmGetRequestsTool, + schema: jsmRequestsBodySchema, + extraInputs: {}, + }, + { + operation: 'get_comments', + tool: jsmGetCommentsTool, + schema: jsmCommentsBodySchema, + extraInputs: { issueIdOrKey: 'SD-123' }, + }, + { + operation: 'get_customers', + tool: jsmGetCustomersTool, + schema: jsmCustomersBodySchema, + extraInputs: { serviceDeskId: '1' }, + }, + { + operation: 'get_organizations', + tool: jsmGetOrganizationsTool, + schema: jsmServiceDeskScopedBodySchema, + extraInputs: { serviceDeskId: '1' }, + }, + { + operation: 'get_queues', + tool: jsmGetQueuesTool, + schema: jsmQueuesBodySchema, + extraInputs: { serviceDeskId: '1' }, + }, + { + operation: 'get_sla', + tool: jsmGetSlaTool, + schema: jsmIssuePaginationBodySchema, + extraInputs: { issueIdOrKey: 'SD-123' }, + }, + { + operation: 'get_transitions', + tool: jsmGetTransitionsTool, + schema: jsmIssuePaginationBodySchema, + extraInputs: { issueIdOrKey: 'SD-123' }, + }, + { + operation: 'get_participants', + tool: jsmGetParticipantsTool, + schema: jsmParticipantsBodySchema, + extraInputs: { issueIdOrKey: 'SD-123' }, + }, + { + operation: 'get_approvals', + tool: jsmGetApprovalsTool, + schema: jsmApprovalsBodySchema, + extraInputs: { issueIdOrKey: 'SD-123' }, + }, +] + +/** Run a set of block inputs through `tools.config.params`, then through the tool's request body. */ +function buildRequestBody( + { operation, tool, extraInputs }: PaginatedCase, + pagination: Record +) { + const paramsFn = JiraServiceManagementBlock.tools.config?.params + if (!paramsFn) throw new Error('Block is missing tools.config.params') + + const toolParams = paramsFn({ + oauthCredential: 'cred-1', + domain: DOMAIN, + operation, + ...extraInputs, + ...pagination, + }) + + const bodyFn = tool.request.body + if (!bodyFn) throw new Error(`${tool.id} is missing request.body`) + + return bodyFn({ ...toolParams, accessToken: ACCESS_TOKEN, domain: DOMAIN }) as Record< + string, + unknown + > +} + +describe.each(PAGINATED_CASES.map((testCase) => [testCase.operation, testCase] as const))( + 'JiraServiceManagementBlock %s', + (_operation, testCase) => { + it('resolves to the expected tool', () => { + const toolFn = JiraServiceManagementBlock.tools.config?.tool + expect(toolFn?.({ operation: testCase.operation })).toBe(testCase.tool.id) + expect(JiraServiceManagementBlock.tools.access).toContain(testCase.tool.id) + }) + + it('sends a body its route contract accepts when pagination is filled in', () => { + const body = buildRequestBody(testCase, { startIndex: '50', maxResults: '25' }) + + expect(body.start).toBe(50) + expect(body.limit).toBe(25) + expect(testCase.schema.parse(body)).toMatchObject({ start: '50', limit: '25' }) + }) + + it('sends a body its route contract accepts when pagination is left blank', () => { + const body = buildRequestBody(testCase, {}) + + expect(body.start).toBeUndefined() + expect(body.limit).toBeUndefined() + expect(() => testCase.schema.parse(body)).not.toThrow() + }) + + it('drops non-numeric pagination input instead of sending NaN', () => { + const body = buildRequestBody(testCase, { startIndex: 'not-a-number', maxResults: '' }) + + expect(body.start).toBeUndefined() + expect(body.limit).toBeUndefined() + expect(() => testCase.schema.parse(body)).not.toThrow() + }) + } +) + +describe('JiraServiceManagementBlock pagination inputs', () => { + it('exposes Start Index and Max Results on exactly the paginated operations', () => { + const operations = PAGINATED_CASES.map(({ operation }) => operation) + + for (const id of ['startIndex', 'maxResults']) { + const subBlock = JiraServiceManagementBlock.subBlocks.find((sb) => sb.id === id) + expect(subBlock, `${id} subBlock is missing`).toBeDefined() + expect(subBlock?.mode).toBe('advanced') + expect(subBlock?.condition).toEqual({ field: 'operation', value: operations }) + expect(JiraServiceManagementBlock.inputs[id]).toBeDefined() + } + }) +}) diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index a32841ec48c..505fb33f6f5 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -5,6 +5,21 @@ import { AuthMode, IntegrationType } from '@/blocks/types' import type { JsmResponse } from '@/tools/jsm/types' import { getTrigger } from '@/triggers' +/** Operations that accept Atlassian's `start`/`limit` pagination query params. */ +const PAGINATED_OPERATIONS: string[] = [ + 'get_service_desks', + 'get_request_types', + 'get_requests', + 'get_comments', + 'get_customers', + 'get_organizations', + 'get_queues', + 'get_sla', + 'get_transitions', + 'get_participants', + 'get_approvals', +] + /** * Coerce an optional numeric block input into an integer, returning undefined for * empty or non-numeric values so no `NaN` reaches the API query string. @@ -583,27 +598,21 @@ Return ONLY the comment text - no explanations.`, value: () => 'approve', condition: { field: 'operation', value: 'answer_approval' }, }, + { + id: 'startIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Pagination start index (default: 0)', + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPERATIONS }, + }, { id: 'maxResults', title: 'Max Results', type: 'short-input', placeholder: 'Maximum results (default: 50)', - condition: { - field: 'operation', - value: [ - 'get_service_desks', - 'get_request_types', - 'get_requests', - 'get_comments', - 'get_customers', - 'get_organizations', - 'get_queues', - 'get_sla', - 'get_transitions', - 'get_participants', - 'get_approvals', - ], - }, + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPERATIONS }, }, { id: 'assetSchemaId', @@ -946,7 +955,8 @@ Return ONLY the comment text - no explanations.`, case 'get_service_desks': return { ...baseParams, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'get_request_types': if (!params.serviceDeskId) { @@ -957,7 +967,8 @@ Return ONLY the comment text - no explanations.`, serviceDeskId: params.serviceDeskId, searchQuery: params.searchQuery, groupId: params.groupId, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'create_request': if (!params.serviceDeskId) { @@ -1014,7 +1025,8 @@ Return ONLY the comment text - no explanations.`, requestStatus: params.requestStatus, searchTerm: params.searchTerm, expand: params.expand, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'add_comment': if (!params.issueIdOrKey) { @@ -1037,7 +1049,8 @@ Return ONLY the comment text - no explanations.`, ...baseParams, issueIdOrKey: params.issueIdOrKey, expand: params.expand, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'get_customers': if (!params.serviceDeskId) { @@ -1047,7 +1060,8 @@ Return ONLY the comment text - no explanations.`, ...baseParams, serviceDeskId: params.serviceDeskId, query: params.customerQuery, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'add_customer': { if (!params.serviceDeskId) { @@ -1069,7 +1083,8 @@ Return ONLY the comment text - no explanations.`, return { ...baseParams, serviceDeskId: params.serviceDeskId, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'get_queues': if (!params.serviceDeskId) { @@ -1079,7 +1094,8 @@ Return ONLY the comment text - no explanations.`, ...baseParams, serviceDeskId: params.serviceDeskId, includeCount: params.includeCount === 'true', - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'get_sla': if (!params.issueIdOrKey) { @@ -1088,7 +1104,8 @@ Return ONLY the comment text - no explanations.`, return { ...baseParams, issueIdOrKey: params.issueIdOrKey, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'get_transitions': if (!params.issueIdOrKey) { @@ -1097,7 +1114,8 @@ Return ONLY the comment text - no explanations.`, return { ...baseParams, issueIdOrKey: params.issueIdOrKey, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'transition_request': if (!params.issueIdOrKey) { @@ -1139,7 +1157,8 @@ Return ONLY the comment text - no explanations.`, return { ...baseParams, issueIdOrKey: params.issueIdOrKey, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'add_participants': if (!params.issueIdOrKey) { @@ -1160,7 +1179,8 @@ Return ONLY the comment text - no explanations.`, return { ...baseParams, issueIdOrKey: params.issueIdOrKey, - limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + start: toOptionalInt(params.startIndex), + limit: toOptionalInt(params.maxResults), } case 'answer_approval': if (!params.issueIdOrKey) { @@ -1466,6 +1486,7 @@ Return ONLY the comment text - no explanations.`, requestStatus: { type: 'string', description: 'Request status filter' }, searchTerm: { type: 'string', description: 'Search term for requests' }, includeCount: { type: 'string', description: 'Include issue count for queues' }, + startIndex: { type: 'string', description: 'Pagination start index' }, maxResults: { type: 'string', description: 'Maximum results to return' }, organizationName: { type: 'string', description: 'Organization name' }, organizationId: { type: 'string', description: 'Organization ID' }, diff --git a/apps/sim/lib/api/contracts/selectors/jsm.test.ts b/apps/sim/lib/api/contracts/selectors/jsm.test.ts new file mode 100644 index 00000000000..1d827f42cb7 --- /dev/null +++ b/apps/sim/lib/api/contracts/selectors/jsm.test.ts @@ -0,0 +1,112 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + jsmApprovalsBodySchema, + jsmCommentsBodySchema, + jsmCustomersBodySchema, + jsmIssuePaginationBodySchema, + jsmParticipantsBodySchema, + jsmQueuesBodySchema, + jsmRequestsBodySchema, + jsmRequestTypesToolBodySchema, + jsmServiceDeskScopedBodySchema, + jsmServiceDesksBodySchema, +} from '@/lib/api/contracts/selectors/jsm' + +const credentials = { + domain: 'example.atlassian.net', + accessToken: 'token-123', +} + +/** + * The JSM tools declare `start`/`limit` as `type: 'number'`, so both the agent tool-call path + * and the block's `toOptionalInt` coercion post numbers to these routes. The contract must + * accept them and hand the route a string for `URLSearchParams`. + */ +const paginatedSchemas = [ + ['jsmServiceDesksBodySchema', jsmServiceDesksBodySchema, {}], + ['jsmServiceDeskScopedBodySchema', jsmServiceDeskScopedBodySchema, { serviceDeskId: '1' }], + ['jsmQueuesBodySchema', jsmQueuesBodySchema, { serviceDeskId: '1' }], + ['jsmRequestTypesToolBodySchema', jsmRequestTypesToolBodySchema, { serviceDeskId: '1' }], + ['jsmRequestsBodySchema', jsmRequestsBodySchema, {}], + ['jsmCommentsBodySchema', jsmCommentsBodySchema, { issueIdOrKey: 'SD-123' }], + ['jsmIssuePaginationBodySchema', jsmIssuePaginationBodySchema, { issueIdOrKey: 'SD-123' }], + ['jsmApprovalsBodySchema', jsmApprovalsBodySchema, { action: 'list', issueIdOrKey: 'SD-123' }], + [ + 'jsmParticipantsBodySchema', + jsmParticipantsBodySchema, + { action: 'list', issueIdOrKey: 'SD-123' }, + ], + ['jsmCustomersBodySchema', jsmCustomersBodySchema, { serviceDeskId: '1' }], +] as const + +describe('JSM contract pagination', () => { + it.each(paginatedSchemas)('%s accepts numeric start/limit', (_name, schema, extra) => { + const parsed = schema.parse({ ...credentials, ...extra, start: 50, limit: 25 }) + + expect(parsed.start).toBe('50') + expect(parsed.limit).toBe('25') + }) + + it.each(paginatedSchemas)('%s still accepts string start/limit', (_name, schema, extra) => { + const parsed = schema.parse({ ...credentials, ...extra, start: '50', limit: '25' }) + + expect(parsed.start).toBe('50') + expect(parsed.limit).toBe('25') + }) + + it('rejects numbers outside the int32 range Atlassian documents', () => { + const body = { ...credentials, issueIdOrKey: 'SD-123' } + + expect(() => jsmCommentsBodySchema.parse({ ...body, limit: 2.5 })).toThrow() + expect(() => jsmCommentsBodySchema.parse({ ...body, start: -1 })).toThrow() + expect(() => jsmCommentsBodySchema.parse({ ...body, limit: Number.NaN })).toThrow() + }) + + /** + * The string branch is deliberately unconstrained: the previous schema was a bare + * `z.string()`, so narrowing it would turn bodies that parse today into 400s. + */ + it('still accepts arbitrary strings the previous schema allowed', () => { + const body = { ...credentials, issueIdOrKey: 'SD-123' } + + expect(jsmCommentsBodySchema.parse({ ...body, limit: 'twenty' }).limit).toBe('twenty') + expect(jsmCommentsBodySchema.parse({ ...body, start: '' }).start).toBe('') + expect(jsmCommentsBodySchema.parse({ ...body, start: '-5' }).start).toBe('-5') + }) + + it('leaves omitted pagination undefined', () => { + const parsed = jsmCommentsBodySchema.parse({ ...credentials, issueIdOrKey: 'SD-123' }) + + expect(parsed.start).toBeUndefined() + expect(parsed.limit).toBeUndefined() + }) +}) + +describe('JSM queues includeCount', () => { + /** The tool declares `includeCount` as a boolean and the block always sends one. */ + it.each([ + [true, 'true'], + [false, 'false'], + ])('accepts the boolean %s', (input, expected) => { + const parsed = jsmQueuesBodySchema.parse({ + ...credentials, + serviceDeskId: '1', + includeCount: input, + }) + + expect(parsed.includeCount).toBe(expected) + }) + + it('still accepts the string form sent by hand-authored callers', () => { + const parsed = jsmQueuesBodySchema.parse({ + ...credentials, + serviceDeskId: '1', + includeCount: 'true', + }) + + expect(parsed.includeCount).toBe('true') + }) +}) diff --git a/apps/sim/lib/api/contracts/selectors/jsm.ts b/apps/sim/lib/api/contracts/selectors/jsm.ts index dea80b9344c..310631a1d83 100644 --- a/apps/sim/lib/api/contracts/selectors/jsm.ts +++ b/apps/sim/lib/api/contracts/selectors/jsm.ts @@ -25,24 +25,50 @@ const jsmFormIdField = z.string({ error: 'Form ID is required' }).min(1, 'Form I const jsmIdListSchema = z.union([z.string(), z.array(z.string())]).optional() +/** + * JSM pagination values reach this boundary in two shapes: tools declare `start`/`limit` as + * `type: 'number'` (so agent tool-calls and the block's `Number.parseInt` both send numbers), + * while hand-authored callers send strings. Atlassian takes them as int32 query params, and the + * routes stringify them into a `URLSearchParams`, so normalize both shapes to a string here. + * + * The string branch stays unconstrained so every body the previous `z.string()` schema accepted + * still parses; the newly accepted number branch is bounded to the int32 range Atlassian documents. + */ +const jsmPaginationField = z + .union([ + z.string(), + z + .number() + .int('Pagination values must be whole numbers') + .min(0, 'Pagination values must be 0 or greater'), + ]) + .transform((value) => String(value)) + .optional() + +/** Boolean query flags arrive as booleans from tool params and as `'true'`/`'false'` strings from block dropdowns. */ +const jsmBooleanFlagField = z + .union([z.string(), z.boolean()]) + .transform((value) => String(value)) + .optional() + export const jsmRequestTypesBodySchema = credentialWorkflowDomainBodySchema.extend({ serviceDeskId: z.string().min(1), }) export const jsmServiceDesksBodySchema = jsmBaseBodySchema.extend({ expand: z.string().optional(), - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmServiceDeskScopedBodySchema = jsmBaseBodySchema.extend({ serviceDeskId: jsmServiceDeskIdField, - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmQueuesBodySchema = jsmServiceDeskScopedBodySchema.extend({ - includeCount: z.string().optional(), + includeCount: jsmBooleanFlagField, }) export const jsmRequestTypesToolBodySchema = jsmServiceDeskScopedBodySchema.extend({ @@ -65,8 +91,8 @@ export const jsmRequestsBodySchema = jsmBaseBodySchema.extend({ requestTypeId: z.string().optional(), searchTerm: z.string().optional(), expand: z.string().optional(), - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmRequestBodySchema = jsmBaseBodySchema.extend({ @@ -94,8 +120,8 @@ export const jsmCommentsBodySchema = jsmBaseBodySchema.extend({ isPublic: z.boolean().optional(), internal: z.boolean().optional(), expand: z.string().optional(), - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmTransitionBodySchema = jsmBaseBodySchema.extend({ @@ -108,8 +134,8 @@ export const jsmTransitionBodySchema = jsmBaseBodySchema.extend({ export const jsmIssuePaginationBodySchema = jsmBaseBodySchema.extend({ issueIdOrKey: jsmIssueIdOrKeyField, - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmApprovalsBodySchema = jsmBaseBodySchema.extend({ @@ -117,23 +143,23 @@ export const jsmApprovalsBodySchema = jsmBaseBodySchema.extend({ issueIdOrKey: jsmIssueIdOrKeyField, approvalId: z.string().optional(), decision: z.string().optional(), - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmParticipantsBodySchema = jsmBaseBodySchema.extend({ action: z.string({ error: 'Action is required' }).min(1, 'Action is required'), issueIdOrKey: jsmIssueIdOrKeyField, accountIds: z.union([z.string(), z.array(z.string())]).optional(), - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, }) export const jsmCustomersBodySchema = jsmBaseBodySchema.extend({ serviceDeskId: jsmServiceDeskIdField, query: z.string().optional(), - start: z.string().optional(), - limit: z.string().optional(), + start: jsmPaginationField, + limit: jsmPaginationField, accountIds: jsmIdListSchema, emails: jsmIdListSchema, }) From ba3fd4ccf8d19b422ae7a842a7301c1277fcaeac Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:28:38 -0700 Subject: [PATCH 2/3] improvement(forking): widen the fork mapping target picker further 320px still clipped the longest secret keys the picker shows. --- .../workspace-forking/components/fork-sync/fork-sync-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index e6cf1687ead..a88d06c0d84 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -83,7 +83,7 @@ const NEW_TRIGGER_URL_VALUE = '__new_trigger_url__' * General). Wide enough to hold a full-length secret key - these are the longest labels the * picker shows, and clipping them is what makes two same-prefixed keys indistinguishable. */ -const MAPPING_TARGET_TRIGGER_CLASS = 'w-[320px] flex-shrink-0' +const MAPPING_TARGET_TRIGGER_CLASS = 'w-[380px] flex-shrink-0' interface DependentBlock { targetBlockId: string From b00038273b838896db942293ec93b2c57622b7c1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Aug 2026 12:34:14 -0700 Subject: [PATCH 3/3] fix(jsm): cap pagination at the documented int32 maximum Addresses review: the schema claimed the int32 range but only floored at 0, so values above 2147483647 were forwarded to Atlassian instead of being rejected at Sim's boundary. Also restores the const tuple for the paginated operation list and drops the widened ToolConfig from the test table. --- .../blocks/jira_service_management.test.ts | 134 ++++++++---------- .../blocks/blocks/jira_service_management.ts | 8 +- .../lib/api/contracts/selectors/jsm.test.ts | 3 + apps/sim/lib/api/contracts/selectors/jsm.ts | 3 +- 4 files changed, 65 insertions(+), 83 deletions(-) diff --git a/apps/sim/blocks/blocks/jira_service_management.test.ts b/apps/sim/blocks/blocks/jira_service_management.test.ts index 78290f82e57..291644f55d8 100644 --- a/apps/sim/blocks/blocks/jira_service_management.test.ts +++ b/apps/sim/blocks/blocks/jira_service_management.test.ts @@ -29,7 +29,7 @@ import { jsmGetSlaTool, jsmGetTransitionsTool, } from '@/tools/jsm' -import type { ToolConfig } from '@/tools/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' const DOMAIN = 'example.atlassian.net' /** Injected by the executor from the OAuth credential before the tool's `body` runs. */ @@ -37,11 +37,36 @@ const ACCESS_TOKEN = 'token-123' interface PaginatedCase { operation: string - tool: ToolConfig + toolId: string + /** The tool's own `request.body`, captured at its concrete param type by `paginatedCase`. */ + buildBody: (params: Record) => Record schema: z.ZodType extraInputs: Record } +/** + * Captures each tool at its own generic so an incompatible tool/contract pairing is still a type + * error at the call site, rather than being erased by a widened `ToolConfig` in the table type. + */ +function paginatedCase( + operation: string, + tool: ToolConfig, + schema: z.ZodType, + extraInputs: Record = {} +): PaginatedCase { + return { + operation, + toolId: tool.id, + buildBody: (params) => { + const bodyFn = tool.request.body + if (!bodyFn) throw new Error(`${tool.id} is missing request.body`) + return bodyFn(params as P) as Record + }, + schema, + extraInputs, + } +} + /** * Every paginated JSM operation, wired to the tool it resolves to and the contract its route * parses the body with. This walks the real chain — block `tools.config.params` → the tool's @@ -49,77 +74,36 @@ interface PaginatedCase { * tools declare `start`/`limit` as `type: 'number'` while the contract demanded strings. */ const PAGINATED_CASES: PaginatedCase[] = [ - { - operation: 'get_service_desks', - tool: jsmGetServiceDesksTool, - schema: jsmServiceDesksBodySchema, - extraInputs: {}, - }, - { - operation: 'get_request_types', - tool: jsmGetRequestTypesTool, - schema: jsmRequestTypesToolBodySchema, - extraInputs: { serviceDeskId: '1' }, - }, - { - operation: 'get_requests', - tool: jsmGetRequestsTool, - schema: jsmRequestsBodySchema, - extraInputs: {}, - }, - { - operation: 'get_comments', - tool: jsmGetCommentsTool, - schema: jsmCommentsBodySchema, - extraInputs: { issueIdOrKey: 'SD-123' }, - }, - { - operation: 'get_customers', - tool: jsmGetCustomersTool, - schema: jsmCustomersBodySchema, - extraInputs: { serviceDeskId: '1' }, - }, - { - operation: 'get_organizations', - tool: jsmGetOrganizationsTool, - schema: jsmServiceDeskScopedBodySchema, - extraInputs: { serviceDeskId: '1' }, - }, - { - operation: 'get_queues', - tool: jsmGetQueuesTool, - schema: jsmQueuesBodySchema, - extraInputs: { serviceDeskId: '1' }, - }, - { - operation: 'get_sla', - tool: jsmGetSlaTool, - schema: jsmIssuePaginationBodySchema, - extraInputs: { issueIdOrKey: 'SD-123' }, - }, - { - operation: 'get_transitions', - tool: jsmGetTransitionsTool, - schema: jsmIssuePaginationBodySchema, - extraInputs: { issueIdOrKey: 'SD-123' }, - }, - { - operation: 'get_participants', - tool: jsmGetParticipantsTool, - schema: jsmParticipantsBodySchema, - extraInputs: { issueIdOrKey: 'SD-123' }, - }, - { - operation: 'get_approvals', - tool: jsmGetApprovalsTool, - schema: jsmApprovalsBodySchema, - extraInputs: { issueIdOrKey: 'SD-123' }, - }, + paginatedCase('get_service_desks', jsmGetServiceDesksTool, jsmServiceDesksBodySchema), + paginatedCase('get_request_types', jsmGetRequestTypesTool, jsmRequestTypesToolBodySchema, { + serviceDeskId: '1', + }), + paginatedCase('get_requests', jsmGetRequestsTool, jsmRequestsBodySchema), + paginatedCase('get_comments', jsmGetCommentsTool, jsmCommentsBodySchema, { + issueIdOrKey: 'SD-123', + }), + paginatedCase('get_customers', jsmGetCustomersTool, jsmCustomersBodySchema, { + serviceDeskId: '1', + }), + paginatedCase('get_organizations', jsmGetOrganizationsTool, jsmServiceDeskScopedBodySchema, { + serviceDeskId: '1', + }), + paginatedCase('get_queues', jsmGetQueuesTool, jsmQueuesBodySchema, { serviceDeskId: '1' }), + paginatedCase('get_sla', jsmGetSlaTool, jsmIssuePaginationBodySchema, { issueIdOrKey: 'SD-123' }), + paginatedCase('get_transitions', jsmGetTransitionsTool, jsmIssuePaginationBodySchema, { + issueIdOrKey: 'SD-123', + }), + paginatedCase('get_participants', jsmGetParticipantsTool, jsmParticipantsBodySchema, { + issueIdOrKey: 'SD-123', + }), + paginatedCase('get_approvals', jsmGetApprovalsTool, jsmApprovalsBodySchema, { + issueIdOrKey: 'SD-123', + }), ] /** Run a set of block inputs through `tools.config.params`, then through the tool's request body. */ function buildRequestBody( - { operation, tool, extraInputs }: PaginatedCase, + { operation, buildBody, extraInputs }: PaginatedCase, pagination: Record ) { const paramsFn = JiraServiceManagementBlock.tools.config?.params @@ -133,13 +117,7 @@ function buildRequestBody( ...pagination, }) - const bodyFn = tool.request.body - if (!bodyFn) throw new Error(`${tool.id} is missing request.body`) - - return bodyFn({ ...toolParams, accessToken: ACCESS_TOKEN, domain: DOMAIN }) as Record< - string, - unknown - > + return buildBody({ ...toolParams, accessToken: ACCESS_TOKEN, domain: DOMAIN }) } describe.each(PAGINATED_CASES.map((testCase) => [testCase.operation, testCase] as const))( @@ -147,8 +125,8 @@ describe.each(PAGINATED_CASES.map((testCase) => [testCase.operation, testCase] a (_operation, testCase) => { it('resolves to the expected tool', () => { const toolFn = JiraServiceManagementBlock.tools.config?.tool - expect(toolFn?.({ operation: testCase.operation })).toBe(testCase.tool.id) - expect(JiraServiceManagementBlock.tools.access).toContain(testCase.tool.id) + expect(toolFn?.({ operation: testCase.operation })).toBe(testCase.toolId) + expect(JiraServiceManagementBlock.tools.access).toContain(testCase.toolId) }) it('sends a body its route contract accepts when pagination is filled in', () => { diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 505fb33f6f5..82b69c1239d 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -6,7 +6,7 @@ import type { JsmResponse } from '@/tools/jsm/types' import { getTrigger } from '@/triggers' /** Operations that accept Atlassian's `start`/`limit` pagination query params. */ -const PAGINATED_OPERATIONS: string[] = [ +const PAGINATED_OPERATIONS = [ 'get_service_desks', 'get_request_types', 'get_requests', @@ -18,7 +18,7 @@ const PAGINATED_OPERATIONS: string[] = [ 'get_transitions', 'get_participants', 'get_approvals', -] +] as const /** * Coerce an optional numeric block input into an integer, returning undefined for @@ -604,7 +604,7 @@ Return ONLY the comment text - no explanations.`, type: 'short-input', placeholder: 'Pagination start index (default: 0)', mode: 'advanced', - condition: { field: 'operation', value: PAGINATED_OPERATIONS }, + condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, }, { id: 'maxResults', @@ -612,7 +612,7 @@ Return ONLY the comment text - no explanations.`, type: 'short-input', placeholder: 'Maximum results (default: 50)', mode: 'advanced', - condition: { field: 'operation', value: PAGINATED_OPERATIONS }, + condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, }, { id: 'assetSchemaId', diff --git a/apps/sim/lib/api/contracts/selectors/jsm.test.ts b/apps/sim/lib/api/contracts/selectors/jsm.test.ts index 1d827f42cb7..d241e125564 100644 --- a/apps/sim/lib/api/contracts/selectors/jsm.test.ts +++ b/apps/sim/lib/api/contracts/selectors/jsm.test.ts @@ -63,6 +63,9 @@ describe('JSM contract pagination', () => { expect(() => jsmCommentsBodySchema.parse({ ...body, limit: 2.5 })).toThrow() expect(() => jsmCommentsBodySchema.parse({ ...body, start: -1 })).toThrow() expect(() => jsmCommentsBodySchema.parse({ ...body, limit: Number.NaN })).toThrow() + expect(() => jsmCommentsBodySchema.parse({ ...body, limit: 2147483648 })).toThrow() + expect(jsmCommentsBodySchema.parse({ ...body, limit: 2147483647 }).limit).toBe('2147483647') + expect(jsmCommentsBodySchema.parse({ ...body, start: 0 }).start).toBe('0') }) /** diff --git a/apps/sim/lib/api/contracts/selectors/jsm.ts b/apps/sim/lib/api/contracts/selectors/jsm.ts index 310631a1d83..7dd2706f3db 100644 --- a/apps/sim/lib/api/contracts/selectors/jsm.ts +++ b/apps/sim/lib/api/contracts/selectors/jsm.ts @@ -40,7 +40,8 @@ const jsmPaginationField = z z .number() .int('Pagination values must be whole numbers') - .min(0, 'Pagination values must be 0 or greater'), + .min(0, 'Pagination values must be 0 or greater') + .max(2147483647, 'Pagination values must be within the int32 range'), ]) .transform((value) => String(value)) .optional()