Skip to content

[NO-REF] Handle non-JSON HTTP error bodies, vendor test fixtures, run tests serially - #266

Closed
pedro-lb wants to merge 5 commits into
masterfrom
fix/vendor-catalog-test-fixtures
Closed

[NO-REF] Handle non-JSON HTTP error bodies, vendor test fixtures, run tests serially#266
pedro-lb wants to merge 5 commits into
masterfrom
fix/vendor-catalog-test-fixtures

Conversation

@pedro-lb

@pedro-lb pedro-lb commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Five commits. The first is a real SDK fix; the rest are about making this repo's test runs legible. One test still fails — details at the bottom, it needs someone with context on the test index.

1. Preserve HTTP error details when the response body is not JSON

helpers.throwHttpErrorFromResponse called response.json() on every non-2XX body. Error responses are not always JSON — rate limit and gateway responses are commonly plain text. For those the parse rejects and the SDK surfaces the parse failure rather than the request failure:

SyntaxError: Unexpected token 'T', "Too many requests" is not valid JSON
  at Response.json (node_modules/node-fetch/src/body.js:149:15)

The status, statusText, url and headers go with it — the details the function exists to attach. A caller cannot tell a rate limit from a bad gateway from a malformed payload. Now the body is read as text and parsed if possible, falling back to the raw text, and to HTTP <status> when empty. All modules route non-2XX responses through this one helper. tracker.js already handled non-JSON bodies this way when emitting error events.

Behavior change: error.message is now always a string. Previously a JSON body without a message field left it undefined, which makes error.message.toLowerCase() throw — spec/src/modules/catalog/catalog-groups-v2.js:223 does exactly that.

2. Vendor the catalog test fixtures

tasks.js and catalog-files.js downloaded fixtures from raw.githubusercontent.com on every run, before any assertion. Now committed under spec/fixtures/ (1.9 KB, byte-identical to the remote copies) and read from disk. spec/ is outside files in package.json, so nothing extra ships.

3-4. Run the suite serially

Every spec throttles itself with a 300ms afterEach, commented as being there to avoid rate limiting. One request per 300ms is ~200/min, just under the x-ratelimit-limit: 201 the API reports — the throttle is calibrated to the limit. --parallel gives each worker its own 300ms throttle, so N workers make N times that rate and the run exceeds the quota. That is why failures moved between spec files run to run.

Measured on this branch:

config passing failing time
--parallel, 5s timeout 991 8 3m
--parallel, 5s (re-run) 996 9 2m
--parallel, 5s + commit 1 1049 6 3m
--parallel, 15s 987 7 3m
serial, 5s 1063 1 4-12m

Commit 4 also reverts a 15s timeout I tried in commit 3: it made things worse, since a rate limited request holding a slot for 15s prolongs the pressure. .mocharc.json is back at 5s.

Trade-off: serial costs wall clock time. If that is not acceptable, the alternative is raising the rate limit on the test API key so --parallel fits within it.

5. Surface the real failure in one search test

spec/src/modules/search.js:465 had no rejection handler on its promise chain, so a failing assertion never reached done() and was reported as a timeout. It exceeded the 5s and the 15s timeout identically — the duration was never the problem. With .catch(done) the real error appears.

