Skip to content

test(server): promote 3 QA server-layer contract anchors - #1972

Merged
kriszyp merged 11 commits into
mainfrom
kris/qa-promote-server-contracts
Aug 4, 2026
Merged

test(server): promote 3 QA server-layer contract anchors#1972
kriszyp merged 11 commits into
mainfrom
kris/qa-promote-server-contracts

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

Promotes gated qa-explorer probes into integrationTests/server/ as standalone (isolation-required) suites. Test-only additions — no runtime code changes.

  • qa702-sse-event-data.test.ts (QA-702 / P-478) — pins the Resource SSE contract: an 8-case event.data payload matrix (undefined/null/''/0/false/object/nested/~300KB string) never crashes a worker and the connection stays healthy, plus the F-133 mid-stream-throw regression leg (a generator that throws mid-stream delivers prior events then closes promptly, not hangs) and a liveness canary. Anchors merged fix: guard SSE writes against undefined event data (#1724) #1863 and F-133. Measured: 10/10 passing, ~6.3s.
  • qa577-upgrade-builtins.test.ts (QA-577 / P-373) — pins the in-place-upgrade built-in-component config backfill: booting over a config lacking a now-built-in component backfills exactly one top-level key with an Activated built-in component(s)... log; a second boot over the same data dir is idempotent (no duplicate/re-log); a fresh install with the key already present is a no-op; OSS-core (no built-ins registered) never grows the Pro-only key. Regression anchor for merged fix(upgrade): activate new built-in components on in-place-upgraded configs #1814. Measured: 4/4 passing, ~12.3s.
  • qa579-cli-exit-codes.test.ts (QA-579 / P-374) — pins CLI failure paths exiting non-zero via a 5-cell exit-code matrix (bad config, unreachable instance, operation timeout, failed op, success control) spawning the real dist/bin/harper.js, with a hard-kill safety net so a regression-to-hang surfaces as an explicit kill, not a false exit 0. Regression anchor for merged fix: ensure CLI failure paths exit non-zero, including operation timeouts #1801. Measured: 5/5 passing, ~6.1s.

Dropped from this batch: P-440 (qa665-ops-surface, gh#1893 ops-API failure-shape contract). Cold-rerun on current main showed 2 of 5 sub-tests failing: the two probes hardcode get_deployment_payload/delete_deployment_payload as "documented-but-absent" operations, but merged commit 3d329a526 ("Implement get_deployment_payload and delete_deployment_payload operations", closing #1893) already registered both on main — they now return a different (still-correct) 4xx shape instead of the generic "Operation not found" the test asserts on. The test's core premise is stale rather than flaky; fixing it would mean picking new genuinely-absent operation names and rewriting those assertions, which is a content change, not a promotion-gate fix, so it was left out rather than fudged to pass.

All three kept specs are requires-isolation (SSE worker/crash assertions, an actual re-boot of the instance, and spawning the real CLI binary), so each stays a standalone file rather than being appended into an existing suite.

Test plan

  • Cold-reran each candidate from a scratch copy on current main (base 3e317c7f5) before promoting
  • npx prettier --check and npx oxlint --format stylish --deny-warnings clean on all touched files (one --write pass needed for a long line in qa702-sse-event-data/resources.js; re-ran the spec after to confirm behavior-neutral)
  • Re-ran all three specs once more from their final integrationTests/server/ location — all green
  • No restartHttpWorkers() against pre-installed fixtures (replaced 4 real calls in qa577 with a direct /Marker/ readiness poll, matching integrationTests/database/eviction-secondary-index.test.ts's pattern)
  • No hard-coded developer paths in any promoted spec or fixture

🤖 Promoted via qa-explorer, generated with Claude Opus 5.

@kriszyp kriszyp changed the title test(server): promote 4 QA server-layer contract anchors test(server): promote 3 QA server-layer contract anchors Jul 28, 2026

@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 introduces three new integration test suites to verify built-in component backfilling on upgrades (QA-577), CLI exit codes on failure paths (QA-579), and SSE event-data payload handling and mid-stream error recovery (QA-702). The review feedback identifies several critical robustness improvements for these tests to prevent potential hangs and unhandled errors on CI. Specifically, it is recommended to wrap the wedged server test in a try-finally block to guarantee cleanup, guard the scratch directory cleanup in the teardown hook against uninitialized states, add error handlers to server listeners to prevent indefinite hangs, and introduce a timeout to the readiness polling request.

Comment thread integrationTests/server/qa579-cli-exit-codes.test.ts
Comment thread integrationTests/server/qa579-cli-exit-codes.test.ts
Comment thread integrationTests/server/qa579-cli-exit-codes.test.ts
Comment thread integrationTests/server/qa702-sse-event-data.test.ts
@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

Comment thread integrationTests/server/qa579-cli-exit-codes.test.ts Outdated
Comment thread integrationTests/server/qa579-cli-exit-codes.test.ts
@kriszyp
kriszyp force-pushed the kris/qa-promote-server-contracts branch 2 times, most recently from edd2502 to ed29b46 Compare July 31, 2026 00:05
kriszyp and others added 6 commits July 31, 2026 07:04
Promotes gated qa-explorer probes into integrationTests/server/ as
standalone (isolation-required) suites:

- qa702-sse-event-data: Resource SSE contract — 8-case event-data payload
  matrix never crashes a worker, plus the F-133 mid-stream-throw
  regression leg and a liveness canary (anchors merged #1863 / F-133).
- qa577-upgrade-builtins: in-place-upgrade built-in-component config
  backfill, idempotent across reboots, no-op on fresh installs and on
  OSS-core (regression anchor for merged PR #1814).
- qa579-cli-exit-codes: 5-cell CLI exit-code matrix spawning the real
  dist/bin/harper.js, with a hard-kill safety net (regression anchor for
  merged PR #1801).

Test-only additions; no runtime code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t SSE uncaught-exception contract

Pre-push review (codex, independent) on the previous commit found two gaps:

- unitTests/resources/auditLog.test.js: the cherry-picked deflake capped the
  event-count poll at 200ms (20 x 10ms), which harper#1966 documents as
  already observed to still flake under a loaded runner. Switch to the
  shared waitFor() helper (used elsewhere in this same file/suite) with a
  2000ms budget instead of a hand-rolled fixed-count loop.

- integrationTests/server/qa702-sse-event-data.test.ts: case (b), the F-133
  mid-stream-throw re-check, computed newUncaught but only logged it — the
  sibling case (a) a few lines above asserts the identical "no NEW
  uncaughtException" contract. Added the missing strictEqual so a regression
  here actually fails the test instead of only appearing in test output.
  Verified locally: 10/10 passing, newUncaught=0 in practice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…the timing

Format Check failed on the two files touched by the earlier commits (no local
pre-commit hook available in this environment to catch it) — ran `prettier --write`.

The audit-log deflake from the previous commit was still flaky on Node v22 even
at a 2000ms waitFor budget, which ruled out "just needs more time." Traced the
actual drop: Table.ts's subscription dispatcher intentionally discards an event
if the record's version has already moved past it ("out of order event, with
old update, don't send anything") so it always delivers the LATEST value per id
rather than every intermediate write. In this test, id 2 is written twice in a
row (put, then put-changed); under load the first write's notification can lose
the race and get coalesced away entirely — a real drop, not a delay, so no
amount of polling recovers it. The only invariant the dispatcher actually
guarantees is one event per distinct id touched (id 1's final state is the
delete, id 2's is the last put) — 2 events, not >2. Asserting `>= 2` instead of
`> 2` matches that real guarantee.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… count

Independent review (codex) on the previous commit flagged that events.length >= 2
proves the right count but not the invariant the comment documents — it would
also pass if, say, both delivered events happened to be for the same id. Assert
the actual per-id contract: the last delivered event for id 1 must be its delete,
and for id 2 must carry the latest put's value.

Also confirmed the review's other finding (F-133 uncaught-exception assertion
under the Bun CI matrix) empirically holds: the Bun shard that exercises this
test already ran on the previous commit's SHA and passed (newUncaught=0), so no
change needed there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… count

Independent review (codex) on the previous commit caught a real race I
introduced: waitFor(() => events.length >= 2) can resolve on two INTERMEDIATE
events (e.g. id 1's put plus id 2's first put) that satisfy the count but
aren't the final per-id state the assertions below check for — which would
make those assertions race the delete/latest-put events that hadn't arrived
yet. Wait for the actual condition (id 1's delete AND id 2's latest put both
present) instead of a proxy count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…p/codex)

qa579-cli-exit-codes.test.ts:
- wrap Cell 3's blackhole-server test body in try/finally so an assertion
  failure can't leave it listening and hang the CI event loop (gemini, high)
- guard the after() scratchHome rm() so a before() failure doesn't mask
  itself behind a TypeError from rm(undefined) (gemini + claude bot)
- add an 'error' listener to probe.listen() so a bind failure rejects
  instead of hanging the promise (gemini + claude bot)
- set USERPROFILE alongside HOME so the CLI child's credential resolution
  is actually isolated on Windows, where os.homedir() reads USERPROFILE
  (kriszyp/codex)

qa702-sse-event-data.test.ts:
- add AbortSignal.timeout(5000) to the readiness-poll request so a hung
  server can't block the before() poll loop indefinitely (gemini)
- relabel the suite/header/comments: this suite cannot reach the actual
  #1863-guarded writeSSE() (confirmed sandboxing boundary), so it should
  not claim to be that fix's regression anchor. The real anchor already
  exists at the unit level (progressEmitter.test.js, which exercises
  writeSSE() directly with undefined/null). Reframed as its own contract
  test for the reachable sibling encoder (contentTypes.ts's
  text/event-stream serialize()) instead. Exercising the real Operations
  API progress stream, or unifying the two encoders, are both real
  production-code changes out of scope for a test-only PR — noted as an
  open question in the PR review rather than guessed at (kriszyp/codex)

qa577-upgrade-builtins.test.ts:
- readBootLog() now falls back to the real default hdb.log location
  (${dataRootDir}/log/hdb.log, per validation/configValidator.ts's
  DEFAULT_LOG_FOLDER) instead of startupOutput.stdout, which can never
  contain the activation log line since the harness always boots with
  --LOGGING_STDSTREAMS=false
- the 2nd-boot idempotence test now diffs only the log bytes appended
  after restart, since that log file is append-only across the restart
  and the 1st boot's activation line would otherwise still be present
  and fail the "no activation log on 2nd boot" assertion even when the
  backfill correctly did not re-fire (kriszyp/codex)

Build, full auditLog.test.js unit suite (22/22), and all 3 promoted
integration suites (19/19) pass locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- qa579-cli-exit-codes.test.ts: use killSignal SIGKILL on the spawnSync
  safety net (SIGTERM alone isn't a hard ceiling against a hang regression
  that installs/ignores a TERM handler); pin Cell 1's assertion to the
  actual deterministic YAML-parse-error message instead of "any non-empty
  output"
- qa702-sse-event-data.test.ts: raise case (a)'s outer timeout 20s -> 35s
  (two sequential 15s consumeSse() calls plus a settle sleep exceeded the
  old budget); assert the actual shipped shape of the F-133 mid-stream-
  throw close (closed && !ended) instead of only logging a verdict; drop
  a bogus `logging: { console: true }` config key (not a real schema key,
  silently dropped by allowUnknown); reference harper#2026 in the pinned
  falsy-data-omission assertion/comments so it reads as a known, tracked
  defect rather than an intended contract
- qa577-upgrade-builtins.test.ts: pin HARPER_BUILTIN_COMPONENTS: '' in
  both negative controls, since the harness spawns with
  `{ ...process.env, ...env }` and an ambient value (e.g. this suite
  running inside harper-pro's own CI) would otherwise flow through and
  falsify what the control is asserting; fix stale "only backfilled key
  today" comment (UPGRADE_BACKFILL_BUILTIN_KEYS is now ['secretCustody',
  'waf'])
- auditLog.test.js: snapshot lastEventById() once after the wait resolves
  instead of recomputing it in each assertion (TOCTOU-shaped, though not
  currently triggerable)

Filed harper#2026 (SSE encoder drops legitimate falsy data / id:0 -- the
production defect the qa702 matrix pins) and harper#2027 (config parse-
errors silently discarded on non-upgrade boots -- out of scope for this
test-only PR, filed for follow-up) per the domain review's findings.

Build, unit (22/22), and integration (19/19) all still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…, cell 4)

Three items the domain review flagged as worth acting on before merge:

- qa702-sse-event-data.test.ts: the before() hook booted with
  logging.level: 'error', which makes the uncaughtException-delta oracle
  silently vacuous -- a healthy boot logs nothing at that level, hdb.log
  may never even be created, and "file absent" is then indistinguishable
  from "logPath drifted to the wrong place." Every uncaughtAfter/Before
  assertion in the suite would still pass either way. Switched to 'info'
  (guarantees real boot content) and added a positive-control assertion
  in before() that hdb.log actually has content.
- qa702-sse-event-data.test.ts: the readiness poll's while loop had no
  failure path -- it just fell out of the 30s deadline and proceeded, so
  a component-load failure would surface as 9 confusing "expected a
  payload event block" test failures instead of one clear "probe never
  became ready." Added the same throw-on-timeout qa577 already had.
- harper#2026 ("SSE encoder drops falsy data (0/false/'') and id: 0")
  only had its data-half pinned by this suite. Added IdZeroPayload
  (resources.js) + a dedicated test case asserting id: 0 is also
  silently omitted, so #2026's eventual fix updates both halves.

Plus the two comment-accuracy items both codex and grok independently
flagged: qa702-sse-event-data/{resources.js,schema.graphql} still called
this suite a "#1863 anchor" (the test file's own header already correctly
says otherwise) -- reworded for consistency; and the F-133 throw-path
assertion checked only a data-block count where its own comment claimed
exact contents -- now asserts the actual pre-throw values
(`{"n":0}`,`{"n":1}` as a prefix) via deepStrictEqual.

Also addressed the remaining nits: qa579 cell 4 now pins the rejected
table name and rules out an unhandled-rejection stack trace (previously
only checked the exit-code sign); qa579 cell 5's near-tautological
`.includes('data')` now matches the actual schema/table; auditLog.test.js
had a comment asserting something the code doesn't do (fixed the wording,
not the code); qa577's latent bare-packageIdentifier-crash comment named
one dereference site when there are two, and claimed "reported separately"
when nothing had actually been filed -- filed harper#2028 and corrected
the comment to name both sites and point at the actual invariant fix.

