Skip to content

Commit c9aed7a

Browse files
authored
fix(jsm): accept the numeric pagination the JSM tools actually send (#6386)
* 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. * improvement(forking): widen the fork mapping target picker further 320px still clipped the longest secret keys the picker shows. * 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.
1 parent d2964af commit c9aed7a

5 files changed

Lines changed: 378 additions & 45 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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, ToolResponse } 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+
toolId: string
41+
/** The tool's own `request.body`, captured at its concrete param type by `paginatedCase`. */
42+
buildBody: (params: Record<string, unknown>) => Record<string, unknown>
43+
schema: z.ZodType
44+
extraInputs: Record<string, string>
45+
}
46+
47+
/**
48+
* Captures each tool at its own generic so an incompatible tool/contract pairing is still a type
49+
* error at the call site, rather than being erased by a widened `ToolConfig` in the table type.
50+
*/
51+
function paginatedCase<P, R extends ToolResponse>(
52+
operation: string,
53+
tool: ToolConfig<P, R>,
54+
schema: z.ZodType,
55+
extraInputs: Record<string, string> = {}
56+
): PaginatedCase {
57+
return {
58+
operation,
59+
toolId: tool.id,
60+
buildBody: (params) => {
61+
const bodyFn = tool.request.body
62+
if (!bodyFn) throw new Error(`${tool.id} is missing request.body`)
63+
return bodyFn(params as P) as Record<string, unknown>
64+
},
65+
schema,
66+
extraInputs,
67+
}
68+
}
69+
70+
/**
71+
* Every paginated JSM operation, wired to the tool it resolves to and the contract its route
72+
* parses the body with. This walks the real chain — block `tools.config.params` → the tool's
73+
* `request.body` → the route contract — which is exactly where `jsm_get_comments` broke: the
74+
* tools declare `start`/`limit` as `type: 'number'` while the contract demanded strings.
75+
*/
76+
const PAGINATED_CASES: PaginatedCase[] = [
77+
paginatedCase('get_service_desks', jsmGetServiceDesksTool, jsmServiceDesksBodySchema),
78+
paginatedCase('get_request_types', jsmGetRequestTypesTool, jsmRequestTypesToolBodySchema, {
79+
serviceDeskId: '1',
80+
}),
81+
paginatedCase('get_requests', jsmGetRequestsTool, jsmRequestsBodySchema),
82+
paginatedCase('get_comments', jsmGetCommentsTool, jsmCommentsBodySchema, {
83+
issueIdOrKey: 'SD-123',
84+
}),
85+
paginatedCase('get_customers', jsmGetCustomersTool, jsmCustomersBodySchema, {
86+
serviceDeskId: '1',
87+
}),
88+
paginatedCase('get_organizations', jsmGetOrganizationsTool, jsmServiceDeskScopedBodySchema, {
89+
serviceDeskId: '1',
90+
}),
91+
paginatedCase('get_queues', jsmGetQueuesTool, jsmQueuesBodySchema, { serviceDeskId: '1' }),
92+
paginatedCase('get_sla', jsmGetSlaTool, jsmIssuePaginationBodySchema, { issueIdOrKey: 'SD-123' }),
93+
paginatedCase('get_transitions', jsmGetTransitionsTool, jsmIssuePaginationBodySchema, {
94+
issueIdOrKey: 'SD-123',
95+
}),
96+
paginatedCase('get_participants', jsmGetParticipantsTool, jsmParticipantsBodySchema, {
97+
issueIdOrKey: 'SD-123',
98+
}),
99+
paginatedCase('get_approvals', jsmGetApprovalsTool, jsmApprovalsBodySchema, {
100+
issueIdOrKey: 'SD-123',
101+
}),
102+
]
103+
104+
/** Run a set of block inputs through `tools.config.params`, then through the tool's request body. */
105+
function buildRequestBody(
106+
{ operation, buildBody, extraInputs }: PaginatedCase,
107+
pagination: Record<string, string>
108+
) {
109+
const paramsFn = JiraServiceManagementBlock.tools.config?.params
110+
if (!paramsFn) throw new Error('Block is missing tools.config.params')
111+
112+
const toolParams = paramsFn({
113+
oauthCredential: 'cred-1',
114+
domain: DOMAIN,
115+
operation,
116+
...extraInputs,
117+
...pagination,
118+
})
119+
120+
return buildBody({ ...toolParams, accessToken: ACCESS_TOKEN, domain: DOMAIN })
121+
}
122+
123+
describe.each(PAGINATED_CASES.map((testCase) => [testCase.operation, testCase] as const))(
124+
'JiraServiceManagementBlock %s',
125+
(_operation, testCase) => {
126+
it('resolves to the expected tool', () => {
127+
const toolFn = JiraServiceManagementBlock.tools.config?.tool
128+
expect(toolFn?.({ operation: testCase.operation })).toBe(testCase.toolId)
129+
expect(JiraServiceManagementBlock.tools.access).toContain(testCase.toolId)
130+
})
131+
132+
it('sends a body its route contract accepts when pagination is filled in', () => {
133+
const body = buildRequestBody(testCase, { startIndex: '50', maxResults: '25' })
134+
135+
expect(body.start).toBe(50)
136+
expect(body.limit).toBe(25)
137+
expect(testCase.schema.parse(body)).toMatchObject({ start: '50', limit: '25' })
138+
})
139+
140+
it('sends a body its route contract accepts when pagination is left blank', () => {
141+
const body = buildRequestBody(testCase, {})
142+
143+
expect(body.start).toBeUndefined()
144+
expect(body.limit).toBeUndefined()
145+
expect(() => testCase.schema.parse(body)).not.toThrow()
146+
})
147+
148+
it('drops non-numeric pagination input instead of sending NaN', () => {
149+
const body = buildRequestBody(testCase, { startIndex: 'not-a-number', maxResults: '' })
150+
151+
expect(body.start).toBeUndefined()
152+
expect(body.limit).toBeUndefined()
153+
expect(() => testCase.schema.parse(body)).not.toThrow()
154+
})
155+
}
156+
)
157+
158+
describe('JiraServiceManagementBlock pagination inputs', () => {
159+
it('exposes Start Index and Max Results on exactly the paginated operations', () => {
160+
const operations = PAGINATED_CASES.map(({ operation }) => operation)
161+
162+
for (const id of ['startIndex', 'maxResults']) {
163+
const subBlock = JiraServiceManagementBlock.subBlocks.find((sb) => sb.id === id)
164+
expect(subBlock, `${id} subBlock is missing`).toBeDefined()
165+
expect(subBlock?.mode).toBe('advanced')
166+
expect(subBlock?.condition).toEqual({ field: 'operation', value: operations })
167+
expect(JiraServiceManagementBlock.inputs[id]).toBeDefined()
168+
}
169+
})
170+
})

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 = [
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+
] as const
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' },

apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const NEW_TRIGGER_URL_VALUE = '__new_trigger_url__'
8383
* General). Wide enough to hold a full-length secret key - these are the longest labels the
8484
* picker shows, and clipping them is what makes two same-prefixed keys indistinguishable.
8585
*/
86-
const MAPPING_TARGET_TRIGGER_CLASS = 'w-[320px] flex-shrink-0'
86+
const MAPPING_TARGET_TRIGGER_CLASS = 'w-[380px] flex-shrink-0'
8787

8888
interface DependentBlock {
8989
targetBlockId: string

0 commit comments

Comments
 (0)