test(apitests): await table clears on re-setup, make check-operations assertions diagnostic#1895
Conversation
…ostic `sql returns all attributes of four property object` failed intermittently on main (Node 24 at db03c47, Node 22 at 07c2bbc9). It is a flake, not a regression: re-running the failed jobs on the same commit with no code change turns them green, and the CI log shows the query taking the legacy engine ("SQL engine v2 fallback: scan on data.FourProp has no usable index condition"), a path that never passes a `sort` to Table.search — so the primary-key sort change in db03c47 is not on it. Two test-harness fixes: - setupTestApp() re-seeds the tables on every call after the first, but `Table.clear()` returns a promise (both `lmdb` and `@harperfast/rocksdb-js` expose `clear(): Promise<void>`) and the four calls were not awaited. Nothing ordered the clear against the PUTs that immediately follow it, so a late clear can wipe the records it was supposed to make room for — which presents, much later, as an empty/short result in whichever suite reads the table next. - The `check operations` assertions were bare `assert(data.some(...))`, so a failure reports `AssertionError` and nothing else. That is why the CI failure above could not be told apart from "returned no rows" without hours of work. Each now reports the status and (truncated) body. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request improves test reliability and diagnostics. In setupTestApp.mjs, it fixes a race condition by awaiting the asynchronous Table.clear() operations using Promise.all before writing new records. In RESTProperties-test.mjs, it introduces a preview helper function to include response status and body details in assertion failure messages. The reviewer suggested wrapping the JSON.stringify call inside the preview helper in a try/catch block to handle potential serialization errors (such as from BigInt values or circular structures) that could otherwise mask the actual test failures.
| function preview(response) { | ||
| return `status ${response.status}, body ${JSON.stringify(response.data)?.slice(0, 2000)}`; | ||
| } |
There was a problem hiding this comment.
In Harper, database records can contain BigInt values (which JSON.stringify cannot serialize natively and will throw a TypeError), or other complex/circular structures. If JSON.stringify(response.data) throws an exception, it will mask the actual assertion failure with a serialization error, making the test failure harder to diagnose. Wrapping the serialization in a try/catch block ensures that the preview helper is robust and always returns a diagnostic string.
function preview(response) {
let bodyPreview;
try {
bodyPreview = JSON.stringify(response.data)?.slice(0, 2000);
} catch (e) {
bodyPreview = `[Serialization failed: ${e.message}]`;
}
return `status ${response.status}, body ${bodyPreview}`;
}References
- Do not rely solely on optional chaining or direct property access on arbitrary objects in critical paths like logging or error serialization; wrap them in try/catch blocks to prevent application crashes or masked errors.
|
Blocker found: a duplicated |
Co-authored-by: Chris Barber <chris@harperdb.io>
| assert( | ||
| assert( | ||
| Array.isArray(response.data) && response.data.some((record) => record.title === 'title0'), | ||
| `SELECT * FROM data.FourProp did not return title0: ${preview(response)}` | ||
| ); | ||
| `SELECT * FROM data.FourProp did not return title0: ${preview(response)}` | ||
| ); |
There was a problem hiding this comment.
Blocker: syntax error — duplicated assert( breaks the file
This is a merge/suggestion-apply artifact: assert( is opened twice with no comma between the inner call and the trailing template literal, and the closing );/template literal are duplicated. This is invalid JavaScript — the file fails to parse, so RESTProperties-test.mjs (and any mocha run that loads it) throws a SyntaxError before a single test executes. This has already been merged to main.
| assert( | |
| assert( | |
| Array.isArray(response.data) && response.data.some((record) => record.title === 'title0'), | |
| `SELECT * FROM data.FourProp did not return title0: ${preview(response)}` | |
| ); | |
| `SELECT * FROM data.FourProp did not return title0: ${preview(response)}` | |
| ); | |
| assert( | |
| Array.isArray(response.data) && response.data.some((record) => record.title === 'title0'), | |
| `SELECT * FROM data.FourProp did not return title0: ${preview(response)}` | |
| ); |
What / why
Main's Unit Test job went red on
unitTests/apiTests/RESTProperties-test.mjs→sql returns all attributes of four property object, and the failure was attributed todb03c47 (
fix(resources): serve a sort on the primary key from primary-store order, thefix for #773). That attribution is wrong, and the test is flaky, not deterministic:
29915353343(SHA07c2bbc9, main HEAD) withno code change turned them green. Main is green now.
db03c474only Node 24 failed (this test); at07c2bbc9Node 22 failed this test while Node 24 failed
test MQTT connections and commands > subscribe root with history; at604f4a97only Node 26 failed, onTable.getRecordCount > does not take a key count when the scan completes within the time budget. Runs before db03c47 were red on yet other tests.(
[sql-engine]: SQL engine v2 fallback: scan on "data.FourProp" has no usable index condition). The legacySELECT *path reachesTable.searchviaResourceBridge.searchByValue, which passessort: searchObject.sort— alwaysundefinedhere. db03c47 only changed theif (sort)branch, so it is not on thiscode path at all.
So this PR does not "fix the regression" (there isn't one). It fixes the two things that
made this class of failure both possible and undiagnosable.
Changes
1.
setupTestApp()did not await the table clears. Every call after the first re-seedsthe tables, and
Table.clear()returns a promise (clear(): Promise<void>on bothlmdband
@harperfast/rocksdb-js). The four calls were fire-and-forget, immediately followed bythe awaited PUTs that re-create the records — nothing orders the clear against them, so a
late clear can wipe the records it was meant to make room for. That presents much later as
an empty or short result in whichever suite reads the table next, which is exactly the
shape of the observed failure. Now awaited.
2. The
check operationsassertions carried no message.assert(data.some(...))failswith a bare
AssertionError— no status, no body, no indication whether the array wasempty or merely missing an attribute. That ambiguity is most of why diagnosing this cost
hours against a CI-only failure. All five now report the status and a truncated body.
Test notes
unitTests/apiTests/RESTProperties-test.mjs: the 14 REST tests pass locally; the 7check operationstests cannot run in this worktree because the operations API does notbind (see the caveat below) — they are exercised in CI.
RBAC-test.mjs+RESTProperties-test.mjstogether to exercise theserverStarted === truebranch that change 1 touches; no behavior change beyond theordering.
npm run test:unit:main: 3767 passing, 10 failing — all 10 inunitTests/components/globalIsolation.test.js, pre-existing in this worktree (itsfixture
node_modulesare not installed) and untouched by this change.integrationTests/apiTests/rest.test.mjsare unaffected.Open concerns
the failure locally (see below). It is correct regardless: an unawaited async clear
racing the writes that follow it is a bug whether or not it is this bug. If the flake
recurs, change 2 means the next CI failure will actually say what came back.
subscribe root with history,Table.getRecordCounttime-budget) look like they deservetheir own issues.
Environment caveat found while working this
On Linux a unix socket path is capped at 107 bytes. The unit-test harness derives the
operations-API domain socket from the repo path (
unitTests/envDir/<pid>/operations-server),so from a long checkout path — e.g. any
.claude/worktrees/<name>worktree —listenfails with
EINVAL. That throw happens insideoperationsServer's registrationtry, soit takes down the TCP listener registration too: the whole operations API silently
disappears and every ops-API test fails with
ECONNREFUSED, with no error surfaced by thedefault test logging. That is why the failing test could not be run locally at all. Same
hazard applies to a real deployment with a long
rootPath. Not fixed here — flagged fortriage.
Refs #773
🤖 Generated with Claude Code (Claude Opus 4.8)