Skip to content

Commit b515c04

Browse files
committed
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.
1 parent d2964af commit b515c04

4 files changed

Lines changed: 395 additions & 44 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import type { z } from 'zod'
6+
import {
7+
jsmApprovalsBodySchema,
8+
jsmCommentsBodySchema,
9+
jsmCustomersBodySchema,
10+
jsmIssuePaginationBodySchema,
11+
jsmParticipantsBodySchema,
12+
jsmQueuesBodySchema,
13+
jsmRequestsBodySchema,
14+
jsmRequestTypesToolBodySchema,
15+
jsmServiceDeskScopedBodySchema,
16+
jsmServiceDesksBodySchema,
17+
} from '@/lib/api/contracts/selectors/jsm'
18+
import { JiraServiceManagementBlock } from '@/blocks/blocks/jira_service_management'
19+
import {
20+
jsmGetApprovalsTool,
21+
jsmGetCommentsTool,
22+
jsmGetCustomersTool,
23+
jsmGetOrganizationsTool,
24+
jsmGetParticipantsTool,
25+
jsmGetQueuesTool,
26+
jsmGetRequestsTool,
27+
jsmGetRequestTypesTool,
28+
jsmGetServiceDesksTool,
29+
jsmGetSlaTool,
30+
jsmGetTransitionsTool,
31+
} from '@/tools/jsm'
32+
import type { ToolConfig } from '@/tools/types'
33+
34+
const DOMAIN = 'example.atlassian.net'
35+
/** Injected by the executor from the OAuth credential before the tool's `body` runs. */
36+
const ACCESS_TOKEN = 'token-123'
37+
38+
interface PaginatedCase {
39+
operation: string
40+
tool: ToolConfig<any, any>
41+
schema: z.ZodType
42+
extraInputs: Record<string, string>
43+
}
44+
45+
/**
46+
* Every paginated JSM operation, wired to the tool it resolves to and the contract its route
47+
* parses the body with. This walks the real chain — block `tools.config.params` → the tool's
48+
* `request.body` → the route contract — which is exactly where `jsm_get_comments` broke: the
49+
* tools declare `start`/`limit` as `type: 'number'` while the contract demanded strings.
50+
*/
51+
const PAGINATED_CASES: PaginatedCase[] = [
52+
{
53+
operation: 'get_service_desks',
54+
tool: jsmGetServiceDesksTool,
55+
schema: jsmServiceDesksBodySchema,
56+
extraInputs: {},
57+
},
58+
{
59+
operation: 'get_request_types',
60+
tool: jsmGetRequestTypesTool,
61+
schema: jsmRequestTypesToolBodySchema,
62+
extraInputs: { serviceDeskId: '1' },
63+
},
64+
{
65+
operation: 'get_requests',
66+
tool: jsmGetRequestsTool,
67+
schema: jsmRequestsBodySchema,
68+
extraInputs: {},
69+
},
70+
{
71+
operation: 'get_comments',
72+
tool: jsmGetCommentsTool,
73+
schema: jsmCommentsBodySchema,
74+
extraInputs: { issueIdOrKey: 'SD-123' },
75+
},
76+
{
77+
operation: 'get_customers',
78+
tool: jsmGetCustomersTool,
79+
schema: jsmCustomersBodySchema,
80+
extraInputs: { serviceDeskId: '1' },
81+
},
82+
{
83+
operation: 'get_organizations',
84+
tool: jsmGetOrganizationsTool,
85+
schema: jsmServiceDeskScopedBodySchema,
86+
extraInputs: { serviceDeskId: '1' },
87+
},
88+
{
89+
operation: 'get_queues',
90+
tool: jsmGetQueuesTool,
91+
schema: jsmQueuesBodySchema,
92+
extraInputs: { serviceDeskId: '1' },
93+
},
94+
{
95+
operation: 'get_sla',
96+
tool: jsmGetSlaTool,
97+
schema: jsmIssuePaginationBodySchema,
98+
extraInputs: { issueIdOrKey: 'SD-123' },
99+
},
100+
{
101+
operation: 'get_transitions',
102+
tool: jsmGetTransitionsTool,
103+
schema: jsmIssuePaginationBodySchema,
104+
extraInputs: { issueIdOrKey: 'SD-123' },
105+
},
106+
{
107+
operation: 'get_participants',
108+
tool: jsmGetParticipantsTool,
109+
schema: jsmParticipantsBodySchema,
110+
extraInputs: { issueIdOrKey: 'SD-123' },
111+
},
112+
{
113+
operation: 'get_approvals',
114+
tool: jsmGetApprovalsTool,
115+
schema: jsmApprovalsBodySchema,
116+
extraInputs: { issueIdOrKey: 'SD-123' },
117+
},
118+
]
119+
120+
/** Run a set of block inputs through `tools.config.params`, then through the tool's request body. */
121+
function buildRequestBody(
122+
{ operation, tool, extraInputs }: PaginatedCase,
123+
pagination: Record<string, string>
124+
) {
125+
const paramsFn = JiraServiceManagementBlock.tools.config?.params
126+
if (!paramsFn) throw new Error('Block is missing tools.config.params')
127+
128+
const toolParams = paramsFn({
129+
oauthCredential: 'cred-1',
130+
domain: DOMAIN,
131+
operation,
132+
...extraInputs,
133+
...pagination,
134+
})
135+
136+
const bodyFn = tool.request.body
137+
if (!bodyFn) throw new Error(`${tool.id} is missing request.body`)
138+
139+
return bodyFn({ ...toolParams, accessToken: ACCESS_TOKEN, domain: DOMAIN }) as Record<
140+
string,
141+
unknown
142+
>
143+
}
144+
145+
describe.each(PAGINATED_CASES.map((testCase) => [testCase.operation, testCase] as const))(
146+
'JiraServiceManagementBlock %s',
147+
(_operation, testCase) => {
148+
it('resolves to the expected tool', () => {
149+
const toolFn = JiraServiceManagementBlock.tools.config?.tool
150+
expect(toolFn?.({ operation: testCase.operation })).toBe(testCase.tool.id)
151+
expect(JiraServiceManagementBlock.tools.access).toContain(testCase.tool.id)
152+
})
153+
154+
it('sends a body its route contract accepts when pagination is filled in', () => {
155+
const body = buildRequestBody(testCase, { startIndex: '50', maxResults: '25' })
156+
157+
expect(body.start).toBe(50)
158+
expect(body.limit).toBe(25)
159+
expect(testCase.schema.parse(body)).toMatchObject({ start: '50', limit: '25' })
160+
})
161+
162+
it('sends a body its route contract accepts when pagination is left blank', () => {
163+
const body = buildRequestBody(testCase, {})
164+
165+
expect(body.start).toBeUndefined()
166+
expect(body.limit).toBeUndefined()
167+
expect(() => testCase.schema.parse(body)).not.toThrow()
168+
})
169+
170+
it('drops non-numeric pagination input instead of sending NaN', () => {
171+
const body = buildRequestBody(testCase, { startIndex: 'not-a-number', maxResults: '' })
172+
173+
expect(body.start).toBeUndefined()
174+
expect(body.limit).toBeUndefined()
175+
expect(() => testCase.schema.parse(body)).not.toThrow()
176+
})
177+
}
178+
)
179+
180+
describe('JiraServiceManagementBlock pagination inputs', () => {
181+
it('exposes Start Index and Max Results on exactly the paginated operations', () => {
182+
const operations = PAGINATED_CASES.map(({ operation }) => operation)
183+
184+
for (const id of ['startIndex', 'maxResults']) {
185+
const subBlock = JiraServiceManagementBlock.subBlocks.find((sb) => sb.id === id)
186+
expect(subBlock, `${id} subBlock is missing`).toBeDefined()
187+
expect(subBlock?.mode).toBe('advanced')
188+
expect(subBlock?.condition).toEqual({ field: 'operation', value: operations })
189+
expect(JiraServiceManagementBlock.inputs[id]).toBeDefined()
190+
}
191+
})
192+
})

