Skip to content

Commit f568fee

Browse files
committed
fix(greenhouse): reproduce incompatible operation carryover
1 parent bc2f74b commit f568fee

3 files changed

Lines changed: 211 additions & 14 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { getTransitiveSubBlockDependents } from '@/lib/workflows/subblocks/dependencies'
2+
import type { StoredTool } from '@/lib/workflows/tool-input/types'
23
import { getBlock } from '@/blocks/registry'
4+
import type { ToolMetadata } from '@/tools/metadata'
5+
import { isUserFacingToolParam } from '@/tools/params'
36

47
/**
58
* Clear every TRANSITIVE `dependsOn` descendant of `changedParamId` in a nested tool's params,
@@ -23,3 +26,20 @@ export function clearDependentToolParams(
2326
}
2427
return next ?? params
2528
}
29+
30+
/** Keeps user-configured inputs that the newly selected operation accepts. */
31+
export function getParamsForToolOperation(
32+
tool: Pick<StoredTool, 'type' | 'params'>,
33+
nextTool: ToolMetadata
34+
): Record<string, string> {
35+
const newParamIds = new Set(
36+
Object.entries(nextTool.params ?? {})
37+
.filter(([, param]) => isUserFacingToolParam(param))
38+
.map(([paramId]) => paramId)
39+
)
40+
const preservedParams: Record<string, string> = {}
41+
Object.entries(tool.params || {}).forEach(([paramId, value]) => {
42+
if (newParamIds.has(paramId) && value) preservedParams[paramId] = value
43+
})
44+
return preservedParams
45+
}

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx‎

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
6060
import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer'
6161
import { ToolUsageControl } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control'
62-
import { clearDependentToolParams } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
62+
import {
63+
clearDependentToolParams,
64+
getParamsForToolOperation,
65+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
6366
import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types'
6467
import {
6568
isAgentToolBlock,
@@ -111,7 +114,6 @@ import {
111114
formatParameterLabel,
112115
getSubBlocksForToolInput,
113116
getToolIdForOperation,
114-
isUserFacingToolParam,
115117
type SubBlocksForToolInput,
116118
} from '@/tools/params'
117119

@@ -979,18 +981,7 @@ export const ToolInput = memo(function ToolInput({
979981
return
980982
}
981983

982-
const newParamIds = new Set(
983-
Object.entries(newToolConfig.params ?? {})
984-
.filter(([, param]) => isUserFacingToolParam(param))
985-
.map(([paramId]) => paramId)
986-
)
987-
988-
const preservedParams: Record<string, string> = {}
989-
Object.entries(tool.params || {}).forEach(([paramId, value]) => {
990-
if (newParamIds.has(paramId) && value) {
991-
preservedParams[paramId] = value
992-
}
993-
})
984+
const preservedParams = getParamsForToolOperation(tool, newToolConfig)
994985

995986
if (tool.type === 'jira') {
996987
const subBlockStore = useSubBlockStore.getState()

‎apps/sim/providers/utils.test.ts‎

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ vi.mock('@/lib/internal/workflows/read-tool-enrichment', () => ({
1111
readWorkflowMetadataForTool: workflowMetadataMocks.readWorkflowMetadataForTool,
1212
}))
1313

14+
import { getParamsForToolOperation } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
1415
import { GreenhouseV2Block } from '@/blocks/blocks/greenhouse'
1516
import { IntuneBlock } from '@/blocks/blocks/intune'
1617
import { MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel'
1718
import { RampBlock } from '@/blocks/blocks/ramp'
1819
import { RevenueCatBlock } from '@/blocks/blocks/revenuecat'
1920
import { VideoGeneratorV3Block } from '@/blocks/blocks/video_generator'
21+
import * as blockRegistry from '@/blocks/registry'
2022
import { normalizeFileInput } from '@/blocks/utils'
2123
import {
2224
findProviderFromModel,
@@ -45,15 +47,21 @@ import {
4547
} from '@/providers/utils'
4648
import { useProvidersStore } from '@/stores/providers/store'
4749
import { greenhouseConvertProspectV2Tool } from '@/tools/greenhouse/convert_prospect_v2'
50+
import { greenhouseCreateApplicationV2Tool } from '@/tools/greenhouse/create_application_v2'
51+
import { greenhouseHireApplicationV2Tool } from '@/tools/greenhouse/hire_application_v2'
4852
import { greenhouseListCandidatesV2Tool } from '@/tools/greenhouse/list_candidates_v2'
4953
import { greenhouseListJobOwnersV2Tool } from '@/tools/greenhouse/list_job_owners_v2'
5054
import { greenhouseListJobsV2Tool } from '@/tools/greenhouse/list_jobs_v2'
5155
import { greenhouseListNotesV2Tool } from '@/tools/greenhouse/list_notes_v2'
5256
import { greenhouseListOffersV2Tool } from '@/tools/greenhouse/list_offers_v2'
5357
import type {
5458
GreenhouseConvertProspectV2Params,
59+
GreenhouseCreateApplicationV2Params,
60+
GreenhouseHireApplicationV2Params,
5561
GreenhouseListCandidatesV2Params,
62+
GreenhouseUpdateApplicationV2Params,
5663
} from '@/tools/greenhouse/types'
64+
import { greenhouseUpdateApplicationV2Tool } from '@/tools/greenhouse/update_application_v2'
5765
import { intuneListDetectedAppsTool } from '@/tools/intune/list-detected-apps'
5866
import { intuneListDevicesTool } from '@/tools/intune/list-devices'
5967
import * as toolMetadata from '@/tools/metadata'
@@ -71,6 +79,184 @@ const mockGetRotatingApiKey = vi.fn().mockReturnValue('rotating-server-key')
7179
const originalRequire = module.require
7280

7381
describe('integration filters through agent execution', () => {
82+
it.each([
83+
{
84+
operation: 'list_jobs',
85+
field: 'status',
86+
staleValue: 'active',
87+
tool: greenhouseListJobsV2Tool,
88+
},
89+
{
90+
operation: 'list_job_owners',
91+
field: 'type',
92+
staleValue: 'NOTE',
93+
tool: greenhouseListJobOwnersV2Tool,
94+
},
95+
{
96+
operation: 'list_notes',
97+
field: 'visibility',
98+
staleValue: 'private',
99+
tool: greenhouseListNotesV2Tool,
100+
},
101+
{
102+
operation: 'list_candidates',
103+
field: 'created_at',
104+
staleValue: '2026-01-01',
105+
tool: greenhouseListCandidatesV2Tool,
106+
},
107+
])(
108+
'clears incompatible $field when switching to $operation',
109+
async ({ operation, field, staleValue, tool }) => {
110+
vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block)
111+
const previousParams = {
112+
[field]: staleValue,
113+
per_page: '25',
114+
credential: 'fixture-credential',
115+
}
116+
const params = getParamsForToolOperation(
117+
{ type: 'greenhouse_v2', params: previousParams },
118+
tool
119+
)
120+
const providerTool = await transformBlockTool(
121+
{ type: 'greenhouse_v2', params },
122+
{
123+
selectedOperation: operation,
124+
getAllBlocks: () => [GreenhouseV2Block],
125+
getTool: () => tool,
126+
}
127+
)
128+
if (!providerTool) throw new Error('Greenhouse agent tool was not created')
129+
const { executionParams } = prepareToolExecution(providerTool, {}, {}, undefined)
130+
const url =
131+
typeof tool.request.url === 'function'
132+
? tool.request.url({ ...executionParams, accessToken: 'fixture-token' })
133+
: tool.request.url
134+
expect(new URL(url).searchParams.has(field)).toBe(false)
135+
expect(new URL(url).searchParams.get('per_page')).toBe('25')
136+
expect(params.credential).toBe('fixture-credential')
137+
expect(previousParams[field]).toBe(staleValue)
138+
}
139+
)
140+
141+
it('uses the newly supplied application target and fields after leaving candidate operations', async () => {
142+
const tool = greenhouseUpdateApplicationV2Tool
143+
vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block)
144+
const params = getParamsForToolOperation(
145+
{
146+
type: 'greenhouse_v2',
147+
params: {
148+
id: '42',
149+
custom_fields: '[{"custom_field_id":7,"value":"candidate value"}]',
150+
source_id: '81',
151+
},
152+
},
153+
tool
154+
)
155+
const providerTool = await transformBlockTool(
156+
{ type: 'greenhouse_v2', params },
157+
{
158+
selectedOperation: 'update_application',
159+
getAllBlocks: () => [GreenhouseV2Block],
160+
getTool: () => tool,
161+
}
162+
)
163+
if (!providerTool) throw new Error('Greenhouse agent tool was not created')
164+
const { executionParams } = prepareToolExecution(providerTool, { id: 99 }, {}, undefined)
165+
const execution = {
166+
...executionParams,
167+
accessToken: 'fixture-token',
168+
} as GreenhouseUpdateApplicationV2Params
169+
const url =
170+
typeof tool.request.url === 'function' ? tool.request.url(execution) : tool.request.url
171+
expect(new URL(url).pathname).toBe('/v3/applications/99')
172+
expect(tool.request.body?.(execution)).toEqual({ source_id: 81 })
173+
})
174+
175+
it('does not carry a list-filter job CSV into a prospect creation', async () => {
176+
const tool = greenhouseCreateApplicationV2Tool
177+
vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block)
178+
const params = getParamsForToolOperation(
179+
{
180+
type: 'greenhouse_v2',
181+
params: {
182+
job_ids: '11,12',
183+
candidate_id: '42',
184+
},
185+
},
186+
tool
187+
)
188+
const providerTool = await transformBlockTool(
189+
{ type: 'greenhouse_v2', params },
190+
{
191+
selectedOperation: 'create_application',
192+
getAllBlocks: () => [GreenhouseV2Block],
193+
getTool: () => tool,
194+
}
195+
)
196+
if (!providerTool) throw new Error('Greenhouse agent tool was not created')
197+
const { executionParams } = prepareToolExecution(
198+
providerTool,
199+
{ prospect: true, job_ids: [81] },
200+
{},
201+
undefined
202+
)
203+
expect(
204+
tool.request.body?.({
205+
...executionParams,
206+
accessToken: 'fixture-token',
207+
} as GreenhouseCreateApplicationV2Params)
208+
).toEqual({ candidate_id: 42, prospect: true, job_ids: [81] })
209+
})
210+
211+
it('uses a newly supplied internal opening ID after leaving opening list filters', async () => {
212+
const tool = greenhouseHireApplicationV2Tool
213+
vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block)
214+
const params = getParamsForToolOperation(
215+
{ type: 'greenhouse_v2', params: { opening_id: 'external-opening' } },
216+
tool
217+
)
218+
const providerTool = await transformBlockTool(
219+
{ type: 'greenhouse_v2', params },
220+
{
221+
selectedOperation: 'hire_application',
222+
getAllBlocks: () => [GreenhouseV2Block],
223+
getTool: () => tool,
224+
}
225+
)
226+
if (!providerTool) throw new Error('Greenhouse agent tool was not created')
227+
const { executionParams } = prepareToolExecution(
228+
providerTool,
229+
{ id: 99, opening_id: 81 },
230+
{},
231+
undefined
232+
)
233+
expect(
234+
tool.request.body?.({
235+
...executionParams,
236+
accessToken: 'fixture-token',
237+
} as GreenhouseHireApplicationV2Params)
238+
).toEqual({ opening_id: 81 })
239+
})
240+
241+
it.each(['credential', 'oauthCredential'])(
242+
'preserves configured %s but not a resolved token across OAuth operations',
243+
(credentialId) => {
244+
vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block)
245+
const params = getParamsForToolOperation(
246+
{
247+
type: 'greenhouse_v2',
248+
params: {
249+
[credentialId]: 'fixture-credential',
250+
accessToken: 'expired-resolved-token',
251+
},
252+
},
253+
greenhouseListJobsV2Tool
254+
)
255+
expect(params[credentialId]).toBe('fixture-credential')
256+
expect(params).not.toHaveProperty('accessToken')
257+
}
258+
)
259+
74260
it.each([
75261
{ operation: 'list_jobs', field: 'status', tool: greenhouseListJobsV2Tool },
76262
{ operation: 'list_offers', field: 'status', tool: greenhouseListOffersV2Tool },

0 commit comments

Comments
 (0)