Skip to content

Commit 809971b

Browse files
committed
fix(workflows): cover legacy workflow-typed tools and retry depth-truncated expansions
- toolInputCallees matches both workflow tool type spellings via isWorkflowBlockType and passes the tool's own type as the legacy canonicalModes fallback, matching providers/utils resolution - a depth-capped expansion no longer poisons the expanded set, so a shallower path re-expands the node in full (Cursor finding) - the allowed-but-workspaceless auth branch now returns 403, not the authz result's 200 - tests: legacy tool type + per-tool index-scope isolation, diamond re-expansion with a real subtree, depth ceiling, shallow-path retry
1 parent bc09b56 commit 809971b

3 files changed

Lines changed: 100 additions & 20 deletions

File tree

apps/sim/app/api/workflows/[id]/references/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const GET = withRouteHandler(async (request: NextRequest, context: RouteC
2828
if (!auth.allowed || !auth.workflow?.workspaceId) {
2929
return NextResponse.json(
3030
{ error: auth.message ?? 'Access denied' },
31-
{ status: auth.status || 403 }
31+
{ status: auth.allowed ? 403 : auth.status }
3232
)
3333
}
3434

apps/sim/lib/workflows/references/operations.test.ts

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,71 @@ describe('resolveWorkflowReferences', () => {
101101
})
102102

103103
it('bounds converging paths (diamond) instead of re-expanding', () => {
104-
// A → B, A → C, B → D, C → D. D reconverges; it must appear under both B and C
105-
// but only expand once (here D is a leaf anyway; the guard prevents blow-up).
104+
// A → B, A → C, B → D, C → D, D → E. D reconverges; it must appear under both
105+
// B and C but expand its subtree (E) only under the first-visited branch.
106+
const workflowsWithE = [...workflows, { id: 'e', name: 'E' }]
106107
const blocks = [
107108
workflowBlock('a', 'b'),
108109
workflowBlock('a', 'c'),
109110
workflowBlock('b', 'd'),
110111
workflowBlock('c', 'd'),
112+
workflowBlock('d', 'e'),
111113
]
112-
const { callees } = resolveWorkflowReferences('a', workflows, blocks, [])
114+
const { callees } = resolveWorkflowReferences('a', workflowsWithE, blocks, [])
113115
const b = callees.find((n) => n.id === 'b')
114116
const c = callees.find((n) => n.id === 'c')
115-
// D expands under the first-visited branch (B) and is a plain leaf under C.
116-
expect(b?.children.map((n) => n.id)).toEqual(['d'])
117-
expect(c?.children.map((n) => n.id)).toEqual(['d'])
118-
expect(c?.children[0]).toMatchObject({ id: 'd', cycle: false, children: [] })
117+
// D expands under the first-visited branch (B) and is a collapsed leaf under C.
118+
expect(b?.children).toEqual([
119+
{
120+
id: 'd',
121+
name: 'D',
122+
cycle: false,
123+
children: [{ id: 'e', name: 'E', cycle: false, children: [] }],
124+
},
125+
])
126+
expect(c?.children).toEqual([{ id: 'd', name: 'D', cycle: false, children: [] }])
127+
})
128+
129+
it('truncates expansion at the depth ceiling', () => {
130+
// A linear chain longer than MAX_REFERENCE_DEPTH (25): w0 → w1 → … → w29.
131+
const chain = Array.from({ length: 30 }, (_, i) => ({ id: `w${i}`, name: `W${i}` }))
132+
const blocks = Array.from({ length: 29 }, (_, i) => workflowBlock(`w${i}`, `w${i + 1}`))
133+
const { callees } = resolveWorkflowReferences('w0', chain, blocks, [])
134+
let depth = 0
135+
let node = callees[0]
136+
while (node) {
137+
depth += 1
138+
node = node.children[0]
139+
}
140+
expect(depth).toBe(25)
141+
})
142+
143+
it('re-expands a depth-truncated node when a shallower path reaches it', () => {
144+
// Root fans out to a 25-deep chain (visited first by name sort: "A…") whose
145+
// tail X gets truncated at the ceiling, and a direct edge (via "Z") to X.
146+
// The shallow path must still show X's child Y instead of a collapsed leaf.
147+
const nodes = [
148+
{ id: 'root', name: 'Root' },
149+
...Array.from({ length: 24 }, (_, i) => ({
150+
id: `a${i}`,
151+
name: `A${String(i).padStart(2, '0')}`,
152+
})),
153+
{ id: 'x', name: 'X' },
154+
{ id: 'y', name: 'Y' },
155+
{ id: 'z', name: 'Z' },
156+
]
157+
const blocks = [
158+
workflowBlock('root', 'a0'),
159+
...Array.from({ length: 23 }, (_, i) => workflowBlock(`a${i}`, `a${i + 1}`)),
160+
workflowBlock('a23', 'x'),
161+
workflowBlock('root', 'z'),
162+
workflowBlock('z', 'x'),
163+
workflowBlock('x', 'y'),
164+
]
165+
const { callees } = resolveWorkflowReferences('root', nodes, blocks, [])
166+
const z = callees.find((n) => n.id === 'z')
167+
const xUnderZ = z?.children.find((n) => n.id === 'x')
168+
expect(xUnderZ?.children.map((n) => n.id)).toEqual(['y'])
119169
})
120170

121171
it('drops dangling / out-of-workspace child ids', () => {
@@ -217,6 +267,29 @@ describe('resolveWorkflowReferences', () => {
217267
expect(callees.map((n) => n.id)).toEqual(['c'])
218268
})
219269

270+
it('resolves legacy workflow-typed tools and isolates index-scoped modes per tool', () => {
271+
// Tool 0 is a legacy `workflow`-typed entry (still rendered/executed by the
272+
// editor); tool 1 is advanced-mode via its own index-scoped key. Tool 0 must
273+
// stay basic (`b`) — tool 1's override must not bleed into it.
274+
const blocks: ReferenceBlockRow[] = [
275+
{
276+
parentId: 'a',
277+
type: 'agent',
278+
childFromSelector: null,
279+
childFromManual: null,
280+
canonicalModes: { '1:workflowId': 'advanced' },
281+
toolInputValues: [
282+
[
283+
{ type: 'workflow', params: { workflowId: 'b' } },
284+
{ type: 'workflow_input', params: { workflowId: 'c', manualWorkflowId: 'd' } },
285+
],
286+
],
287+
},
288+
]
289+
const { callees } = resolveWorkflowReferences('a', workflows, blocks, [])
290+
expect(callees.map((n) => n.id)).toEqual(['b', 'd'])
291+
})
292+
220293
it('resolves workflow tools from a JSON-stringified tool-input value', () => {
221294
const blocks: ReferenceBlockRow[] = [
222295
{

apps/sim/lib/workflows/references/operations.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export interface ReferenceBlockRow {
6363
* Aggregated `tool-input` sub-block values on this block (agent-style tool
6464
* lists). Each entry is one sub-block's raw value — an array of tool objects,
6565
* or that array JSON-stringified. `workflow_input` tools carry the callee id
66-
* in `params.workflowId`. Null when the block has no tool-input sub-blocks.
66+
* in `params.workflowId` (basic) or `params.manualWorkflowId` (advanced).
67+
* Null when the block has no tool-input sub-blocks.
6768
*/
6869
toolInputValues: unknown[] | null
6970
}
@@ -85,11 +86,12 @@ interface ReferenceGraph {
8586
}
8687

8788
/**
88-
* Callee workflow ids referenced by a block's tool-input values: `workflow_input`
89-
* tools resolved to their ACTIVE canonical member — the basic `params.workflowId`
90-
* selector or the advanced `params.manualWorkflowId` input, per the tool's
91-
* index-scoped `canonicalModes` override ({@link scopeCanonicalModesForTool}) —
92-
* mirroring how execution picks the live value.
89+
* Callee workflow ids referenced by a block's tool-input values: workflow tools
90+
* (`workflow_input`, plus legacy stored entries typed `workflow`) resolved to
91+
* their ACTIVE canonical member — the basic `params.workflowId` selector or the
92+
* advanced `params.manualWorkflowId` input, per the tool's index-scoped
93+
* `canonicalModes` override ({@link scopeCanonicalModesForTool}) — mirroring how
94+
* execution picks the live value.
9395
*/
9496
function toolInputCallees(
9597
toolInputValues: unknown[] | null,
@@ -101,14 +103,15 @@ function toolInputCallees(
101103
const { array } = coerceObjectArray(value)
102104
if (!array) continue
103105
array.forEach((tool, toolIndex) => {
104-
if (!isRecord(tool) || tool.type !== BlockType.WORKFLOW_INPUT || !isRecord(tool.params)) {
106+
if (
107+
!isRecord(tool) ||
108+
typeof tool.type !== 'string' ||
109+
!isWorkflowBlockType(tool.type) ||
110+
!isRecord(tool.params)
111+
) {
105112
return
106113
}
107-
const scoped = scopeCanonicalModesForTool(
108-
canonicalModes ?? undefined,
109-
toolIndex,
110-
BlockType.WORKFLOW_INPUT
111-
)
114+
const scoped = scopeCanonicalModesForTool(canonicalModes ?? undefined, toolIndex, tool.type)
112115
const active = resolveActiveCanonicalValue(
113116
WORKFLOW_ID_CANONICAL_GROUP,
114117
{
@@ -232,6 +235,10 @@ function buildTree(
232235
path.add(childId)
233236
nodes.push({ id: childId, name, cycle: false, children: expand(childId, depth + 1) })
234237
path.delete(childId)
238+
// A depth-capped expansion is incomplete — allow a shallower path to retry
239+
// it in full instead of collapsing to a leaf. Each retry starts strictly
240+
// shallower, so this stays bounded.
241+
if (depth + 1 >= MAX_REFERENCE_DEPTH) expanded.delete(childId)
235242
}
236243
return nodes
237244
}

0 commit comments

Comments
 (0)