apps/sim/blocks/blocks/jira_service_management.ts

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ import { AuthMode, IntegrationType } from '@/blocks/types'
55
import type { JsmResponse } from '@/tools/jsm/types'
66
import { getTrigger } from '@/triggers'
77

8+
/** Operations that accept Atlassian's `start`/`limit` pagination query params. */
9+
const PAGINATED_OPERATIONS: string[] = [
10+
'get_service_desks',
11+
'get_request_types',
12+
'get_requests',
13+
'get_comments',
14+
'get_customers',
15+
'get_organizations',
16+
'get_queues',
17+
'get_sla',
18+
'get_transitions',
19+
'get_participants',
20+
'get_approvals',
21+
]
22+
823
/**
924
* Coerce an optional numeric block input into an integer, returning undefined for
1025
* empty or non-numeric values so no `NaN` reaches the API query string.
@@ -583,27 +598,21 @@ Return ONLY the comment text - no explanations.`,
583598
value: () => 'approve',
584599
condition: { field: 'operation', value: 'answer_approval' },
585600
},
601+
{
602+
id: 'startIndex',
603+
title: 'Start Index',
604+
type: 'short-input',
605+
placeholder: 'Pagination start index (default: 0)',
606+
mode: 'advanced',
607+
condition: { field: 'operation', value: PAGINATED_OPERATIONS },
608+
},
586609
{
587610
id: 'maxResults',
588611
title: 'Max Results',
589612
type: 'short-input',
590613
placeholder: 'Maximum results (default: 50)',
591-
condition: {
592-
field: 'operation',
593-
value: [
594-
'get_service_desks',
595-
'get_request_types',
596-
'get_requests',
597-
'get_comments',
598-
'get_customers',
599-
'get_organizations',
600-
'get_queues',
601-
'get_sla',
602-
'get_transitions',
603-
'get_participants',
604-
'get_approvals',
605-
],
606-
},
614+
mode: 'advanced',
615+
condition: { field: 'operation', value: PAGINATED_OPERATIONS },
607616
},
608617
{
609618
id: 'assetSchemaId',
@@ -946,7 +955,8 @@ Return ONLY the comment text - no explanations.`,
946955
case 'get_service_desks':
947956
return {
948957
...baseParams,
949-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
958+
start: toOptionalInt(params.startIndex),
959+
limit: toOptionalInt(params.maxResults),
950960
}
951961
case 'get_request_types':
952962
if (!params.serviceDeskId) {
@@ -957,7 +967,8 @@ Return ONLY the comment text - no explanations.`,
957967
serviceDeskId: params.serviceDeskId,
958968
searchQuery: params.searchQuery,
959969
groupId: params.groupId,
960-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
970+
start: toOptionalInt(params.startIndex),
971+
limit: toOptionalInt(params.maxResults),
961972
}
962973
case 'create_request':
963974
if (!params.serviceDeskId) {
@@ -1014,7 +1025,8 @@ Return ONLY the comment text - no explanations.`,
10141025
requestStatus: params.requestStatus,
10151026
searchTerm: params.searchTerm,
10161027
expand: params.expand,
1017-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1028+
start: toOptionalInt(params.startIndex),
1029+
limit: toOptionalInt(params.maxResults),
10181030
}
10191031
case 'add_comment':
10201032
if (!params.issueIdOrKey) {
@@ -1037,7 +1049,8 @@ Return ONLY the comment text - no explanations.`,
10371049
...baseParams,
10381050
issueIdOrKey: params.issueIdOrKey,
10391051
expand: params.expand,
1040-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1052+
start: toOptionalInt(params.startIndex),
1053+
limit: toOptionalInt(params.maxResults),
10411054
}
10421055
case 'get_customers':
10431056
if (!params.serviceDeskId) {
@@ -1047,7 +1060,8 @@ Return ONLY the comment text - no explanations.`,
10471060
...baseParams,
10481061
serviceDeskId: params.serviceDeskId,
10491062
query: params.customerQuery,
1050-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1063+
start: toOptionalInt(params.startIndex),
1064+
limit: toOptionalInt(params.maxResults),
10511065
}
10521066
case 'add_customer': {
10531067
if (!params.serviceDeskId) {
@@ -1069,7 +1083,8 @@ Return ONLY the comment text - no explanations.`,
10691083
return {
10701084
...baseParams,
10711085
serviceDeskId: params.serviceDeskId,
1072-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1086+
start: toOptionalInt(params.startIndex),
1087+
limit: toOptionalInt(params.maxResults),
10731088
}
10741089
case 'get_queues':
10751090
if (!params.serviceDeskId) {
@@ -1079,7 +1094,8 @@ Return ONLY the comment text - no explanations.`,
10791094
...baseParams,
10801095
serviceDeskId: params.serviceDeskId,
10811096
includeCount: params.includeCount === 'true',
1082-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1097+
start: toOptionalInt(params.startIndex),
1098+
limit: toOptionalInt(params.maxResults),
10831099
}
10841100
case 'get_sla':
10851101
if (!params.issueIdOrKey) {
@@ -1088,7 +1104,8 @@ Return ONLY the comment text - no explanations.`,
10881104
return {
10891105
...baseParams,
10901106
issueIdOrKey: params.issueIdOrKey,
1091-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1107+
start: toOptionalInt(params.startIndex),
1108+
limit: toOptionalInt(params.maxResults),
10921109
}
10931110
case 'get_transitions':
10941111
if (!params.issueIdOrKey) {
@@ -1097,7 +1114,8 @@ Return ONLY the comment text - no explanations.`,
10971114
return {
10981115
...baseParams,
10991116
issueIdOrKey: params.issueIdOrKey,
1100-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1117+
start: toOptionalInt(params.startIndex),
1118+
limit: toOptionalInt(params.maxResults),
11011119
}
11021120
case 'transition_request':
11031121
if (!params.issueIdOrKey) {
@@ -1139,7 +1157,8 @@ Return ONLY the comment text - no explanations.`,
11391157
return {
11401158
...baseParams,
11411159
issueIdOrKey: params.issueIdOrKey,
1142-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1160+
start: toOptionalInt(params.startIndex),
1161+
limit: toOptionalInt(params.maxResults),
11431162
}
11441163
case 'add_participants':
11451164
if (!params.issueIdOrKey) {
@@ -1160,7 +1179,8 @@ Return ONLY the comment text - no explanations.`,
11601179
return {
11611180
...baseParams,
11621181
issueIdOrKey: params.issueIdOrKey,
1163-
limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined,
1182+
start: toOptionalInt(params.startIndex),
1183+
limit: toOptionalInt(params.maxResults),
11641184
}
11651185
case 'answer_approval':
11661186
if (!params.issueIdOrKey) {
@@ -1466,6 +1486,7 @@ Return ONLY the comment text - no explanations.`,
14661486
requestStatus: { type: 'string', description: 'Request status filter' },
14671487
searchTerm: { type: 'string', description: 'Search term for requests' },
14681488
includeCount: { type: 'string', description: 'Include issue count for queues' },
1489+
startIndex: { type: 'string', description: 'Pagination start index' },
14691490
maxResults: { type: 'string', description: 'Maximum results to return' },
14701491
organizationName: { type: 'string', description: 'Organization name' },
14711492
organizationId: { type: 'string', description: 'Organization ID' },

0 commit comments

Comments
 (0)