Skip to content

test(apitests): await table clears on re-setup, make check-operations assertions diagnostic#1895

Merged
kriszyp merged 2 commits into
mainfrom
test/apitests-clear-race-and-flake-diagnostics
Jul 23, 2026
Merged

test(apitests): await table clears on re-setup, make check-operations assertions diagnostic#1895
kriszyp merged 2 commits into
mainfrom
test/apitests-clear-race-and-flake-diagnostics

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 22, 2026

Copy link
Copy Markdown
Member

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 to
db03c47 (fix(resources): serve a sort on the primary key from primary-store order, the
fix for #773). That attribution is wrong, and the test is flaky, not deterministic:

  • Re-running the failed jobs only of run 29915353343 (SHA 07c2bbc9, main HEAD) with
    no code change turned them green. Main is green now.
  • The failures move around: at db03c474 only Node 24 failed (this test); at 07c2bbc9
    Node 22 failed this test while Node 24 failed test MQTT connections and commands > subscribe root with history; at 604f4a97 only Node 26 failed, on
    Table.getRecordCount > does not take a key count when the scan completes within the time budget. Runs before db03c47 were red on yet other tests.
  • The CI log for the failing case shows the query taking the legacy engine
    ([sql-engine]: SQL engine v2 fallback: scan on "data.FourProp" has no usable index condition). The legacy SELECT * path reaches Table.search via
    ResourceBridge.searchByValue, which passes sort: searchObject.sort — always
    undefined here. db03c47 only changed the if (sort) branch, so it is not on this
    code 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-seeds
the tables, and Table.clear() returns a promise (clear(): Promise<void> on both lmdb
and @harperfast/rocksdb-js). The four calls were fire-and-forget, immediately followed by
the 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 operations assertions carried no message. assert(data.some(...)) fails
with a bare AssertionError — no status, no body, no indication whether the array was
empty 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 7
    check operations tests cannot run in this worktree because the operations API does not
    bind (see the caveat below) — they are exercised in CI.
  • Ran RBAC-test.mjs + RESTProperties-test.mjs together to exercise the
    serverStarted === true branch that change 1 touches; no behavior change beyond the
    ordering.
  • npm run test:unit:main: 3767 passing, 10 failing — all 10 in
    unitTests/components/globalIsolation.test.js, pre-existing in this worktree (its
    fixture node_modules are not installed) and untouched by this change.
  • No product code is touched, so the table.search({ sort: ... }) without an indexed condition throws "id is not indexed and not combined with any other conditions" #773 primary-key-sort regression tests in
    integrationTests/apiTests/rest.test.mjs are unaffected.

Open concerns

  • Change 1 is a plausible cause of the flake, not a proven one — I could not reproduce
    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.
  • Worth deciding separately: a couple of the other unit-test flakes (MQTT
    subscribe root with history, Table.getRecordCount time-budget) look like they deserve
    their 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 — listen
fails with EINVAL. That throw happens inside operationsServer's registration try, so
it 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 the
default 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 for
triage.

Refs #773

🤖 Generated with Claude Code (Claude Opus 4.8)

…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>

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment on lines +10 to +12
function preview(response) {
return `status ${response.status}, body ${JSON.stringify(response.data)?.slice(0, 2000)}`;
}

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.

medium

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
  1. 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.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Blocker found: a duplicated assert( in unitTests/apiTests/RESTProperties-test.mjs (lines 328-334) is a syntax error - the file (and thus the whole mocha suite loading it) fails to parse. This landed in the last commit (c5dcaa212, applying a suggested-change) and is already merged to main. See inline comment for the fix.

@kriszyp
kriszyp marked this pull request as ready for review July 22, 2026 17:26
Comment thread unitTests/apiTests/RESTProperties-test.mjs Outdated
Co-authored-by: Chris Barber <chris@harperdb.io>
@kriszyp
kriszyp merged commit 228e4bc into main Jul 23, 2026
13 of 21 checks passed
@kriszyp
kriszyp deleted the test/apitests-clear-race-and-flake-diagnostics branch July 23, 2026 12:33
Comment on lines +328 to +334
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)}`
);

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.

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.

Suggested change
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)}`
);

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