Skip to content

test(core): add getResponseMeta-paths integration test (non-GC angle)#3929

Merged
ntucker merged 2 commits intofix/journey-mutation-on-result-cache-hit-scalarfrom
cursor/journey-bug-integration-test-5fd9
Apr 28, 2026
Merged

test(core): add getResponseMeta-paths integration test (non-GC angle)#3929
ntucker merged 2 commits intofix/journey-mutation-on-result-cache-hit-scalarfrom
cursor/journey-bug-integration-test-5fd9

Conversation

@ntucker
Copy link
Copy Markdown
Collaborator

@ntucker ntucker commented Apr 28, 2026

Stacks on #3925.

Adds packages/core/src/controller/__tests__/getResponseMeta-paths.ts alongside the existing getResponseMeta-countRef.ts to cover the journey-mutation bug from a second, non-GC angle.

The two tests are complementary:

Test Consumer of paths Failure mode it locks in Fires under ImmortalGCPolicy?
getResponseMeta-countRef.ts (existing) GCPolicy.createCountRef({key, paths})entityCount Under-counted entity reaped while still referenced No (GCPolicy-only)
getResponseMeta-paths.ts (new) entityExpiresAt(paths, state.entitiesMeta)expiresAt → hooks' refetch gate 2nd+ subscriber observes a too-late expiresAt and never triggers entity-expiry refetch Yes

The new test asserts a property a normal hook user can observe — every subscriber to the same endpoint must receive the same expiresAt from Controller.getResponseMeta() — without poking implementation-detail state like gcPolicy['entityCount']:

const FOO_1_EXPIRY = 100;
const FOO_2_EXPIRY = 1_000_000;
// state has Foo|'1' and Foo|'2' with above expiries
const m1 = controller.getResponseMeta(ep, state);  // miss
const m2 = controller.getResponseMeta(ep, state);  // hit
const m3 = controller.getResponseMeta(ep, state);  // hit

expect(m1.expiresAt).toBe(FOO_1_EXPIRY);          // earliest
expect(m2.expiresAt).toBe(m1.expiresAt);          // same
expect(m3.expiresAt).toBe(m1.expiresAt);          // same

The state shape (endpoints slot populated, no top-level meta.expiresAt) corresponds to real-world cases where entityExpiresAt(paths, …) is the only thing computing the endpoint's effective expiresAt: SSR hydration, controller.set(Entity, …), or useQuery. The resulting expiresAt flows directly into useSuspense / useCache / useFetch / useDLE / useLive's refetch gate if (Date.now() <= expiresAt && !forceFetch) return;, so a too-late value silently suppresses refetch on the 2nd+ subscriber.

Verification

  • On the fix: both tests pass.
  • Reintroduce the buggy paths.shift() (and remove the journey-at-write change): m3.expiresAt returns 1_000_000 (FOO_2_EXPIRY) instead of 100 (FOO_1_EXPIRY) — the new test fails at line 81; the existing countRef test also fails as it did pre-fix.
  • yarn jest --selectProjects ReactDOM --testPathPatterns "packages/(normalizr|core|endpoint)" — passes.

No production code changes.

Open in Web Open in Cursor 

The previous integration test poked GCPolicy['entityCount'] directly to
prove the journey-mutation bug. Replace it with a test that asserts the
public-API consequence the bug creates outside of GC: every subscriber
to the same endpoint must observe the same expiresAt from
Controller.getResponseMeta().

This is the property the bug actually broke for non-GC users: with the
buggy paths.shift(), entityExpiresAt(paths, …) iterates a
progressively-shorter list, dropping the entity with the earliest
expiry first. Subscriber 2 observes a too-late expiresAt; subscriber 3+
observe Infinity and never refetch. Fires under ImmortalGCPolicy too,
since entityExpiresAt is unconditional whenever the endpoint has no
top-level meta.expiresAt — typical for state populated via
controller.set(Entity, …), SSR hydration, or useQuery.

Verified the assertion fails on the buggy paths.shift() (m3.expiresAt
returns FOO_2_EXPIRY instead of FOO_1_EXPIRY) and passes on the fix.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-site Ignored Ignored Preview Apr 28, 2026 0:57am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

⚠️ No Changeset found

Latest commit: 92d57e0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.21%. Comparing base (059b0e3) to head (92d57e0).
⚠️ Report is 1 commits behind head on fix/journey-mutation-on-result-cache-hit-scalar.

