Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions apps/sim/blocks/blocks/jira_service_management.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @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, ToolResponse } 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 {
Comment thread
waleedlatif1 marked this conversation as resolved.
operation: string
toolId: string
/** The tool's own `request.body`, captured at its concrete param type by `paginatedCase`. */
buildBody: (params: Record<string, unknown>) => Record<string, unknown>
schema: z.ZodType
extraInputs: Record<string, string>
}

/**
* 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<P, R extends ToolResponse>(
operation: string,
tool: ToolConfig<P, R>,
schema: z.ZodType,
extraInputs: Record<string, string> = {}
): 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<string, unknown>
},
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
* `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[] = [
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, buildBody, extraInputs }: PaginatedCase,
pagination: Record<string, string>
) {
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,
})

return buildBody({ ...toolParams, accessToken: ACCESS_TOKEN, domain: DOMAIN })
}

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.toolId)
expect(JiraServiceManagementBlock.tools.access).toContain(testCase.toolId)
})

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()
}
})
})
75 changes: 48 additions & 27 deletions apps/sim/blocks/blocks/jira_service_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
'get_service_desks',
'get_request_types',
'get_requests',
'get_comments',
'get_customers',
'get_organizations',
'get_queues',
'get_sla',
'get_transitions',
'get_participants',
'get_approvals',
] as const

/**
* 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.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading