fix(api-gateway): test the compiled output, and enforce it - #11482
fix(api-gateway): test the compiled output, and enforce it#11482igorlukanin wants to merge 2 commits into
Conversation
|
Claude finished @igorlukanin's task in 4m 27s —— View job Review complete — approach is sound; 0 blocking, 1 medium, 4 low (5 inline comments)Full review
VerdictThe diagnosis is right and the fix is the right shape. Claims I was able to verify statically:
I could not run the suite — Praise
FindingsMedium
Low
One note not attachable inline
Not concerns
|
2f12ee5 to
8c558aa
Compare
|
Claude finished @igorlukanin's task in 2m 6s —— View job Re-review after push — 0 blocking, 1 medium, 5 low; 5 prior threads re-affirmed, 1 new commentFull review
What changed since the last reviewThe push tightened the guard in two ways, both good:
Also re-verified in the current tree:
I could not run the suite — FindingsEvery finding from the previous review survives in the current content — I checked each against the file rather than assuming, and left the threads unresolved rather than replying on them: Medium
Low
New, on the code added in this push
Praise
Not concerns
No threads resolved — all five prior concerns are still applicable in the current diff. |
| test('nothing narrows collection behind testMatch', () => { | ||
| // The assertions below reason from `testMatch` alone, so a key that skips a | ||
| // file jest would otherwise collect — the usual "temporarily ignore the | ||
| // flaky suite" edit — would slip a silently-inert test past them. | ||
| expect(jestConfig.testPathIgnorePatterns).toBeUndefined(); | ||
| expect(jestConfig.modulePathIgnorePatterns).toBeUndefined(); | ||
| expect(jestConfig.roots).toBeUndefined(); | ||
| }); |
There was a problem hiding this comment.
This test enumerates config keys, but the narrowing actually in effect today lives in the unit script, where the guard can't see it. package.json:18 is still:
"unit": "CUBE_JS_NATIVE_API_GATEWAY_INTERNAL=true jest --coverage --forceExit dist/test"
That trailing dist/test is a positional testPathPattern regex — functionally the same class of narrowing as testPathIgnorePatterns, just applied from the other side and invisible to require('jest.config.js'). So the exact edit this test is written to catch ("temporarily ignore the flaky suite") still lands unguarded if someone makes it in the script instead of the config: jest … dist/test/graphql leaves this suite green while 13 of the 14 don't run.
It's benign as written — the pattern is a superset of what testMatch collects, so today it filters nothing. Two things worth doing:
- Drop the positional. It's now redundant with
testMatch, and the PR body already claims it's gone ("now green with no path argument, so the config carries the target rather than the script compensating for it") — the script wasn't actually changed to match, so code and description disagree. - Either way, note the limitation here. The comment at lines 73-75 reads as though
testMatchis the only thing that decides collection, and a reader will believe it; a line saying "CLI arguments can still narrow this, and are out of scope" keeps the guard honest about its own reach.
Separately, minor: toBeUndefined() on three keys gives a failure message ("expected undefined, received [...]") that doesn't say why the key is forbidden. A .toBeUndefined() with the reason in the assertion — or expect({ testPathIgnorePatterns, roots }).toEqual({}) with a comment — costs nothing and saves the next person a git log. And testRegex belongs in the list: it's mutually exclusive with testMatch, so setting it makes jest throw rather than silently narrow, but that's a different failure to reason about, not a covered one.
There was a problem hiding this comment.
You're right that the PR body and the code disagreed, and the body was the wrong one. Corrected there.
Kept the positional, though, and took your point 2. It's deliberate: it's a superset of what testMatch collects so it filters nothing, and all 13 sibling dist-testing packages carry the same argument — dropping it here alone makes this package the odd one out right as CORE-736 goes to make them uniform. The redundancy is the point: if someone deletes testMatch, the script still narrows to dist/test instead of collecting untransformable sources.
But your actual finding stands regardless of which way that goes: a CLI positional is a second route to narrowing that the guard cannot see, and the comment implied testMatch was the only one. The guard now says so explicitly, and points at the unit script.
Also folded in the rest: testRegex is in the list (with a note that it makes jest throw rather than narrow — verified, jest refuses to start with both set, so it's named for the reader rather than caught), and the three toBeUndefined() calls are now one filter producing the offending key names, so the failure says which key and not just expected undefined.
There was a problem hiding this comment.
Took the reword, which is the arm of your "either drop it or reword" that matches the scope decision. The body bullet now states that the positional stays, why, and that it is a config-invisible narrowing route — so the six-months-later reader auditing the claim finds the reasoning instead of an unfinished fix.
Thanks for checking .test.d.ts / .test.js.map against the dist walk — that over-collection hazard is real for the new assertion and I hadn't verified it from that direction.
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## master #11482 +/- ##
===========================================
- Coverage 79.46% 59.10% -20.36%
===========================================
Files 480 223 -257
Lines 98810 17891 -80919
Branches 3636 3636
===========================================
- Hits 78515 10574 -67941
+ Misses 19774 6796 -12978
Partials 521 521
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Claude finished @igorlukanin's task in 2m 12s —— View job Re-review after push — 0 blocking, 0 medium, 1 low; 5 of 6 prior findings fixed, 5 threads resolvedFull review
What changed in
|
| Prior finding | Status in current tree |
|---|---|
Medium — Date.now restore leaks on a failing assertion |
✅ Fixed. A single afterEach(() => jest.restoreAllMocks()) at the top of the describe (date-parser.test.ts:8-10), all 10 inline calls dropped. Verified all 10 mocking sites still use jest.spyOn(Date, 'now'), so restore actually restores, and the comment states why the placement matters. |
Low — isCollected fails for every file on win32 |
✅ Fixed, and fixed the better way of the two I suggested: matching happens package-relative (toPosix(relativeToPackage(file)) vs. p.replace('<rootDir>/', '')), which immunizes against the separator issue and the glob-metacharacter-in-checkout-path issue at once, rather than only the first. |
Low — only source → compiled checked; orphaned dist tests |
✅ Fixed. no collected test has lost its source (lines 124-136) closes the reverse direction. Confirmed the dist walk can't over-collect: .test.d.ts and .test.js.map both carry a second dot after .test., so /\.(test|spec)\.[^.]+$/ rejects them and only .test.js is considered. |
Low — __snapshots__ excluded only incidentally |
✅ Fixed, added to IGNORED_DIRS with the reasoning inline. |
Low — '…Z' as unknown as Date |
✅ Fixed, now new Date('2020-09-22T13:03:20.518Z'); the three-line justification comment is gone with it. |
| Low (new last pass) — CLI narrowing invisible to the config guard | 🟡 Partially. Points 2 and 3 done: the comment at lines 94-96 now says explicitly that the check "bounds the config, not every route in" and names the unit script, and narrowingKeys/toEqual([]) gives a failure message that names the offending key instead of expected undefined, received [...]. testRegex is in the list with the mutual-exclusivity note. Point 1 — dropping the positional — was not done; see below. |
Also re-verified in the current tree: PACKAGE_ROOT (__dirname is <pkg>/dist/test, so ../.. is right), compiledCounterpart against tsconfig.json (rootDir: ".", outDir: "dist"), micromatch's brace support for {test,spec}, and micromatch: "^4.0.8" at package.json:67 matching an existing yarn.lock key.
I could not run the suite — node_modules isn't installed in this environment and a monorepo yarn install is out of budget for a review pass. CI is the check on the 238-green figure.
Remaining finding
Low — package.json:18 still passes dist/test positionally.
"unit": "CUBE_JS_NATIVE_API_GATEWAY_INTERNAL=true jest --coverage --forceExit dist/test"
Now that the guard documents this as out of scope, the code and the comment agree — so this is no longer a correctness gap in the test, just a leftover. It's still worth dropping, for one reason that isn't about the guard: the PR body says the suite is "now green with no path argument, so the config carries the target rather than the script compensating for it", and the script does still carry the argument. Either drop it or reword that bullet, so a reader auditing the claim in six months doesn't conclude the fix was never finished. Functionally it filters nothing today — the positional is a superset of testMatch. Fix this →
Praise
- Choosing the relative-path form of the win32 fix over the absolute-path one closes two failure modes with one change, and the comment explains the asymmetry (vacuous pass on one assertion, all-files-red on the other) rather than just asserting the fix. That asymmetry is the part a future reader would otherwise have to rediscover.
- The
afterEachcomment says why body-local restore is wrong — "one genuine failure would leaveDate.nowmocked for every test after it" — which is the durable form. Someone re-adding an inlinerestoreAllMocks()now has to argue with a stated reason. - Answering the CLI-narrowing point by scoping the guard's claim in a comment, rather than pretending the check is total, is the right call. A guard that overstates its reach is worse than one that's honest about its edge.
- Still the case that deriving from the real
jestConfig.testMatchthrough micromatch — jest's own glob library — rather than restating the pattern is what makes this survive edits, and thattoBeGreaterThan(5)keeps the walk from going vacuous.
Not concerns
- Security, performance, auth: nothing in scope — test infrastructure only, no runtime code paths touched.
micromatchas a direct devDependency: fine, and not published (filesis["README.md", "dist/src/*"]).require()+eslint-disablefor micromatch: justified, no@types/micromatchin the tree.- The local-time
new Date(2021, 1, 15, …)constructions indate-parser.test.tsare TZ-dependent, but that's unchanged from master — out of scope here. - Docs: no user-facing surface. The
jest.config.jscomment documenting thejest date-parservs.jest test/date-parser.test.tsgotcha is the right place for it. - Not migrating to ts-jest: agreed, out of scope, and the PR is explicit.
Resolved 5 of my own threads (the medium plus four lows) as no longer applicable. Left the CLI-narrowing thread unresolved, since the positional is still in the script.
Summary
packages/cubejs-api-gateway'sunitscript targetsdist/test, but its jest config never said so — it extendsjest.base.config, which sets notestMatch, so jest's default was in effect and collected the TypeScript/ESM sources undertest/. Nothing transforms them, so each one dies withSyntaxError: Cannot use import statement outside a module; the script's path argument was the only thing keeping the run green. Two of those sources were plain.js(test/date-parser.test.js,test/normalize-query-filters-dates.test.js), which made them look editable when in fact only their compiled copies indist/testever run..jsfiles suggest: the.tstests fail from source in exactly the same way. No test in this package runs from source. So the fix is not "convert two files" — it is to make the config express thedisttarget it always had, and enforce it.testMatchis now pinned to<rootDir>/dist/test/**/*.{test,spec}.js, and the two.jsfiles are converted to.tsso the directory is uniform..spec.is in the pattern because jest's defaulttestMatchaccepted it: pinning without it would silently drop any future spec file, which is the same defect in a new costume.test/test-collection.test.tsguards it: every test file on disk must have a compiled counterpart thattestMatchactually collects, and no source may be collected directly. It evaluates the realtestMatchthroughmicromatch— the glob library jest itself uses — rather than restating the pattern, so it stays honest if someone edits it. The walk covers the whole package, not justtest/, because a test added undersrc/compiles todist/src/and would be silently uncollected there too. It also asserts that no narrowing key (testPathIgnorePatterns,modulePathIgnorePatterns,roots,testRegex) has been added behindtestMatch— the routine "temporarily ignore the flaky suite" edit would otherwise reintroduce exactly this bug past a guard that reasons fromtestMatchalone. And it closes the reverse direction: a compiled test whose source is gone.tscis incremental and onlybuilddoesrm -rf dist, so renaming or deleting a test leaves its olddistcopy collected forever, running code that no longer exists — this file's own subject, mirrored. Matching is done on package-relative posix paths so the guard doesn't silently match nothing on Windows (micromatch reads\as an escape), which would have failed asymmetrically: one assertion passing vacuously while the other named all 14 files.date-parser.test.jsdidDate.now = jest.fn()...thenDate.now.mockRestore(), andmockRestore()on a barejest.fn()restores nothing — soDate.nowstayed mocked for the rest of the file, returning aDatewhereDate.now()must return a number. Both are nowjest.spyOn(Date, 'now'), restored from a singleafterEachrather than inline after eachexpect. Inline was still a leak on the failure path only: measured, one injected failure took 3 tests red (two of them unrelated ones reading the real clock); fromafterEachthe same injection takes exactly 1.micromatchmoves intodevDependencies; it was already in the tree as a transitive dep of jest, and^4.0.8resolves to an existingyarn.lockentry, so--frozen-lockfileneeds no lockfile change.Testing
dist/is deliberate rather than incidental — 14 packages do it,test/snapshotResolver.jsis built around it, andpackage.jsonshipsmain: dist/src/index.js, so the compiled copies are the artifact users get. Migrating this package to ts-jest so its tests run from source is a real alternative and a much larger change; it is not attempted here.One consequence to know about:
npx jest date-parserworks, but naming the source path with its extension (npx jest test/date-parser.test.ts) reports "No tests found", because the path that exists at run time is the compiled.js. The jest config documents this.Test plan
yarn tsc && yarn unitin the package: 14 suites / 240 tests / 23 snapshots green — the 13/235 pre-change baseline plus the guard's 5 tests. Also green with no path argument, which is the point: the config now carries the target instead of the script's positional compensating for it. The positional stays inunitas fail-safe redundancy and for uniformity with the other 13 dist-testing packages — it is a superset of whattestMatchcollects, so it filters nothing today. It is a second, config-invisible route to narrowing collection, and the guard says so rather than pretendingtestMatchis the only one.testMatch(which restores the original bug), deleting a compiled counterpart, adding a test undersrc/, adding a.test.mts, adding atestPathIgnorePatterns, and leaving a compiled test with no source each turn it red, naming the offending file or key. Widening the pattern todist/**/*.{test,spec}.jskeeps it green, confirming it follows the config instead of a hardcoded copy.date-parsertests verified to still bite, not merely pass: mutatingsrc/date-parser.js(end.add(-1, match[3])→-2, thelast N <unit>branch) turns 5 of them red, including theDate.now-mocking ones, and green again on restore. Mutating the singularlast <unit>branch instead turns nothing red — that line is genuinely uncovered, which is a pre-existing coverage gap rather than evidence the tests are inert.eslint0 errors / 21 warnings, identical to master. The two converted files enter lint scope for the first time (packagelintiseslint "**/*.{ts,tsx}") and add no warnings of their own.yarn install --frozen-lockfilesucceeds with noyarn.lockchange.