Left open (not fixed here, see PR Review section): qa577's inert
built-in stand-in doesn't prove the in-memory config-mirror activation
path was exercised on first boot -- the domain reviewer downgraded this
to minor (test-only PR, coverage gap not a production risk) and it needs
a stand-in with an observable load-time side effect, which is a fixture
redesign beyond a review-comment-sized change.

Build, unit (22/22), and integration (20/20, +1 for the new id:0 case)
all still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kriszyp
kriszyp force-pushed the kris/qa-promote-server-contracts branch from ed29b46 to 5a207af Compare July 31, 2026 14:16
@kriszyp
kriszyp marked this pull request as ready for review July 31, 2026 14:16
Comment thread integrationTests/server/qa577-upgrade-builtins.test.ts Outdated
kriszyp and others added 3 commits July 31, 2026 08:53
Suite 2 and Suite 3 asserted hdb.log does NOT contain the activation-log
snippet, but readBootLog() returns '' when hdb.log is missing, so the
assertion would pass trivially if the log file was never created or the
path drifted. Add a positive-control assertion (log has real 'info'-level
boot content) before each negative check, mirroring the established
pattern in qa702-sse-event-data.test.ts.

Addresses cb1kenobi/Barber AI review feedback on PR #1972.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The integration harness already forces --LOGGING_LEVEL=debug (a superset
of info), so the explicit logging.level: 'info' doesn't "enable" content
that wasn't already there — it's a defensive pin against that harness
default changing, not the reason the positive control passes. Caught by
the independent pre-push review (round 2, delta).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… divergence)

