Skip to content

Commit e885854

Browse files
fix(tools): stop normalizeToolId eating the _v2 version suffix
table_query_rows_v2 starts with 'table_query_rows_', so the resource-suffix strip normalized it to the v1 tool. The executor then logged the v2 id while issuing v1's request shape — GET /rows?filter=<predicate> instead of POST /query with a predicate body — so a correctly configured table_v2 block 400'd with 'Filter looks like a v2 predicate tree but reached the legacy filter compiler'. The guard was right; the tool resolution was wrong. A trailing _v<n> is a version marker, not a resource id, so it is no longer stripped. Versioned ops are matched longest-first and listed alongside their unversioned form, so table_query_rows_v2_<tableId> still normalizes to table_query_rows_v2 rather than collapsing to v1. Applies to the knowledge ops too — same loop shape, same latent trap the first time one of them is versioned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M563cbFy2S74GvSDf2C3R
1 parent 4f9c104 commit e885854

2 files changed

Lines changed: 68 additions & 10 deletions

File tree

apps/sim/tools/normalize.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { normalizeToolId } from '@/tools/normalize'
6+
7+
describe('normalizeToolId', () => {
8+
it('strips a resource-id suffix', () => {
9+
expect(normalizeToolId('table_query_rows_tbl_1a2c1741')).toBe('table_query_rows')
10+
expect(normalizeToolId('knowledge_search_5cc56998-3e1d-4d91')).toBe('knowledge_search')
11+
expect(normalizeToolId('workflow_executor_f3d81b32')).toBe('workflow_executor')
12+
expect(normalizeToolId('deployed_block_executor_custom_block_9')).toBe(
13+
'deployed_block_executor'
14+
)
15+
})
16+
17+
it('leaves a bare tool id alone', () => {
18+
expect(normalizeToolId('table_query_rows')).toBe('table_query_rows')
19+
expect(normalizeToolId('gmail_send')).toBe('gmail_send')
20+
})
21+
22+
/**
23+
* Regression: `table_query_rows_v2` starts with `table_query_rows_`, so the
24+
* resource-suffix strip turned it into the v1 tool. The executor then logged
25+
* the v2 id while issuing v1's `GET /rows?filter=<predicate>` — which reached
26+
* the legacy filter compiler and 400'd on a correctly configured v2 block.
27+
*/
28+
it('does not mistake a version suffix for a resource id', () => {
29+
expect(normalizeToolId('table_query_rows_v2')).toBe('table_query_rows_v2')
30+
})
31+
32+
it('still strips a resource id from a VERSIONED op', () => {
33+
expect(normalizeToolId('table_query_rows_v2_tbl_1a2c1741')).toBe('table_query_rows_v2')
34+
})
35+
36+
it('is not fooled by a table id that merely starts with v', () => {
37+
expect(normalizeToolId('table_query_rows_v2x')).toBe('table_query_rows')
38+
expect(normalizeToolId('table_query_rows_version')).toBe('table_query_rows')
39+
})
40+
})

apps/sim/tools/normalize.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@
66
*
77
* Pure string utility — no server dependencies, safe to import in client components.
88
*/
9+
10+
/**
11+
* A trailing `_v2`-style segment is a VERSION marker, not a resource id, so it
12+
* must not be stripped: `table_query_rows_v2` is its own registered tool, and
13+
* normalizing it to `table_query_rows` silently executes the v1 tool's request
14+
* shape under the v2 tool's name.
15+
*/
16+
const VERSION_SUFFIX = /^v\d+$/
17+
18+
/**
19+
* Longest id first, so a versioned op claims its own suffixed ids before the
20+
* unversioned prefix can match them (`table_query_rows_v2_<tableId>` must
21+
* normalize to `table_query_rows_v2`, not `table_query_rows`).
22+
*/
23+
function stripResourceSuffix(toolId: string, ops: string[]): string | null {
24+
for (const op of [...ops].sort((a, b) => b.length - a.length)) {
25+
if (!toolId.startsWith(`${op}_`) || toolId.length <= op.length + 1) continue
26+
if (VERSION_SUFFIX.test(toolId.slice(op.length + 1))) continue
27+
return op
28+
}
29+
return null
30+
}
31+
932
export function normalizeToolId(toolId: string): string {
1033
// Custom (deploy-as-block) tools: 'deployed_block_executor_custom_block_<id>' ->
1134
// 'deployed_block_executor'. Note the id deliberately does NOT start with
@@ -23,14 +46,12 @@ export function normalizeToolId(toolId: string): string {
2346
}
2447

2548
const knowledgeOps = ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document']
26-
for (const op of knowledgeOps) {
27-
if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) {
28-
return op
29-
}
30-
}
49+
const knowledge = stripResourceSuffix(toolId, knowledgeOps)
50+
if (knowledge) return knowledge
3151

3252
const tableOps = [
3353
'table_query_rows',
54+
'table_query_rows_v2',
3455
'table_insert_row',
3556
'table_batch_insert_rows',
3657
'table_update_row',
@@ -41,11 +62,8 @@ export function normalizeToolId(toolId: string): string {
4162
'table_delete_row',
4263
'table_get_schema',
4364
]
44-
for (const op of tableOps) {
45-
if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) {
46-
return op
47-
}
48-
}
65+
const table = stripResourceSuffix(toolId, tableOps)
66+
if (table) return table
4967

5068
return toolId
5169
}

0 commit comments

Comments
 (0)