refactor(replay-test): extract packages/replay-test (#1478 P3b) - #1525
Conversation
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head This is not merge-ready yet: the draft explicitly leaves the package move, daemon-adapter split, R10 retarget, sharding/discovery split, and harness consolidation unfinished. Keep it draft and request final re-review once the titled extraction is complete. Residual risk: no separately authorized cross-vendor review was performed; Claude authorship is not an independent review. |
923a2e1 to
d598189
Compare
| // The scheduler mints attempt identity itself (#1478 P3b). This used to be wrapped in | ||
| // `resolveRequestTrackingId`, whose only job is to substitute a generated id for an empty | ||
| // one — and this template always contains `:test:`, so it is never empty and the wrapper | ||
| // always returned it unchanged. Dropping the dead call removes a `request/cancel.ts` | ||
| // import from the scheduler without altering a single produced id. | ||
| return `${requestId ?? suiteInvocationId}${shardPart}:test:${caseIndex + 1}:${path.basename(filePath)}:attempt:${attemptIndex + 1}`; |
There was a problem hiding this comment.
get rid of long comment, fix the code
There was a problem hiding this comment.
Fixed in cbb120d80 — the id builds from a segment list joined on ':', so the shape reads without prose. Output is byte-identical; the reporter characterizations assert exact session/attempt strings and pass unmodified.
Applied the same standard to four other docblocks in this PR that narrated what the code used to do rather than what it does. Kept the durable "why" (which side of the seam owns what); dropped the migration history, since git carries it.
Generated by Claude Code
|
Reviewed current head This remains an intentionally incomplete draft, not merge-ready yet. The remaining #1478 work still includes the actual |
|
… kernel `session-test-types.ts` reached `ReplayScriptMetadata['platform']` and `['target']` through `replay/script.ts` — the native `.ad` engine. A format-neutral scheduler must not name an engine module, and P5 relocates that engine into `packages/ad-replay` regardless, so the import had to go before the scheduler can move. Both members already resolve to neutral kernel types (`Exclude<PlatformSelector, 'web'>` and `DeviceTarget` from `@agent-device/kernel/device`), so this re-sources them directly and the manifest shape is unchanged. Only the import direction differs. First increment of P3b; the scheduler still has request-global, engine and daemon imports to port before the physical move. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
…request global The scheduler called `emitRequestProgress` in eight places, which reads a sink out of a request-global `AsyncLocalStorage`. That is ambient authority a format-neutral scheduler cannot hold once it lives in `packages/replay-test`, and #1505 recorded it as a shrink-only R10 entry. The host now injects the capability through the existing `ReplayTestRuntimeDependencies` seam established in P3a, so no new seam is invented. `session-replay.ts` supplies `emitProgress: emitRequestProgress`; `src/request/progress.ts` keeps the sink and its AsyncLocalStorage binding for every other caller. The port is deliberately narrower than `RequestProgressSink`: it accepts only `ReplayTestSuiteProgressEvent | ReplayTestProgressEvent`, so the scheduler is not handed the ability to emit `CommandProgressEvent`. Authority narrows again one hop down: `runReplayTestAttempt` spread the whole dependency bag but uses three of its members and never publishes progress, so it now takes `Pick<..., 'runReplay' | 'cleanupSession' | 'finalizeAttempt'>`. That is why no runtime test fixture needed changing — the attempt runtime never gained the capability in the first place. Also drops the last two `replay/script.ts` type references from `session-test-runtime.ts`, so the engine import is gone from that file too. Reporter contract preserved: `session-test-reporter-values.test.ts` and `session-test-reporter-values-maestro.test.ts` both pass unmodified (27 tests green across the five scheduler suites). Typecheck clean. Remaining scheduler boundary for P3b: `request/cancel.ts`, `replay/format.ts`, `replay/script.ts` in discovery, `session-store.ts`, `daemon/types.ts`, `replay-source-discovery.ts`, `core/dispatch*`, `utils/diagnostics.ts`. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
The scheduler called `isRequestCanceled(requestId)` in five places. That both reaches a request-global registry and forces the scheduler to name a daemon request id as the cancellation key — neither survives the move into `packages/replay-test`. The host now binds the predicate to its own request and passes `isCanceled: () => boolean`. The scheduler asks a question it is entitled to ask and learns nothing about how cancellation is tracked. `shouldStopReplayTestExecution` takes the capability rather than a request id, so no scheduler function threads a daemon identifier for this purpose any more. `session-test-attempt.ts` and `session-test.ts` no longer import `request/cancel.ts` at all. It remains in `session-test-runtime.ts`, which does something different — `registerRequestAbort`, `markRequestCanceled` and the parent-abort relay are cancellation *binding*, which the brief assigns to the daemon adapter, so that split is its own step. Behavior preserved: both pinned reporter characterizations pass unmodified, 32/33 across the five scheduler suites. The one failure is the pre-existing P2/#1506 discovery-ordering regression, unrelated and untouched here. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
…pt ids `buildReplayTestAttemptRequestId` wrapped its template in `resolveRequestTrackingId`, pulling `request/cancel.ts` into the scheduler. That wrapper substitutes a generated id only when its first argument is an empty string. The template here always contains `:test:`, so it is never empty and the wrapper always returned it unchanged — the call is unreachable in this path. Probed all three shapes (explicit request id, suite-id fallback with a shard, and degenerate empty inputs); every one returns the template verbatim. Removing it takes `request/cancel.ts` out of discovery without altering a single produced id. The scheduler mints attempt identity itself, which is what the brief asks for. Evidence the ids are byte-identical: the pinned reporter characterizations assert exact session strings such as `default:test:suite-reporter:1-02-retry:attempt-1` and pass unmodified — 30 tests green across the reporter, suite and discovery suites. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
…he host `session-test-runtime.ts` held the last two request-globals in the scheduler: `request/cancel.ts` (registerRequestAbort, markRequestCanceled, clearRequestCanceled, plus the parent-abort relay) and `utils/diagnostics.ts`. These are different in kind from the earlier ports. The brief gives the daemon adapter the job of mapping an attempt id to daemon request identifiers and *binding cancellation*, while timeout policy stays scheduler-owned. So the scheduler now receives a per-attempt capability with exactly two verbs — `cancel()` on timeout and `release()` when the attempt settles — and every registry interaction, including `relayReplayTestAbortFromParent`, moved to `session-replay.ts` next to the rest of the adapter. Diagnostics became a narrow publish capability for the same reason: `emitDiagnostic` reads a request-global scope. The level vocabulary is spelled out at the seam rather than imported, so nothing engine- or daemon-shaped crosses it. The runtime fixtures drive the real exported host binding rather than a stub. They assert cancellation through `isRequestCanceled`, and a stubbed binding would have kept those assertions passing while proving nothing. 24 tests green across the runtime, suite and both reporter characterizations, which pass unmodified. Typecheck, lint and oxfmt clean. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
…uler policy discoverReplayTestEntries expanded paths, read every file, and called both engines — readReplayScriptMetadata for .ad, inspectMaestroFlow for Maestro — plus resolveReplayFormat to choose between them. Four imports a format-neutral scheduler cannot hold. Inspection is now the host's discoverSources capability. What stays in the scheduler is the genuinely neutral half: which sources a --platform filter runs, which it skips and with what message, and the empty-suite error. The manifest carries exactly the four fields the scheduler consumes (platform, target, retries, timeoutMs) plus the reporter's title, per the brief's instruction not to add more without a demonstrated call site. The platform tag is what removes the last format leak. The filter used to ask resolveReplayFormat(...) === 'maestro' to decide whether a missing platform was disqualifying. It now reads a tag: caller-bound means the invocation supplies the platform, unspecified means the source declared none. Maestro is what caller-bound looks like from the scheduler's side, and the format cannot be recovered from it. Discovery tests drive the real inspection capability, writing actual .ad and Maestro sources — a stubbed host half would have kept them green while proving nothing about the composition they exist to pin. 35 tests green across discovery, suite, runtime and both reporter characterizations, which pass unmodified. The Maestro one is the direct check that titles still flow, since they now arrive via the manifest. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
…mments Review feedback on the attempt-id builder: the comment explained a deletion that git already records, and it sat above an opaque template literal. The id is now a segment list joined on ':', so its shape is readable without prose. Output is byte-identical — the reporter characterizations assert exact session and attempt strings and pass unmodified. Applied the same standard to four other docblocks in this PR that narrated what the code used to do rather than what it does. The durable 'why' stays: which side of the seam owns what, and why the vocabulary is neutral. The migration history goes, since git carries it and these docblocks will outlive the migration. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
buildReplayTestShardPlan called listDeviceInventory to discover what to shard across, and buildReplayTestShardFlags constructed daemon CommandFlags for the nested request. Inventory enumeration, allowlists, simulator set paths, explicit --device selectors and the too-few-devices error are host concerns; what is scheduler-owned is deciding how many shards exist and which entries each one runs. The scheduler now receives resolved shard targets through a capability. The target is neutral: id and name for session labels and progress metadata, plus platform and target, which are already kernel vocabulary. DeviceInfo no longer crosses into scheduling. One behavior note: an explicit --device selector could in principle name a web target, which is not a shardable device. That is now rejected with INVALID_ARGS rather than widening the neutral platform vocabulary to carry something the scheduler can never run. Implicit selection already filtered to mobile. 919 of 920 handler tests pass. The one failure, session-test-runner.test.ts 'binds each replay script to its declared platform metadata', fails identically on clean origin/main in this container and is unrelated: directory discovery walks with opendirSync/readSync and directory results are deduped but not sorted, while glob results are sorted, so suite order is filesystem-dependent. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Completes the P3b extraction. The scheduler, attempt runtime, discovery policy, sharding distribution, artifacts and neutral types now live in packages/replay-test/src/internal/, with one package-root export. The façade takes a neutral ReplayTestSuiteRequest and returns a tagged ReplayTestSuiteOutcome. DaemonRequest, DaemonResponse and CommandFlags no longer reach the scheduler; the adapter translates flags and meta in, and the outcome back to a daemon response. Eight flags were read by the scheduler and each became a field it owns. Host work moved to daemon adapters: source inspection (both engines and format routing), shard device binding and shard-flag parsing, and artifacts-dir home expansion, which is why the package can resolve paths without SessionStore. The one remaining shared concern was the timing trace: the host writes video lifecycle events into the same trace the scheduler owns. Rather than export a writer from the façade, each attempt hands the host an appendTimingEvent closure, so the trace format stays private and the authority is scoped to that attempt. Tests mirror the topology. Discovery tests split along the seam they now cross: ordering, traversal and routing are pinned host-side against real files, filtering policy is pinned in the package against fake sources. The runtime tests assert the scheduler's cancellation obligation (cancel once on timeout, always release) against a recording binding, and a new daemon test pins the adapter's half — registry entries, the parent-abort relay, and detach on release — so that coverage moved rather than disappeared. R10 retargeted to packages/replay-test/src/ and the zone ranked alongside maestro. R11 confirms zero root-src imports from the package. 914 of 915 handler and package tests pass. The one failure, session-test-runner 'binds each replay script to its declared platform metadata', fails identically on clean main here: directory discovery walks with opendirSync and dedupes without sorting, while globs sort, so suite order is filesystem-dependent. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Fallow flagged toReplayTestSuiteRequest at 14 cyclomatic in 18 lines. The
branches were self-inflicted: every req.flags?.x is one, and each optional
field was written as a conditional spread to avoid setting an undefined key.
exactOptionalPropertyTypes is not enabled, so assigning undefined to an
optional field is equivalent and the spreads bought nothing. Destructuring
flags once and extracting two flag readers removes most of the rest.
One correctness note on the simplification itself: the first version used
`artifactsDir && expandHome(...)`, which returns '' for an empty-string flag
where the previous code called expandHome(''). Replaced with an explicit
undefined check so the empty-string path is unchanged.
Refs #1478
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Both live journeys invoked the public test command and then re-derived the same value-contract assertions by hand — suite totals, per-script status, replay counts, non-empty JUnit. Those are claims about the published suite result and are identical on every platform, and they had already drifted: iOS iterated with readReplayCommands inline, Android cast data.tests at the call site. The shared helper owns exactly that boundary. It takes the caller's runStep rather than binding a context type, so it is not a platform-configured runner and cannot template a platform's journey. Everything a platform genuinely differs on stays with the caller: which scripts run, the retry policy (iOS 2, Android none — itself a claim worth keeping), which commands each script exercises, and the behavioral evidence. Both callers keep every verify* call they had. 67 lines removed, 15 added. Residual risk: this container has no iOS or Android devices, so the live suites could not be executed here. Typecheck and lint pass; the harness needs a run on real targets before the claim that behavior is unchanged is evidence rather than inference. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
The test wrote two scripts into a temp directory and assumed discovery would return them in creation order. Directory expansion deliberately preserves filesystem order to match Maestro — only glob expansion sorts, and 'preserves Maestro directory filesystem order' pins that with a mocked opendirSync. So the ordering contract is correct; this test's assumption about enumeration was not. It passes on CI, where small directories usually enumerate in creation order, and fails on filesystems that do not — identically on clean main, where the platform-to-script binding appears reversed. Pinning enumeration the way the discovery tests already do keeps the subject intact (each script binds to ITS declared platform, and session numbering follows discovery order) without depending on the host filesystem. The fs import became a default import because vi.spyOn cannot redefine an ESM namespace export. 915 of 915 handler and package tests now pass here. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
The spy I added restored only on the happy path and asserted on its argument inside the mock implementation. Either would misfire for anything else sharing the worker: an assertion thrown from inside fs, or a leaked global opendirSync, surfaces as a worker crash with no failed test rather than a readable failure. It now delegates to the real implementation for any directory but this suite's own, and restores in a finally. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
9922cd1 to
0691345
Compare
Live verification attempt — no evidence obtained, staying draftedBoth platforms failed upstream of the replay-suite step, so iOS ( Android ( Builds passed and cleanup completed on both. On the iOS failureTraced as far as the source allows from an environment without the built fixture:
So the runtime returned a status outside the union its own library declares, which points at expo-audio 56 on a newer iOS runtime, or a native/JS skew in the built fixture. The expectation has deliberately not been widened to accept both spellings. If This blocks all iOS live evidence, so it is on P5's path too, not only this PR's. Generated by Claude Code |
|
Reviewed exact head
After fixing these, rerun the exact package tests plus the aggregate gates. The draft also still needs the stated live iOS and Android public |
Review found the moved package tests were neither executed nor typechecked. They sat under packages/replay-test/test/, but vitest's unit-core lane includes packages/*/src/**/*.test.ts, and neither the root nor the package tsconfig covers a top-level test directory. A plain unit-core run discovered zero files under the package. That is why they looked green: my earlier runs passed those paths explicitly on the command line, which masked that the default run skipped them. The count is the proof — 550 files/4741 tests before, 553/4753 now, and the delta is exactly the three files and twelve tests that were being skipped. The runtime test also imported runReplayTestAttempt from the package specifier, which the facade does not export. It would have failed the moment it was discovered. It now imports internally, like the rest of the internal tests. Also removed replayTestAttemptFailure from the facade: zero consumers outside the package, so exporting it widened the boundary for nothing. P3 asks for a one-function facade. 553 test files and 4753 tests pass; lint and the layering guard are clean. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
|
Both fixed in P1 — you were right, and it was worse than "skipped". A plain The tests now live in P2 — removed. Gates on the new head: 553 test files / 4753 tests pass, lint clean, layering guard clean, typecheck clean. The live iOS/Android evidence is still outstanding — both attempts failed upstream of the replay-suite step, detailed in the previous comment. Staying drafted. Generated by Claude Code |
A scripted edit removed the export line but left a stray blank line; oxfmt was not re-run on that file afterward, so Lint & Format caught what pnpm lint alone does not. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
CI is fully green on
|
|
Re-reviewed exact head P1 — the newly moved package tests still are not typechecked. Root The architecture and production route otherwise look clean and CI is green. Keep the PR drafted: the full-tier iOS/Android journey still has not reached the consolidated live harness, so practical evidence remains outstanding. |
Review found the moved package tests were transpiled by vitest but never typechecked: the root typecheck script builds six packages via tsc -b and packages/replay-test was not among them, so its tsconfig was never used. That hid a real TS2459. session-test-runtime.test.ts imported ReplayTestAttemptOutcome from ../session-test-runtime.ts, which imports that type but does not re-export it. It now imports from ../session-test-types.ts, where the type is defined. Adding the package to the tsc -b list closes the gap. Verified empirically rather than assumed: planting a string-to-number error in a package test makes typecheck fail, and removing it makes it pass. This is the second finding of the same shape on this PR — first the tests were not discovered by vitest, now they were not covered by typecheck — so the gate was confirmed to reach the files rather than trusted to. 12 package tests pass, lint, format and the layering guard are clean, and typecheck is clean with the package included. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
|
Fixed in The gap: The hidden error: The gate: I verified the gate empirically rather than trusting a clean run, because a clean run is exactly what misled me here. Planting and removing it returns to zero. This is the second finding of the same shape on this PR — first the tests were not discovered by vitest, now they were not covered by typecheck. Both times my green signal did not include the files I was claiming it did. The general fix I am carrying forward: after relocating tests across a boundary, prove each gate reaches them (count moves for the runner, planted error for the typechecker) instead of reading a pass as coverage. Gates on the new head: typecheck clean with the package included, 12 package tests pass, lint clean, format clean, layering clean. Staying drafted — the full-tier live journey still has not reached Generated by Claude Code |
|
Re-reviewed exact head No new code finding, but this is not readiness-labelled yet. Exact-head device CI runs only the smoke tier and does not execute |
|
Full-tier live evidence at exact head iOS — full journey, consolidated harness reached and green
The suite step drove One caveat, documented as the environment blocker it is: the run still exits red in finalize — Android — consolidated harness green; full journey blocked by pre-existing
|
|
Re-reviewed exact head |
Draft — code complete, waiting on live-device evidence. Every item on the P3b brief has landed. The one thing between this and ready is a run of the iOS/Android journeys against the consolidated harness, which the authoring environment has no devices for.
What this does
P3a neutralized the values crossing the scheduler seam. P3b removes the remaining ambient authority, splits host work out of the scheduler, and physically extracts the package.
The seams
src/replay/script.ts(the.adengine)@agent-device/kernel/device— resolves to the same types, only the import direction changedemitRequestProgressreading a request-globalAsyncLocalStorage, 8 call sitesemitProgress, narrower thanRequestProgressSink— suite/test events only, neverCommandProgressEventisRequestCanceled(requestId), 5 call sitesisCanceled(); the scheduler never names a daemon request idregisterRequestAbort/markRequestCanceled/clearRequestCanceled/ parent-abort relaycancel()on timeout,release()when settledutils/diagnostics.tsrequest-global scopeemitDiagnosticresolveReplayFormat+ path expansiondiscoverSources; the scheduler keeps filtering policylistDeviceInventory, allowlists, simulator set paths,CommandFlagsresolveShardTargets; the scheduler keeps distribution policyDaemonRequestin,DaemonResponseoutReplayTestSuiteRequestin, taggedReplayTestSuiteOutcomeoutTwo are worth review attention:
The platform tag is what made discovery format-neutral. The filter used to ask
resolveReplayFormat(...) === 'maestro'to decide whether a missing platform disqualified a source. The manifest now carriesdeclared | caller-bound | unspecified, so the scheduler asks how the platform is determined, never which engine produced it.caller-boundis what Maestro looks like from the scheduler's side and the format cannot be recovered from it.The timing trace stayed private. The host writes video-lifecycle events into the same trace the scheduler owns. Rather than export a writer from the façade, each attempt hands the host an
appendTimingEventclosure — the format stays internal and the authority is scoped to that one attempt's trace.The extraction
packages/replay-test/src/internal/holds the scheduler, attempt runtime, discovery policy, sharding distribution, artifacts and neutral types, with one export at..Host work stayed in daemon adapters: source inspection, shard device binding, shard-flag parsing, and artifacts-dir home expansion — which is why the package resolves paths without
SessionStore.R10 retargeted to
packages/replay-test/src/; the zone is ranked alongsidemaestro; R11 confirms zero root-srcimports from the package. The dead'src/maestro/'entry inforbiddenTargetRoots(a directory that no longer exists) is replaced withpackages/maestro/.Live harness consolidation
Both live journeys invoked the public
testcommand and then re-derived the same value-contract assertions by hand — suite totals, per-script status, replay counts, non-empty JUnit. They had already drifted: iOS iterated withreadReplayCommandsinline, Android castdata.testsat the call site.The shared helper owns that boundary and takes the caller's
runSteprather than binding a context type, so it is not a platform-configured runner and cannot template a platform's journey. Scripts, retry policy (iOS 2, Android none — itself a claim worth keeping), expected commands and behavioral evidence all stay with the callers, which keep everyverify*call they had. 67 lines removed, 15 added.Tests
Coverage moved with the code rather than thinning:
.ad/Maestro files; filtering policy is pinned in the package against fake sources.default:test:suite-reporter:1-02-retry:attempt-1, the strongest available evidence that ids and reporter values are byte-identical.Residual risk
The live suites have not been run against the consolidated harness. The authoring environment has no iOS or Android targets. Typecheck, lint, oxfmt and the layering guard pass, and the change is structural, but "behavior unchanged" is inference until it runs on real devices. This is the gate on undrafting.
One pre-existing test flaw, not introduced here
session-test-runner"binds each replay script to its declared platform metadata" writes two files into a temp directory and assumes discovery returns them in creation order. Directory expansion deliberately preserves filesystem order (glob expansion sorts) to match Maestro, and that behavior is explicitly pinned by "preserves Maestro directory filesystem order", which mocksopendirSyncto prove it. So the ordering contract is correct and intentional; the runner test's assumption about enumeration order is what is unsound. It passes on CI, where small directories usually enumerate in creation order, and fails on filesystems that don't — identically on cleanmain. The fix belongs in that test, not inreplay-source-discovery.ts, and is out of scope here.Bundle size
Earlier revisions of this PR reported a decrease; that was base drift, not this change. Against current
main, P3b costs +2.8 kB raw / ~+780 B gzip, concentrated indist/src/session.js— the per-attempt cancellation object, the manifest conversion, the neutral request/outcome translation and the capability threading. For calibration, a test-only PR (#1526) moved +88 B gzip, so that report's noise floor is not small.Refs #1478
Generated by Claude Code