From 6af6410ac0a9968191f36b932b8fbd5f472e176f Mon Sep 17 00:00:00 2001 From: Brun Christophe Date: Mon, 13 Jul 2026 15:04:48 +0200 Subject: [PATCH 1/3] fix(datasource): coerce Zendesk id filter values to integers The agent parses Number values through `to_f`, so `id = N` / `id IN [...]` filters reach the datasource as floats (e.g. `123.0`). The PK-lookup short-circuit forwarded those to Zendesk's REST endpoints, so `/tickets/show_many?ids=123.0` was rejected -- the error body embeds a validation regex that then fails JSON parsing, surfacing as APIError/500 -- and the id-keyed result map missed even when the call succeeded. Normalise id values to Integer in `extract_id_lookup`, the single choke point shared by Ticket (fetch_tickets_by_ids) and User/Organization (find_one). Surfaced by the workflow fallback-inbox flow, which lists client records via `id IN [...]`; existing specs built condition trees with raw integers and never exercised the parser's float casting. Co-Authored-By: Claude Opus 4.8 --- .../collections/base_collection.rb | 13 +++++++++++-- .../collections/ticket_spec.rb | 18 ++++++++++++++++++ .../collections/user_spec.rb | 10 ++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb index a9aac3202..398a892e0 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb @@ -45,15 +45,24 @@ def aggregate_count(_caller, _filter) # Zendesk Search has no `id:` operator, so collections short-circuit # PK lookups to /resource/{id} when the filter is `id = N` or `id IN [...]`. + # + # The agent parses Number values through `to_f`, so an `id IN [...]` filter + # arrives as floats (e.g. `123.0`). Zendesk's REST ids are integers, so the + # values are normalised here before hitting /show_many or /resource/{id}; + # otherwise the API rejects `123.0` (500) or the id-keyed result map misses. def extract_id_lookup(node) return nil unless node.is_a?(Leaf) && node.field == 'id' case node.operator - when Operators::EQUAL then [node.value] - when Operators::IN then Array(node.value) + when Operators::EQUAL then [normalize_id(node.value)] + when Operators::IN then Array(node.value).map { |v| normalize_id(v) } end end + def normalize_id(value) + Integer(value, exception: false) || value + end + def project(record, projection) return record if projection.nil? diff --git a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb index 3d7ff9e06..251f07e34 100644 --- a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb +++ b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb @@ -86,6 +86,24 @@ def zendesk_record(attrs) expect(client).to have_received(:fetch_tickets_by_ids).with([1, 2]).once end + it 'coerces float ids (as produced by the agent parser) to integers' do + bulk_response = { 1 => { 'id' => 1, 'requester_id' => nil } } + allow(client).to receive_messages(fetch_tickets_by_ids: bulk_response, fetch_user_emails: {}) + + filter = Filter.new(condition_tree: Leaf.new('id', 'in', [1.0, 2.0])) + expect(collection.list(nil, filter, ['id']).map { |r| r['id'] }).to eq([1]) + expect(client).to have_received(:fetch_tickets_by_ids).with([1, 2]) + end + + it 'coerces a float id on an EQUAL lookup to an integer' do + ticket = { 'id' => 215, 'requester_id' => nil } + allow(client).to receive_messages(fetch_tickets_by_ids: { 215 => ticket }, fetch_user_emails: {}) + + filter = Filter.new(condition_tree: Leaf.new('id', 'equal', 215.0)) + expect(collection.list(nil, filter, ['id']).map { |r| r['id'] }).to eq([215]) + expect(client).to have_received(:fetch_tickets_by_ids).with([215]) + end + it 'silently drops ids the bulk endpoint did not return' do allow(client).to receive(:fetch_tickets_by_ids).with([1, 2]).and_return( 1 => { 'id' => 1, 'requester_id' => nil } diff --git a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb index 24f1f7fe3..bcf0a325d 100644 --- a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb +++ b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb @@ -38,6 +38,16 @@ def zendesk_user(attrs) expect(client).not_to have_received(:search) end + it 'coerces a float id (as produced by the agent parser) to an integer' do + user = zendesk_user('id' => 7, 'email' => 'a@b.com', 'name' => 'A') + allow(client).to receive_messages(find_user: user, search: []) + + filter = Filter.new(condition_tree: Leaf.new('id', 'equal', 7.0)) + collection.list(nil, filter, ['id', 'email']) + expect(client).to have_received(:find_user).with(7) + expect(client).not_to have_received(:search) + end + it 'searches with type:user otherwise' do allow(client).to receive(:search).and_return([]) From ddf5a275c1f70ac20ffbf8baae62e0c721a989d1 Mon Sep 17 00:00:00 2001 From: Brun Christophe Date: Mon, 13 Jul 2026 15:16:39 +0200 Subject: [PATCH 2/3] fix(datasource): leave fractional ids untouched in normalize_id Integer(123.9) truncates to 123, so a fractional id filter would match an unrelated record instead of nothing. Only normalise values whose integer conversion equals the original; leave fractional floats as-is. Addresses PR review feedback. Co-Authored-By: Claude Opus 4.8 --- .../collections/base_collection.rb | 8 +++++++- .../collections/ticket_spec.rb | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb index 398a892e0..8fc4126b1 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb @@ -59,8 +59,14 @@ def extract_id_lookup(node) end end + # Only integral values are normalised. A fractional float (`123.9`) is + # left untouched rather than truncated to `123`, which would otherwise + # match an unrelated record instead of nothing. def normalize_id(value) - Integer(value, exception: false) || value + normalized = Integer(value, exception: false) + return value if normalized.nil? || (value.is_a?(Float) && normalized != value) + + normalized end def project(record, projection) diff --git a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb index 251f07e34..320c1163d 100644 --- a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb +++ b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb @@ -104,6 +104,14 @@ def zendesk_record(attrs) expect(client).to have_received(:fetch_tickets_by_ids).with([215]) end + it 'does not truncate a fractional id to an unrelated record' do + allow(client).to receive_messages(fetch_tickets_by_ids: {}, fetch_user_emails: {}) + + filter = Filter.new(condition_tree: Leaf.new('id', 'equal', 123.9)) + expect(collection.list(nil, filter, ['id'])).to eq([]) + expect(client).to have_received(:fetch_tickets_by_ids).with([123.9]) + end + it 'silently drops ids the bulk endpoint did not return' do allow(client).to receive(:fetch_tickets_by_ids).with([1, 2]).and_return( 1 => { 'id' => 1, 'requester_id' => nil } From a2eff83dfb491733e47f46dcdb914df158ad407f Mon Sep 17 00:00:00 2001 From: Brun Christophe Date: Mon, 13 Jul 2026 15:22:44 +0200 Subject: [PATCH 3/3] refactor(datasource): drop unreachable fractional-id guard Revert normalize_id to the minimal integral coercion. The fractional branch guarded an input the agent parser never produces (integer PKs yield integral floats via to_f), and its spec asserted an empty result that the real client would not return -- a fractional id still hits show_many and raises the same APIError this fix addresses. Keep only what is strictly necessary. Co-Authored-By: Claude Opus 4.8 --- .../collections/base_collection.rb | 8 +------- .../collections/ticket_spec.rb | 8 -------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb index 8fc4126b1..398a892e0 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb @@ -59,14 +59,8 @@ def extract_id_lookup(node) end end - # Only integral values are normalised. A fractional float (`123.9`) is - # left untouched rather than truncated to `123`, which would otherwise - # match an unrelated record instead of nothing. def normalize_id(value) - normalized = Integer(value, exception: false) - return value if normalized.nil? || (value.is_a?(Float) && normalized != value) - - normalized + Integer(value, exception: false) || value end def project(record, projection) diff --git a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb index 320c1163d..251f07e34 100644 --- a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb +++ b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/ticket_spec.rb @@ -104,14 +104,6 @@ def zendesk_record(attrs) expect(client).to have_received(:fetch_tickets_by_ids).with([215]) end - it 'does not truncate a fractional id to an unrelated record' do - allow(client).to receive_messages(fetch_tickets_by_ids: {}, fetch_user_emails: {}) - - filter = Filter.new(condition_tree: Leaf.new('id', 'equal', 123.9)) - expect(collection.list(nil, filter, ['id'])).to eq([]) - expect(client).to have_received(:fetch_tickets_by_ids).with([123.9]) - end - it 'silently drops ids the bulk endpoint did not return' do allow(client).to receive(:fetch_tickets_by_ids).with([1, 2]).and_return( 1 => { 'id' => 1, 'requester_id' => nil }