Additional details and impacted files
@@                               Coverage Diff                                @@
##           fix/journey-mutation-on-result-cache-hit-scalar    #3929   +/-   ##
================================================================================
  Coverage                                            98.21%   98.21%           
================================================================================
  Files                                                  154      154           
  Lines                                                 3018     3018           
  Branches                                               604      604           
================================================================================
  Hits                                                  2964     2964           
  Misses                                                  11       11           
  Partials                                                43       43           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ntucker ntucker marked this pull request as ready for review April 28, 2026 12:55
Restore the GC-side getResponseMeta-countRef.ts integration test that
the prior commit replaced. The two tests cover the journey-mutation
bug from complementary angles:

- getResponseMeta-countRef.ts: GC consumer of paths (entityCount
  under-counting → premature reaping under default GCPolicy).
- getResponseMeta-paths.ts: non-GC consumer of paths
  (entityExpiresAt → suppressed entity-expiry refetch; fires under
  ImmortalGCPolicy too).

Both pass on the fix; both fail on the buggy paths.shift().
@cursor cursor Bot changed the title test(core): replace getResponseMeta-countRef with getResponseMeta-paths test(core): add getResponseMeta-paths integration test (non-GC angle) Apr 28, 2026
@ntucker ntucker merged commit 05e1298 into fix/journey-mutation-on-result-cache-hit-scalar Apr 28, 2026
24 checks passed
@ntucker ntucker deleted the cursor/journey-bug-integration-test-5fd9 branch April 28, 2026 13:08
ntucker added a commit that referenced this pull request Apr 28, 2026
* chore: rebase PR #3925 onto latest master

* pkg: Bump peerdeps of @data-client/react to support 0.17 (#3927)

* pkg: Bump peerdeps of @data-client/react to support 0.17

* pkg: Update yarn.lock for peerdep bump

* perf(normalizr): store consumer-facing journey at write time (#3928)

Move the per-hit paths.slice(1) (and the hasStringDeps filter loop)
out of GlobalCache.getResults and into the cache write.

GlobalCache.paths() already produces the placeholder-free,
function-free shape every consumer needs. Hand it to
WeakDependencyMap.set as the journey, and the cache-hit branch can
return that array by reference - no per-hit allocation, no per-hit
typeof === 'function' walk.

Safety: paths is now a shared reference held by every subsequent
hit. The contract that consumers must not mutate it was established
by the journey-mutation fix (PR #3925) and is exercised by the
existing globalCache.test.ts regression test.

* fix: Scalar reversion

* test(core): add getResponseMeta-paths integration test (non-GC angle) (#3929)

* test(core): replace getResponseMeta-countRef with getResponseMeta-paths

The previous integration test poked GCPolicy['entityCount'] directly to
prove the journey-mutation bug. Replace it with a test that asserts the
public-API consequence the bug creates outside of GC: every subscriber
to the same endpoint must observe the same expiresAt from
Controller.getResponseMeta().

This is the property the bug actually broke for non-GC users: with the
buggy paths.shift(), entityExpiresAt(paths, …) iterates a
progressively-shorter list, dropping the entity with the earliest
expiry first. Subscriber 2 observes a too-late expiresAt; subscriber 3+
observe Infinity and never refetch. Fires under ImmortalGCPolicy too,
since entityExpiresAt is unconditional whenever the endpoint has no
top-level meta.expiresAt — typical for state populated via
controller.set(Entity, …), SSR hydration, or useQuery.

Verified the assertion fails on the buggy paths.shift() (m3.expiresAt
returns FOO_2_EXPIRY instead of FOO_1_EXPIRY) and passes on the fix.

* test(core): keep existing countRef integration test alongside paths test

Restore the GC-side getResponseMeta-countRef.ts integration test that
the prior commit replaced. The two tests cover the journey-mutation
bug from complementary angles:

- getResponseMeta-countRef.ts: GC consumer of paths (entityCount
  under-counting → premature reaping under default GCPolicy).
- getResponseMeta-paths.ts: non-GC consumer of paths
  (entityExpiresAt → suppressed entity-expiry refetch; fires under
  ImmortalGCPolicy too).

Both pass on the fix; both fail on the buggy paths.shift().
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.

1 participant