`b: ThrowGen (throws after 2 of 5) over SSE` was the sole failure on
`Integration Tests 5/6 (uWS HTTP)`; every other shard was green. Rather
than assume the assertion over-reached, QA-886 characterized the two
servers byte-for-byte on the identical workload.

Both deliver exactly the two pre-throw events (`{"n":0}`, `{"n":1}`),
byte-identical modulo a uWS-only leading `:\n\n` header-flush comment.
They diverge only at termination:

- Node (`server/http.ts` pipeBodyToResponse, ~426-461) closes the socket
  WITHOUT the terminal `0\r\n\r\n` chunk, deliberately -- its own comment
  says this "correctly signals a failed/truncated transfer... instead of
  implying it completed". The incomplete chunked framing is the only
  signal a client gets, and it is the intended one.
- uWS (`server/serverHelpers/uwsServer.ts` streamResponse, ~340-356)
  routes the source's 'error' and 'end' handlers through the SAME
  `finish(true)` -> `res.end()` path, so it DOES write the terminal
  chunk. The wire response becomes byte-indistinguishable from a
  generator that legitimately finished: a mid-stream failure is silently
  presented to the client as success.

So the spec was right and the uWS path is wrong. `HttpResponse.close()`
is available and is the correct primitive for the error branch; that is a
product fix, tracked separately as F-272 and not made here.

This change pins BOTH shapes explicitly rather than skipping under uWS,
so the divergence stays visible in the suite and cannot drift or be
"fixed" in the wrong direction unnoticed. Verified locally at
`c28e5f83f`: 11/11 green under Node and 11/11 under HARPER_UWS_HTTP=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kriszyp
kriszyp merged commit b4196a3 into main Aug 4, 2026
43 checks passed
@kriszyp
kriszyp deleted the kris/qa-promote-server-contracts branch August 4, 2026 11:40
kriszyp added a commit that referenced this pull request Aug 4, 2026
Suite 2 and Suite 3 asserted hdb.log does NOT contain the activation-log
snippet, but readBootLog() returns '' when hdb.log is missing, so the
assertion would pass trivially if the log file was never created or the
path drifted. Add a positive-control assertion (log has real 'info'-level
boot content) before each negative check, mirroring the established
pattern in qa702-sse-event-data.test.ts.

Addresses cb1kenobi/Barber AI review feedback on PR #1972.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

4 participants