Skip to content

test: assert the specific error code instead of any failure (#1781 B4) - #1790

Merged
thymikee merged 1 commit into
mainfrom
test/1781-b4-assert-right-reason
Aug 18, 2026
Merged

test: assert the specific error code instead of any failure (#1781 B4)#1790
thymikee merged 1 commit into
mainfrom
test/1781-b4-assert-right-reason

Conversation

@thymikee

@thymikee thymikee commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

Umbrella #1781, item B4 "assert the right reason". The repo's typed-error rule: failures are AppError with a code from the closed KNOWN_APP_ERROR_CODES set (packages/kernel/src/errors.ts). This PR converts the 21 test assertions across the repo that instead accepted ANY failure — bare expect(...).toThrow(), bare assert.throws(fn), bare assert.rejects(p) — into assertions on the specific code (or, where the propagated error is genuinely opaque, an identity assertion with a comment explaining why).

Count breakdown (correcting an earlier revision of this body, which said 20 and disagreed with its own site list): 20 + 1 = 21.

  • 20 are syntactically bare — zero matcher argument. That is what the paren-matching re-derivation of the site list found (multi-line matchers and .not.toThrow() excluded), and it is the number the earlier body quoted.
  • 1 more is bare in effect but not in syntax: resumable-upload-range.test.ts:19's assert.throws(fn, value). A string second argument to node:assert's throws/rejects is the assertion failure message, never an error matcher — a documented Node.js gotcha — so that site accepted any error too. It is converted here as well, which is why the per-site list below totals 21 rather than 20.

Added a synchronous assertThrowsAppError(fn, {code, message?}) sibling to the existing assertRejectsAppError helper in src/__tests__/test-utils/app-error.ts, exported via the test-utils index. packages/provider-limrun and packages/provider-webdriver have no test-utils dir and cannot import from src/, so those sites use vitest's expect(...).toThrow(expect.objectContaining({ code })) or an inline assert.rejects(p, matcherFn) instead — no cross-package imports, no new re-export surface.

Sites converted

  • packages/provider-limrun/src/app-log-runtime.test.ts:153-155 — bare .toThrow() x3 → UNSUPPORTED_OPERATION
  • src/daemon/__tests__/app-log.test.ts:39 — bare .toThrow() → message match (plain Error, not AppError, from verified-file's identity check; message differs by operation)
  • src/daemon/__tests__/resumable-upload-range.test.ts:13 — bare assert.throws(fn)INVALID_ARGS. Also line 19's assert.throws(fn, value) — the string-second-argument gotcha described above, the +1 in the count.
  • packages/provider-webdriver/src/webdriver-client.test.ts:229 — bare assert.rejects(p) → asserts the raw AbortSignal.timeout() rejection's name, since the transport re-throws it unwrapped (not an AppError)
  • src/daemon/handlers/__tests__/session-device-claims.test.ts:129,151,174 — bare assert.rejects(p) x3 → identity assertions; each test's point is device-claim rollback/retention behavior around an opaque mocked upstream failure, not any particular error shape
  • src/platforms/android/__tests__/settings.test.ts:109 — bare assert.rejects(p)UNSUPPORTED_OPERATION
  • src/platforms/android/__tests__/snapshot.test.ts:1071,1342 — bare assert.rejects(p) x2 → COMMAND_FAILED + message
  • src/platforms/android/__tests__/touch-helper-session.test.ts:526 — bare assert.rejects(p)COMMAND_FAILED, wrong-protocol message
  • src/platforms/apple/core/__tests__/runner-command-retry.test.ts:472,527,550,762,881,1016 — bare assert.rejects(p) x6 → COMMAND_FAILED with the recovery-path-specific details/message
  • src/platforms/apple/core/__tests__/runner-transport.test.ts:61 — bare assert.rejects(p) → identity assertion; fetchWithTimeout does not wrap fetch() failures into an AppError

Totals by matcher: 4 .toThrow, 2 assert.throws, 15 assert.rejects = 21.

No repo-wide scanner/lint rule added (explicitly out of scope per #1781); no test loosened.

Red evidence

docs/agents/testing.md requires red evidence for a regression pin. Every matcher class introduced here was made to face its wrong failure and observed to reject it. Each mutation was applied alone and reverted before the next.

# Matcher class Site Mutation Red result
A assertRejectsAppError(p, {code}) android/__tests__/settings.test.ts:109 expected code UNSUPPORTED_OPERATIONINVALID_ARGS AssertionError: Expected values to be strictly equal: + 'UNSUPPORTED_OPERATION' - 'INVALID_ARGS' — 1 failed | 14 passed (15)
B assertThrowsAppError(fn, {code, message}) (new) daemon/__tests__/resumable-upload-range.test.ts:14 expected code INVALID_ARGSCOMMAND_FAILED + 'INVALID_ARGS' - 'COMMAND_FAILED' — 1 failed | 1 passed (2)
C vitest .toThrow(expect.objectContaining({code})) provider-limrun/src/app-log-runtime.test.ts:153 expected code UNSUPPORTED_OPERATIONINVALID_ARGS AssertionError: expected error to match asymmetric matcher — 1 failed | 9 passed (10)
D1 inline assert.rejects(p, fn) asserting AppError code+details apple/core/__tests__/runner-command-retry.test.ts:478 expected code COMMAND_FAILEDINVALID_ARGS + 'COMMAND_FAILED' - 'INVALID_ARGS' — 1 failed | 37 passed (38)
D2 inline assert.rejects(p, fn) asserting AppError code+message android/__tests__/touch-helper-session.test.ts:537 expected code COMMAND_FAILEDINVALID_ARGS + 'COMMAND_FAILED' - 'INVALID_ARGS' — 1 failed | 8 passed (9)
E .toThrow(regex) message match daemon/__tests__/app-log.test.ts:39 non-mark branch expects /must not be a symbolic link/ expected [Function] to throw error matching /must not be a symbolic link/ but got 'Final path must be a regular file: …' — 2 failed | 2 passed (4)
F1 identity exemption (error) => error === X daemon/handlers/__tests__/session-device-claims.test.ts:129 mock rejects a different instance carrying the same message AssertionError: The validation function is expected to return "true". Received false / Error: device not ready — 1 failed | 8 passed (9)
F2 identity exemption (error) => error === X apple/core/__tests__/runner-transport.test.ts:61 fetch throws a different instance, same message The validation function is expected to return "true". Received false / Error: request timed out after reaching runner — 1 failed | 5 passed (6)
G inline non-AppError name assertion provider-webdriver/src/webdriver-client.test.ts:229 expected name TimeoutErrorAbortError The validation function is expected to return "true". Received false / TimeoutError: The operation was aborted due to timeout — 1 failed | 12 passed (13)

F1 and F2 are the ones worth reading closely: the identity exemptions reject a same-message replacement error, so they pin the propagation path itself, not the wording.

Before/after A/B — the assertions are newly load-bearing

The table above shows the new assertions reject a wrong expectation. This pair shows the old ones did not, by applying one product-side perturbation to origin/main's file and to this branch's file:

Perturbation origin/main (bare) this branch
adb fingerprint failure changes from unknown command to error: device offline, so the code becomes COMMAND_FAILED instead of UNSUPPORTED_OPERATION (settings.test.ts) 🟢 15 passed (15) — bare assert.rejects accepts the wrong failure 🔴 + 'COMMAND_FAILED' - 'UNSUPPORTED_OPERATION' — 1 failed | 14 passed (15)
a completely unrelated error propagates out of the claim-rollback path (session-device-claims.test.ts) 🟢 9 passed (9) — bare assert.rejects accepts it 🔴 The validation function is expected to return "true". Received false / Error: completely unrelated failure — 1 failed | 8 passed (9)

Restored → green

All mutations reverted (git status clean); the full touched set re-run:

Test Files  10 passed (10)
     Tests  153 passed (153)

Follow-up surfaced

src/utils/app-log-files.ts:43 and src/utils/verified-file.ts:35,110 throw plain Error instead of AppError, which is why app-log.test.ts:39 had to become a message match instead of a code assertion. This is a typed-error-rule gap in product code, out of scope for this PR. Filing as its own issue, referencing #1781 B4.

Test plan

  • npx vitest run on all 10 touched test files — 153 tests passed
  • Red evidence: 9 single-site mutations, one per matcher class, each observed red and reverted (above)
  • Before/after A/B on two representative sites — green on origin/main, red here
  • pnpm run typecheck — clean
  • npx oxlint --deny-warnings on touched files — clean

CI note

Analyze (java-kotlin) (GitHub's CodeQL default-setup scan) is red on an unrelated infra failure: the SARIF-upload step hits HTTP 503: No server is currently available while POSTing results, the same transient outage that hit Bundle Size earlier (which has since passed on rerun). This PR touches zero Java/Kotlin files. It's a GitHub-managed "dynamic" default-setup scan (not an in-repo workflow), so gh run rerun can't retry it — GitHub reruns it on the next push automatically. It is also not a required check: gh api repos/callstack/agent-device/rules/branches/main shows no required_status_checks rule for main. Safe to ignore / merge past.

Re-checked on the current run (32049027824, job 95443646924): same failure mode, unchanged — ##[error]No server is currently available to service your request at the upload step, preceded by the buildless-extraction jar fetch warning. Every other check on the PR is green.

Converts the 20 test assertions across the repo that accepted ANY
failure (bare `expect(...).toThrow()`, bare `assert.throws(fn)`, bare
`assert.rejects(p)`) into assertions on the specific AppError `code`
each test is actually about, or — where the propagated error is
genuinely opaque (a mocked upstream failure whose identity, not its
shape, is the point) — identity assertions with a comment explaining
why.

Added a synchronous `assertThrowsAppError(fn, {code, message?})`
sibling to the existing `assertRejectsAppError` helper in
src/__tests__/test-utils/app-error.ts, exported via the test-utils
index, for the two src/ sites that needed it.
packages/provider-limrun and packages/provider-webdriver have no
test-utils dir and cannot import from src/, so those sites use
vitest's `expect(...).toThrow(expect.objectContaining({ code }))` or
an inline `assert.rejects(p, matcherFn)` instead.

Sites converted:
- packages/provider-limrun/src/app-log-runtime.test.ts:153-155
  (bare `.toThrow()` x3 -> `UNSUPPORTED_OPERATION`)
- src/daemon/__tests__/app-log.test.ts:39 (bare `.toThrow()` ->
  message match; plain Error, not AppError, from verified-file's
  identity check)
- src/daemon/__tests__/resumable-upload-range.test.ts:13 (bare
  `assert.throws(fn)` -> `INVALID_ARGS`); also fixed line 19's
  `assert.throws(fn, value)`, a documented Node.js gotcha where a
  string second argument is the failure message, not a matcher, so
  it was equally bare in effect
- packages/provider-webdriver/src/webdriver-client.test.ts:229 (bare
  `assert.rejects(p)` -> asserts the raw AbortSignal.timeout()
  rejection's `name`, since the transport re-throws it unwrapped)
- src/daemon/handlers/__tests__/session-device-claims.test.ts:129,
  151, 174 (bare `assert.rejects(p)` x3 -> identity assertions; each
  test's point is device-claim rollback/retention around an opaque
  mocked upstream failure)
- src/platforms/android/__tests__/settings.test.ts:109 (bare
  `assert.rejects(p)` -> `UNSUPPORTED_OPERATION`)
- src/platforms/android/__tests__/snapshot.test.ts:1071, 1342 (bare
  `assert.rejects(p)` x2 -> `COMMAND_FAILED` + message)
- src/platforms/android/__tests__/touch-helper-session.test.ts:526
  (bare `assert.rejects(p)` -> `COMMAND_FAILED`, wrong-protocol
  message)
- src/platforms/apple/core/__tests__/runner-command-retry.test.ts:472,
  527, 550, 762, 881, 1016 (bare `assert.rejects(p)` x6 ->
  `COMMAND_FAILED` with the recovery-path-specific details/message)
- src/platforms/apple/core/__tests__/runner-transport.test.ts:61
  (bare `assert.rejects(p)` -> identity assertion; fetchWithTimeout
  does not wrap fetch() failures into an AppError)

No repo-wide scanner/lint rule added (explicitly out of scope per
#1781); no test loosened.
@github-actions

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.26 MB 2.26 MB -1.1 kB
JS gzip 744.4 kB 744.0 kB -490 B
npm tarball 863.7 kB 862.7 kB -906 B
npm unpacked 3.01 MB 3.01 MB -2.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.1 ms 28.0 ms -0.1 ms
CLI --help 67.4 ms 66.4 ms -1.0 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/prepare-kind.js -891 B -306 B
dist/src/selector-runtime.js +245 B +72 B
dist/src/interaction2.js +15 B 0 B

@thymikee

Copy link
Copy Markdown
Member Author

Request changes before merge:

  • The PR body says 20 matcherless assertions, but its own per-site list totals 21 (4 toThrow, 2 assert.throws, 15 assert.rejects). Please correct the count and scope wording.
  • B4 is specifically making failure-reason assertions load-bearing. docs/agents/testing.md requires red evidence for a regression pin, but the PR/discussion only show green runs. Deliberately mutate a representative expected code and an identity exemption (or otherwise make each matcher class reject its wrong failure), run the affected tests, and quote the red results in the PR; then restore and report green.

The changed assertions themselves appear honest on review. CI is green for the relevant test gates; the only failure is CodeQL Java/Kotlin failing while GitHub returned 503 during SARIF upload, which is infrastructure-unrelated to this TypeScript-only test change.

@thymikee

Copy link
Copy Markdown
Member Author

Both points addressed in the PR body — no code changes were needed, so no new commit.

1. Count. You're right, and the fix is arithmetic, not scope. It's 21, made of 20 + 1:

  • 20 syntactically bare (zero matcher argument) — that's what the paren-matching re-derivation found, and it's the number the old body quoted.
  • 1 bare in effect only: resumable-upload-range.test.ts:19's assert.throws(fn, value). The string second argument is node:assert's failure message, not a matcher, so it accepted any error too. Converting it is what makes the site list total 21.

The old body reported the scan's 20 as if it were the conversion count and buried the +1 in a per-site bullet. Body now states 21 up front with the breakdown, and carries your 4/2/15 tally explicitly.

2. Red evidence. Done properly — I made each matcher class reject its wrong failure rather than just one code and one identity, since the classes fail differently. Nine single-site mutations, each applied alone and reverted before the next; full table with quoted output in the body. Summary:

Class Mutation Red
assertRejectsAppError code UNSUPPORTED_OPERATIONINVALID_ARGS + 'UNSUPPORTED_OPERATION' - 'INVALID_ARGS'
assertThrowsAppError (new) code INVALID_ARGSCOMMAND_FAILED + 'INVALID_ARGS' - 'COMMAND_FAILED'
vitest objectContaining({code}) code UNSUPPORTED_OPERATIONINVALID_ARGS expected error to match asymmetric matcher
inline AppError matcher (x2 sites) code COMMAND_FAILEDINVALID_ARGS + 'COMMAND_FAILED' - 'INVALID_ARGS'
.toThrow(regex) message wrong branch regex ...but got 'Final path must be a regular file: …'
identity exemption (x2 sites) a different instance with the same message propagates The validation function is expected to return "true". Received false
non-AppError name assertion TimeoutErrorAbortError same, on the real TimeoutError

The identity mutation is deliberately the mean one: a same-message replacement error. That pins the propagation path itself, not the wording — which is the only thing an identity exemption can honestly claim to pin.

I also added the half your request implies but doesn't ask for: proof the old assertions were not load-bearing. One product-side perturbation, applied to origin/main's file and to this branch's:

Perturbation origin/main this branch
fingerprint failure becomes error: device offline, so the code is COMMAND_FAILED not UNSUPPORTED_OPERATION 🟢 15 passed (15) 🔴 1 failed | 14 passed
an unrelated error propagates out of the claim-rollback path 🟢 9 passed (9) 🔴 1 failed | 8 passed

Green on main, red here — the pins are new, not decorative.

All mutations reverted, git status clean, full touched set re-run: 10 files / 153 tests passed.

CI: re-checked on the current run — Analyze (java-kotlin) fails the same way (##[error]No server is currently available to service your request at SARIF upload). Unchanged, infra-side, not rerunnable via gh run rerun (GitHub-managed default setup), not a required check. Everything else is green.

@thymikee
thymikee merged commit 681cad2 into main Aug 18, 2026
30 of 32 checks passed
@thymikee
thymikee deleted the test/1781-b4-assert-right-reason branch August 18, 2026 08:13
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-18 08:13 UTC

thymikee added a commit that referenced this pull request Aug 18, 2026
…et probe

main's #1790 tightened this test to expect the raw TimeoutError DOMException,
which this PR intentionally normalizes into AppError{reason:
webdriver_request_timeout}. On the merge ref the two met and Coverage went red.
The regression now asserts the structured contract and that the second request's
budget is the shared remainder (~118ms of 200 after an 80ms first call).
thymikee added a commit that referenced this pull request Aug 18, 2026
…eaking billed sessions (#1782)

* fix(webdriver): give cloud session creation its own budget and stop leaking billed sessions

Cloud lease allocation ran under the generic 30s/1-retry request policy, so
BrowserStack iOS real-device session creation (45-90s) aborted client-side at
~60s on most runs. Each timed-out POST /session still completed server-side and,
being non-idempotent, was retried — leaving two billed provider sessions per
failed open with no id to release them.

- POST /session is its own phase: a 180s create budget (default), zero retries,
  and no request-bound abort, so the daemon always learns the session id.
- lease_allocate carries a 300s allocation budget surfaced to providers as
  LeaseLifecycleContext.deadline, and a matching 330s client envelope that
  preserves the daemon on timeout (a reset would SIGKILL mid-create and orphan
  every billed session the daemon held).
- The request's cancellation signal is ownership evidence: a session that
  completes after the requester left is released, not registered; a create that
  the transport gives up on surfaces typed evidence (provider + lease) so an
  operator can find and stop the maybe-orphaned session.

Closes #1774

* refactor: one canceled-request error, and tighten the #1774 shapes

Review pass over the session-create fix:

- The canceled-request error had nine hand-rolled copies (src/request/cancel,
  maestro shared, exec, retry, install-source x2, and the new provider one).
  It now has one definition in @agent-device/kernel/errors:
  createRequestCanceledError(details?, cause?) + isRequestCanceledError +
  REQUEST_CANCELED_REASON. Callers add evidence or a sharper hint; the reason
  itself is not overridable, so nothing can build one the predicate misses.
- lease_allocate's timeout bundle moves beside INSTALL_TIMEOUT_POLICY in the
  registry (same {...DEFAULT, envelopeMs, onTimeout} shape); the request timeout
  constant stays exported from timeout-policy like its siblings.
- Transport: fetch helper returns Response's own ok/status; the timeout reason
  const is private behind isWebDriverRequestTimeout.
- Client: one-use options type inlined; the two deadline helpers share one floor.
- Session-manager tests: shared makeRuntime/jsonResponse/afterEach restore.

Net -29 lines with the feature in.

* chore: keep the canceled-request reason private to the kernel

* fix: typed cancellation everywhere + own the AWS remote-access ARN through startup

Second-order follow-ups the #1774 refactor made cheap:

- markRequestCanceled aborts the request signal WITH the kernel's typed
  canceled error as its reason. Every signal.throwIfAborted(), aborted fetch,
  and 'throw signal.reason' in the daemon (20+ sites) now surfaces a canceled
  request as such instead of a bare DOMException that normalized to UNKNOWN —
  and no site has to know the factory exists.
- AWS Device Farm prepareSession owns the remote-access ARN from the moment
  create-remote-access-session answers: a startup timeout, the allocation
  deadline, or a canceled request now stops it before the failure surfaces
  (previously a timed-out startup left a RUNNING billed session behind — the
  same leak class as the WebDriver session, one phase earlier). The startup
  wait is capped by LeaseLifecycleContext.deadline and wakes on cancellation.
- BrowserStack's pre-session local app upload honors the request signal (an
  upload is not billed, so plain abort is right there).
- lease_heartbeat/lease_release share lease_allocate's preserve-daemon policy:
  the rationale — the daemon owns billed sessions; a reset orphans them all —
  applies verbatim.

Each AWS ownership test proven red without the guard (3/3).

* refactor: dedupe billed-resource cleanup and lease-signal wiring

Shrink pass — same behavior, less duplication:

- releaseOnFailure(primaryError, release) in webdriver-utils replaces the two
  identical 'best-effort stop the billed resource, attach cleanupError to the
  primary AppError' helpers (WebDriver session + AWS remote-access ARN); shared
  errorMessage too.
- The lease handler pulls the request signal from getRequestSignal(requestId)
  like every sibling handler, instead of threading a requestSignal arg through
  LeaseHandlerArgs and the request-handler chain. Drops the field, the wiring,
  and five mechanical test edits; the handler test now proves the request-bound
  signal (abort it, watch the provider's signal flip) rather than arg identity.
- Inlined the one-use requestHeaders back into fetchWebDriver.

Handler-signal test proven red without the wiring.

* fix(lease): the daemon releases a lease allocated for a gone requester; honest release evidence

Review follow-up. The provider was doing the daemon's job: it treated the request
signal as 'ownership evidence, not an interrupt' and needed three paragraphs to
say so. The daemon owns the request, so it now decides — generically, for every
provider — what happens to a lease that finished allocating after its requester
left: release it (provider + registry) and answer with the canceled error.

- lease.ts: after allocate returns, isRequestCanceled(requestId) →
  releaseAllocationForGoneRequester(). Release evidence is claimed ONLY on a
  clean release (no warnings, no throw); a WEBDRIVER_SESSION_DELETE_FAILED
  release is reported released:false with providerSessionId + a stop-by-hand
  hint (thymikee's finding: the previous evidence was success-shaped even when
  DELETE failed).
- WebDriverSessionManager: the createOwnedSession/releaseCanceledSession trio is
  gone; allocate is plain 'create with a budget; on failure clean up' again.
- LeaseLifecycleContext.signal is just cancellation, like everywhere else; the
  ownership-semantics comments on the contract, client, registry, AWS prepare and
  utils shrink to what the code no longer says itself.
- Tests: the two provider-level cancellation tests move to the daemon handler
  (where the logic now lives), plus the failing-DELETE regression; both proven
  red without the post-allocate check.

* fix(aws): the allocation deadline bounds remote-access startup, not the 120s default

Live iOS real-device run: startup needed ~128s and hit the standalone 120s
default while the daemon's 300s allocation budget still had room — the new
ownership guard correctly stopped the ARN, but the open failed for no reason.
When the daemon supplies a deadline it is the bound; the default only applies
standalone. Rerun: open in 112s, snapshot, clean close, session STOPPING.

* test(aws): pin that the allocation deadline outlives the 120s startup default; drop empty import

Review follow-ups on 7f9d148: a virtual-clock test (Date.now advanced 10s per
poll, RUNNING at 150s, deadline 300s) that fails on the old min(default,
deadline) logic and passes now; and the empty 'import {} from kernel/errors'
left in maestro/shared.ts is removed.

* refactor: finish the dedupe — one release path, kernel errorMessage, AWS on releaseOnFailure

Code-quality review at 7f9d148:
1. aws-device-farm.ts still carried its own copy of releaseOnFailure (the dedupe
   commit's script aborted before reaching it and I mis-verified). Now uses the
   shared helper; private copy deleted.
2. Empty 'import {} from kernel/errors' in maestro/shared.ts removed (2738700).
3. errorMessage() lives in @agent-device/kernel/errors; the two copies this PR
   had added (lease.ts, webdriver-utils.ts) import it. Sweeping the pre-existing
   copies is a follow-up.
4. lease.ts has ONE release path: releaseLease(registry, provider, lease,
   request, ctx) → { released (registry), provider } used by both the
   lease_release case (wire shape unchanged) and the gone-requester branch, which
   folds a throwing provider release into releaseError. 'released' now means the
   same thing in both; the provider verdict is a separate 'providerReleased'
   (warnings-free, no throw) that drives the stop-by-hand hint. -~35 lines.
5. sessionCreateTimeoutMs is Omit-ed at the WebDriverTransportOptions boundary
   instead of Pick-ed back out internally.

* fix(lease): 'released' on a canceled allocation means the billed session is confirmed gone

Re-review at 3665ea0: unifying the release path had made the cancellation
error report released:true from the daemon's registry record while the provider
DELETE had failed — success-shaped again, with the operator verdict demoted to
a second key. Fixed at the source of the ambiguity:

- LeaseReleaseOutcome names its bookkeeping field registryReleased.
- On the canceled error, 'released' is true only when registryReleased AND the
  provider released without warnings AND without throwing; the registry record
  is exposed as 'registryReleased'. The stop-by-hand hint keys on 'released'.
- lease_release keeps its existing wire field ('released' = registry; provider
  cleanup rides in 'provider'), unchanged.
- Regressions: failed DELETE and throwing release both pin released:false /
  registryReleased:true (+ providerSessionId, warnings|releaseError, hint);
  both proven red on registry-only semantics.

* ci: retrigger default-setup CodeQL

Run 32051017472 is wedged on GitHub's side: status=completed with
Analyze (python) still queued and Analyze (java-kotlin) failed only at SARIF
upload (503, 'No server is currently available'). It can be neither cancelled
nor rerun, and default-setup CodeQL has no dispatchable workflow, so a new push
is the only way to get a fresh run. No source change.

* test(webdriver): assert the typed timeout contract on the shared-budget probe

main's #1790 tightened this test to expect the raw TimeoutError DOMException,
which this PR intentionally normalizes into AppError{reason:
webdriver_request_timeout}. On the merge ref the two met and Coverage went red.
The regression now asserts the structured contract and that the second request's
budget is the shared remainder (~118ms of 200 after an 80ms first call).
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.

1 participant