Worth noting the file has 53 .then((res) => { and 2 .catch(done), so other tests there can mask failures the same way. Left alone here.

The remaining failure — needs your context

1063 passing (4m)
1 failing

1) ConstructorIO - Search
     getSearchResults
       Should return a response with a valid query, section and preFilterExpression:
   AssertionError: expected 1 to deeply equal 2
   at spec/src/modules/search.js:493:51

The preFilterExpression is (group_id=BrandXY AND Color=red) OR (Color=blue AND Brand=XYZ). In items.csv, 10005 (BrandXY, red) matches the first branch and 10008 (blue, XYZ) the second, so the expected 2 looks right against the fixture. The index returns 1, which suggests the test index does not currently hold what the fixture describes — several specs call replaceCatalog against the shared test key, so its state is not fixture-determined.

I did not guess at the expected number. This one needs someone who knows the intended state of the test index.

🤖 Generated with Claude Code

The catalog and tasks specs downloaded their fixture files from
raw.githubusercontent.com in a `before` hook on every run. When the
request is rate limited, the response body is plain text rather than a
file, which surfaces as `SyntaxError: Unexpected token 'T', "Too many
requests" is not valid JSON` and a cascade of 5s timeouts unrelated to
the code under test.

Commit the four fixtures (1.9 KB total) under `spec/fixtures/` and read
them from disk via a `readCatalogFixture` helper, removing the network
dependency from test setup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 16:23
@pedro-lb
pedro-lb requested a review from a team as a code owner August 18, 2026 16:23

@constructor-claude-bedrock constructor-claude-bedrock Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR vendors four catalog fixture files into the repo and replaces network fetches with synchronous disk reads, eliminating a flaky test failure caused by GitHub rate-limiting on raw content URLs.

Inline comments: 2 discussions added

Overall Assessment: ✅ Pass

Comment thread spec/mocha.helpers.js
Comment thread spec/src/modules/catalog/catalog-files.js

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes a test-suite network dependency by vendoring small catalog fixture files into the repository and updating specs to read them from disk instead of fetching them from raw.githubusercontent.com at runtime.

Changes:

  • Added catalog fixture files under spec/fixtures/.
  • Introduced readCatalogFixture in spec/mocha.helpers.js to load fixtures from disk as buffers.
  • Updated affected specs to use the local fixtures and removed the prior network-fetch setup.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/src/modules/tasks.js Replaces remote fixture fetch with helpers.readCatalogFixture('items.csv') for catalog upload setup.
spec/src/modules/catalog/catalog-files.js Loads all catalog-related fixtures from disk and removes the before hook that fetched them over the network.
spec/mocha.helpers.js Adds readCatalogFixture helper to synchronously read vendored fixtures as buffers.
spec/fixtures/variations.csv Adds vendored variations catalog fixture.
spec/fixtures/items.csv Adds vendored items catalog fixture.
spec/fixtures/item_groups.csv Adds vendored item groups fixture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread spec/src/modules/catalog/catalog-files.js
@pedro-lb
pedro-lb marked this pull request as draft August 18, 2026 16:28
`throwHttpErrorFromResponse` parsed every non-2XX body as JSON. Error
responses are not always JSON: rate limit and gateway responses are
commonly plain text or HTML. For those, `response.json()` rejected and
the SDK surfaced the parse failure instead of the request failure:

    SyntaxError: Unexpected token 'T', "Too many requests" is not valid JSON
      at Response.json (node_modules/node-fetch/src/body.js:149:15)

The real status, statusText, url and headers were lost with it, which are
the details the function exists to attach.

Read the body as text and attempt to parse it, falling back to the raw
text as the error message, and to `HTTP <status>` when the body is empty.
`error.message` is now always a string, where previously it was
`undefined` for a JSON body without a `message` field.

All modules route their non-2XX responses through this helper, so this
covers every endpoint. `tracker.js` already handled non-JSON bodies this
way when emitting error events.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pedro-lb pedro-lb changed the title [NO-REF] Vendor catalog test fixtures instead of fetching them at test time [NO-REF] Handle non-JSON HTTP error bodies and vendor catalog test fixtures Aug 18, 2026
pedro-lb and others added 3 commits August 18, 2026 13:44
The suite makes real API calls, and the global 5s timeout is not enough
for the slower endpoints when the run is being rate limited. Requests
that complete normally take 50-350ms, so anything approaching 5s is
already backing off rather than working.

Across four runs on this branch the timeouts landed in seven different
spec files, so this raises the shared timeout rather than patching each
`describe` that happened to lose the race. Hook timeouts matter most
here: `--retries 3` does not apply to hooks, so one slow `before` takes
out its entire block on the first attempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts the 15s timeout from the previous commit: it made things worse
(987 passing / 7 failing, against 1049 / 6 at 5s) because a rate limited
request holding a slot for 15s prolongs the pressure instead of easing it.

Every spec throttles itself with a 300ms `afterEach` delay, commented as
being there to avoid rate limiting. One request per 300ms is ~200/min,
which sits just under the `x-ratelimit-limit: 201` the API reports — the
throttle is calibrated to the limit.

`--parallel` gives each worker its own 300ms throttle, so N workers make
N times the calibrated rate and the run exceeds the quota. That is why
the failures move between spec files run to run, and why they surface as
`Too many requests` and 5s-plus response times rather than as assertion
failures.

Running serially restores the rate the throttle was written for. This
trades wall clock time for a run that can actually pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test's promise chain had no rejection handler, so a failing assertion
never reached `done()` and the failure was reported as a timeout instead.
That is why it exceeded both the 5s and the 15s timeout identically: the
duration was never the problem, `done()` simply was never called.

Adding `.catch(done)` reports the actual assertion error. This is
diagnostic — the underlying expectation still needs fixing once CI shows
what the endpoint returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pedro-lb pedro-lb changed the title [NO-REF] Handle non-JSON HTTP error bodies and vendor catalog test fixtures [NO-REF] Handle non-JSON HTTP error bodies, vendor test fixtures, run tests serially Aug 18, 2026
@pedro-lb

Copy link
Copy Markdown
Contributor Author

Tried to apply a quick fix here by versioning fixtures instead of fetching them, but found out tests are currently firing requests so the fix will be a bit longer. @Mudaafi would love to align on this!

@pedro-lb pedro-lb closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants