mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
main
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d80fb35ec3 |
fix(browserstack): carry the full provider-allocation config over the lease_allocate envelope (#2494) (#2495)
* fix(browserstack): forward provider session metadata over the lease envelope `--provider-project`, `--provider-build`, and `--provider-session-name` are stored in the connection profile and reach the daemon on the line transport (which forwards the whole request), but the compact JSON-RPC lease envelope carried only `providerApp`. Over HTTP/remote daemons the daemon's lease-lifecycle provider therefore saw no session-naming metadata and created BrowserStack sessions as "Untitled Project" / "Untitled Build" with an empty name. Read the four provider session-metadata flags through one shared projection (`readLeaseAllocateProviderMetadata`) used by both the client's `buildHttpRpcPayload` and the daemon's `toLeaseDaemonRequest`, so the transports agree and the producer and consumer cannot drop a sibling again. Closes #2494 * chore(gates): pin lease_allocate wire digests for the forwarded provider metadata (#2494) * test(daemon-http): move the lease provider-metadata check into its own file The provider-scenario daemon-http-server test is over the 1000-line tripwire and may not grow; the lease_allocate metadata assertion now lives in daemon-http-lease-allocate.test.ts, mirroring the lease projection in http-server.ts. * fix(browserstack): carry the full provider-allocation config over the lease envelope The lease envelope named only the session-label fields, so a fresh remote allocation still failed in prepareSession before the names could take effect: device selection (platform, device), providerOsVersion, and the configured device-feature/AWS knobs were dropped on the HTTP transport, while the line transport forwards the whole request for free. readLeaseAllocateProviderMetadata becomes readLeaseAllocateProviderFlags and projects the full set the lease-lifecycle provider reads, pinned exhaustive against CloudProviderProfileFields so a new field cannot silently miss it. The producer and consumer already share this reader, so both transports agree. Covered end to end in cloud-webdriver-lease-http.test.ts: the exact client envelope is driven through a real daemon HTTP server into the real BrowserStack prepareSession, asserting the capabilities that reach the hub. * chore(gates): re-pin lease_allocate wire digests for the broadened projection (#2494) |
||
|
|
c61b6ed407 |
feat(remote): add HarmonyOS proxy lease backend (#2266)
* feat(remote): add HarmonyOS proxy lease backend Signed-off-by: Ark <artin@cat.ms> * feat(remote): add HarmonyOS proxy lease backend Signed-off-by: Ark <artin@cat.ms> * feat(remote): complete HarmonyOS lease backend wiring Signed-off-by: Ark <artin@cat.ms> * feat(remote): complete HarmonyOS lease backend wiring Signed-off-by: Ark <artin@cat.ms> * fix(remote): accept Harmony runtime hints Signed-off-by: Ark <artin@cat.ms> * fix(remote): complete Harmony runtime lease plumbing Signed-off-by: Ark <artin@cat.ms> * test(wire): acknowledge Harmony lease additions Signed-off-by: Ark <artin@cat.ms> * test(remote): close HarmonyOS lease review gaps Signed-off-by: Ark <artin@cat.ms> * fix(runtime): update HarmonyOS support error text Signed-off-by: Ark <artin@cat.ms> * fix(remote): preserve HarmonyOS runtime and proxy device identity Signed-off-by: Ark <raft-mobile-ark@mail.build> * fix(script): avoid eager contracts import and relocate Harmony test Signed-off-by: Ark <raft-mobile-ark@mail.build> --------- Signed-off-by: Ark <artin@cat.ms> Signed-off-by: Ark <raft-mobile-ark@mail.build> Co-authored-by: Ark <raft-mobile-ark@mail.build> |
||
|
|
0627190524 |
refactor(move): move lease scope vocabulary into @agent-device/contracts (#2380)
* refactor(contracts): move the lease scope vocabulary into @agent-device/contracts * chore(gates): pin the contracts lease-scope subpath in the exports snapshot * fix(tests): re-point remote-proxy-parity lease-scope import to contracts The test landed on main after this branch cut and still imported the pre-move path src/core/lease-scope.ts; use the @agent-device/contracts/lease-scope subpath like the other consumers. |
||
|
|
367e795ee7 |
fix(remote): let a plain-session client read its own failure record (#2382)
`GET /sessions/<session>/requests/<requestId>/diagnostics` applied the `<tenant>:` prefix rule to every caller carrying a tenant, but `scopeRequestSession` only writes that prefix under tenant isolation — which the daemon forces exactly when the auth hook ATTESTS the tenant. A client whose tenant is only declared (the `x-agent-device-tenant` header on a daemon with no auth hook) therefore ran in a plain session such as `default` or `cwd:<hash>:default` and was then refused 401 reading the record its own failed command wrote, directly and through `agent-device proxy`. The addressability rule now lives beside the naming rule in `session-tenant-scope.ts`, which exists so the two cannot disagree. `isTenantAddressableSessionName` takes the caller's session namespace and applies the prefix rule only where the namespace is actually partitioned; `resolveTrustedTenant` now reports whether the tenant was attested, and `authorizeAuxiliaryHttpRequest` hands that namespace to the route. The attested case is unchanged: an attested tenant is still refused any session outside its own prefix, with the same typed UNAUTHORIZED error. |
||
|
|
7bcbf1350b |
feat(remote): proxy parity for Simulator observation (#2198 slice B) (#2351)
* feat(remote): give a proxied client the daemon's own failure envelope, cancellation, and version check #2198 slice B. Direct-daemon and proxy execution over the same deterministic Simulator fixture now publish the same responses, and the three places where they did not are closed: - A client that disconnects mid-request behind the proxy now cancels the daemon request. The proxy's upstream fetch is bound to its client's connection, so the daemon's own disconnect cancellation (`markRequestCanceled`) fires exactly as it does for a direct client. - The proxy forwards `GET /sessions/<session>/requests/<id>/diagnostics` (#1801), so a remote client behind it localizes a failure's diagnostics record instead of reporting `logPathUnavailable: HTTP 404`. GET only; the route still enumerates nothing. - The client's ADR 0006 health check reads the `upstream` link a proxy's /health already nests: a proxy whose daemon speaks another RPC protocol fails at health, before the command RPC. The provider scenario harness exposes its request boundary so a scenario daemon can sit behind a real HTTP server and proxy; the new parity suite runs one script direct and proxied and compares the published responses with transport identity removed, and proves two proxied clients contending for one device fail at claim admission before any lifecycle call. * test(remote): a proxied lease that expires tears its session down and a reacquired lease starts clean #2198 acceptance: lease expiry, session cleanup, and device reacquisition do not reuse prior capture or comparison state. The parity world takes a clock-driven LeaseRegistry; a leased session behind the proxy captures a diff baseline, its lease lapses past the proxy TTL, the next request is refused as UNAUTHORIZED/LEASE_NOT_FOUND with the session torn down, and a freshly allocated lease reopens and reports baselineInitialized on its first diff. * test(remote): a proxied lease heartbeat renews the lease and keeps the session's comparison state #2198 acceptance: lease heartbeat through the proxy. A leased session behind the proxy captures a diff baseline; an explicit lease_heartbeat RPC just before the lease's reported expiry moves the expiry forward; a request past the old expiry but inside the renewed window still finds the session and reports the baseline. Red without the renewing heartbeat (LEASE_NOT_FOUND). * test(remote): follow the daemon client and session artifact path moves |
||
|
|
51ed6217cc |
refactor(daemon): relocate the daemon client out of src/daemon (#2360)
* refactor(daemon): extract the repair-tombstone reader below store and client `findUnrecoveredRepairCommitFailure` reads session artifacts off disk and is reached from the daemon client, which had to import `session-store.ts` — the daemon's largest server module — for it. Move the tombstone shape, its file reader and the unrecovered-commit scan into `session-repair-tombstone.ts`, a leaf below both, and give the tombstone file name a single owner. No behavior change; both consumers keep their existing tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc * refactor(daemon): relocate the daemon client out of src/daemon `src/daemon/client/` is the daemon's client, not the daemon: no daemon file imports it, and its consumers are the CLI, the Node client, the proxy command and the injected dispatch type. Move it to `src/daemon-client/` as renames so `src/daemon` is server code plus the shared kernel the client still needs — `config.ts`, `daemon-process.ts`, `request-progress-protocol.ts`, `daemon-request.ts` and the extracted `session-repair-tombstone.ts`. Zone name and rank are unchanged (`daemon-client`, 5); the zone now falls out of the folder instead of a `src/daemon/client/` prefix. Tests move unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc * refactor(daemon): move the session artifact path helpers out of session-store `src/cli.ts` and `src/remote/remote-request-diagnostics.ts` reach into `session-store.ts` for one pure path function, `resolveRemoteRequestDiagnosticsPath`, which made every CLI process eagerly evaluate the daemon's session store and its whole subtree — the script writer, the event log, the action recorder and the replay transaction vocabulary. The four artifact path helpers name files; they hold no store state. Move them to `src/daemon/session-artifact-paths.ts`, a leaf over `session-paths.ts`, and point all ten consumers at it. `src/cli.ts`'s eager closure drops from 379 modules to 365 and no longer contains `session-store.ts`; the store itself is 464 -> 341 lines. AGENTS.md's declaration-site pointer follows. No behavior change: the helpers are unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc * chore(gates): re-key the daemon-client gate paths onto src/daemon-client Path-keyed enforcement follows the relocated files: the fallow health baseline entries, the oxlint per-file override, the wire-compat surface/ledger/mutation paths, and the layering zone derivation (the `src/daemon/client/` prefix is dead now that the folder itself names the zone). R10's external daemon request/session-state importer list gains the five client modules. The edges are unchanged by this PR — the client has always built `DaemonRequest` and read `DaemonResponse`; it sat inside `src/daemon/` and so fell under the prefix skip. Naming the files keeps the dependency enumerated and shrink-only, so a new `src/daemon-client/` module reaching `session-state` still fails. Its size assertion now reads the recorded list instead of a literal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
dcd8b65d4c |
refactor(daemon): split src/daemon/types.ts into request types and session state (#2346)
* refactor(daemon): split daemon/types.ts into request and session-state modules `src/daemon/types.ts` served two audiences from one file: the dispatch request shape and the daemon's live session record. It also sat in the only daemon type cycle — it imported `RefFrame` from `ref-frame.ts`, which imported `SessionState` back — so neither file could be read in isolation. Three modules replace it, each importing only downward: - `daemon-request-wire.ts` declares `DaemonWireRequest`: a dispatched request with no `internal` key and no property path to `SessionState` or `DeviceLease`, so a consumer can read a request's command, flags and public metadata without depending on the session record. - `daemon-request.ts` adds the daemon-only half (`DaemonRequestInternal`, which stays unexported) plus the response vocabulary. - `session-state.ts` owns `SessionState` and the shapes only it holds. The cycle is cut by `ref-frame-slot.ts`, declared below both `ref-frame.ts` and `session-state.ts`: it owns the frame VALUE (the class stays unexported, so the type remains nominal and unconstructible from outside), while `ref-frame.ts` keeps every lifetime transition and every `session.refFrame` write. No behavior change: every importer moves to the module owning the symbol it uses, with no re-export shim at the old path. `client-normalizers.ts` takes `SessionRuntimeHints` from `@agent-device/kernel/contracts`, which declares it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y * test(daemon): assert the wire request shape cannot reach session state A type-level walk over `DaemonWireRequest` fails `tsc` if the shape regains an `internal` key or grows a property path back to `SessionState` or `DeviceLease`. Positive controls over `DaemonRequest` prove the walk finds both when they are there, so a walk that never matches anything cannot pass by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y * test(daemon): keep the three over-budget test files at their base length Splitting `daemon/types.ts` turns one combined import into two in every file that used both halves. Three of those test files are already over the 1,000-line tripwire, where the size ratchet allows no growth, so each sheds one line that was carrying nothing: - `snapshot-handler.test.ts` and `find.test.ts` each drop a `toHaveLength` assertion an adjacent `toEqual` on an explicit array literal already makes. - `session-replay-repair-transaction.test.ts` names the filtered close actions instead of wrapping the expression across three lines inside `expect`. No assertion is weakened and no test content is removed. Splitting these files along the modules they mirror is the standing remedy, but none of those modules split here, so it stays out of this change and is tracked in #2353. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y * chore(gates): point the daemon modularity and wire-compat gates at the split modules R7 now locates the `SessionState` declaration by the declaration itself rather than by a recorded path: `sessionStateWritePressure` measures the merge-base tree too, and that tree still declares it in `daemon/types.ts` — a path constant would measure it as zero pressure and bank the headroom. R10's external-importer ratchet covers all three modules that replaced `daemon/types.ts`, so moving a symbol between them cannot reopen the boundary to a new outside zone. The recorded membership is unchanged: `client-normalizers.ts` and `remote/daemon-artifacts.ts` both import `daemon-request.ts` only. The daemon RPC closure gate waives `DaemonRequest`, `DaemonResponse` and `DaemonArtifact` by path, so those three keys follow the declarations to `daemon-request.ts`. `DaemonRequest`'s rationale now says what it is — the server-side narrowing of the kernel declaration that fixes the wire shape — rather than calling it a re-export alias. The `live-state-shape` and session-resource declaration sites move with `SessionState`; the depgraph lookalike fixture takes a new plausible path now that `daemon/session-state.ts` is the real root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6e22e266d7 |
refactor(contracts): own the daemon HTTP wire contract so clients stop importing src/daemon (#2322)
* refactor(contracts): own the daemon HTTP wire contract so clients stop importing src/daemon Move the pure wire vocabulary (base path, header names, URL/auth/tenant builders, /health payload) from src/daemon into @agent-device/contracts as the daemon-http subpath, so src/remote and src/cli stop importing daemon server internals. buildDaemonHealthPayload takes the version its caller advertises (R18 keeps host mechanics out of contracts); both callers pass readVersion(). Wire-compat surface, mutation, and ledger references follow the package path. * chore(gates): pin the moved daemon HTTP wire surface and teach the released-baseline check file moves Exports map + snapshot gain the daemon-http subpath. The wire ledger re-keys the eight moved declarations (buildDaemonHealthPayload moves with its new caller-supplied version parameter, acked additive). The released-baseline comparison now classifies a baseline declaration that re-appears unchanged at exactly one new path as a move instead of a removal: a file move is not wire surface a released peer stopped sending. A move that changes shape is a change acked at the destination path, and a name still owned by the baseline stays a removal. |
||
|
|
7bf8d8c4a3 |
fix(remote): materialize test suite artifacts against a remote daemon (#2272)
* fix(remote): materialize test suite artifacts against a remote daemon (#2246) `agent-device test` crashed with ENOENT against a remote daemon because the scheduler resolved `--artifacts-dir` against the caller's `cwd`, sent over the wire, on the daemon's own filesystem. Mirrors #1802's read-side fix for the same command: the CLI now redirects `--artifacts-dir` to a temp directory the daemon owns before the suite runs, and the daemon rewrites every artifact path in its response back to the caller-local root and registers the suite directory as one downloadable artifact through the existing screenshot/record transport, extended here to also support directories via the codebase's existing safe archive extractor (the archive comes from a remote daemon, a different trust domain, so a raw `tar` invocation was not enough). * fix(remote): publish test artifacts atomically * perf(cli): keep artifact downloads lazy |
||
|
|
48eb1b06e3 |
feat: open Limrun uploaded apps (#2110)
* feat: open Limrun uploaded apps * refactor: generalize deferred provider app selection * refactor: fold limrun preinstall into allocation * refactor: keep app selection provider independent * refactor: enforce provider-independent commands * refactor: restrict provider imports from commands * refactor: keep provider guard lightweight * fix: enforce provider-independent commands * fix: enforce provider catalog lease admission * refactor: deepen provider app catalog admission * refactor: simplify provider capability projection * fix: restrict Limrun uploaded apps on public daemon * refactor: separate provider app catalog lookup * fix: simplify uploaded app provider boundaries |
||
|
|
315a0ac679 |
fix(daemon): fail closed when the auth hook is silent about tenant (#2104)
An auth hook that ran but returned no tenantId opted the deployment into tenant attestation; falling back to the client's own claim (RPC body meta.tenantId, aux-route x-agent-device-tenant header) let a holder of one valid shared token impersonate any tenant on /rpc and on the diagnostics/ upload/download routes. resolveTrustedTenant() in the new src/daemon/server/tenant-trust.ts is now the single seam both surfaces go through and the only place that computes the resulting identity: hook attests -> use it; no hook configured -> keep today's client-declared behavior (loopback/dev unchanged); hook configured but silent with a client-declared tenant -> refuse (401) instead of trusting the claim, and no raw client-declared metadata survives into the dispatched request in that case either. Fixes #2095 |
||
|
|
af6f12e391 |
chore: adopt shared oxlint config (#2115)
* chore: adopt shared oxlint config * fix: preserve project lint boundaries * fix: remove redundant oxlint config |
||
|
|
139e3aa7f4 |
fix: surface remote install network causes (#1863)
* fix: surface remote install network causes * fix: sanitize remote error causes * fix: bound sanitized error causes after redaction |
||
|
|
d0d5c8594c | fix: serve remote daemon request diagnostics to the caller (#1801) (#1814) | ||
|
|
05a1d76f2e |
test: add daemon RPC wire-surface compatibility gate (#1717)
* test: gate daemon RPC wire compatibility against the last released tag (#1432) ADR 0006 fixes exactly when DAEMON_RPC_PROTOCOL_VERSION must be bumped, and nothing checked that it was. The runtime guard (readRemoteDaemonHealth) refuses a mismatched peer, but only fires when someone remembered the bump — a wire change that skipped it left both sides advertising protocol 2 while parsing different payloads, which is the failure ADR 0006 exists to prevent. Local daemons cannot skew (isReusableDaemonInfo takes over on any package version mismatch). Cross-machine is skewed by design — proxy, cloud/limrun, a remote macOS host — and ADR 0006 explicitly rules package version out as the compatibility gate there, so the one boundary where skew is intended was the one boundary with no gate. test/wire-compat/surface.ts declares the wire surface grouped by the ADR bullet each group serves, quoting it, with an `uncovered` note where a bullet is only partly digestible (the /health and /rpc literals inside http-server.ts stay reviewer-owned: a moved route 404s at connect time rather than misparsing). ledger.json records what each declaration hashes to, at which protocol version. Two gates, split for the same reason the replay-compat corpus splits: - unit-core holds the ledger to its source and prints the digest to paste; - Released-Surface Compatibility reads the ledger at the last RELEASED tag and requires the drift since then to carry a bump or a compatibleChanges ack. From one commit a bumped ledger and an unbumped one are both just an edited file, so only a released baseline can tell them apart. Acks are keyed by the digest they cover, so one "added an optional field" cannot launder later changes. Digests ignore comments and formatting; the manifest's closure is derived from the AST, so a field typed by an unlisted sibling fails rather than sitting outside the gate. CI cost: one added job (checkout + toolchain + two node scripts, ~1 min), mirroring the existing full-history replay-compat job. * test: close wire-surface overclaim and make the closure fail closed (#1432) Addresses both review P1s on #1717. P1 — the manifest materially overclaimed ADR 0006 coverage. It quoted all four bullets while digesting only the payload TYPES, so the producer and consumer seams could break a skewed peer without moving a listed digest. Now listed on both sides of every boundary: JSON-RPC method sets and the projections that turn each method's params into a DaemonRequest, createRpcError/sendJson/ writeRpcResponseEnvelope, resolveToken and the auth-hook types, upload preflight/finalize/308 handlers and the resumable ticket shape, artifact route and download/inventory framing, REST error mapping, and the client's own payload builder, lease-method mapping, response parser and error projection. 57 -> 117 declarations. What stays out is now named rather than implied: createDaemonHttpServer's dispatch wiring and the /health and /rpc literals inside it. Everything it dispatches WITH is digested individually, and a moved route 404s at connect time rather than misparsing — the loud failure, not the silent one. P1 — imported and re-exported payload shapes escaped the closure. declarationHomes() scanned only the manifest's own files and the walk continued silently when a name could not be placed, so a listed type could gain foo?: ImportedShape from a new module and stay green. Resolution is now explicit and fails closed: relative imports, workspace specifiers (through the owning package's own exports map, so a re-pointed export cannot drop a type), and facade re-export chains. Every referenced name must land on a listed declaration, a waiver with a written reason, a declared external module, or the TS/Node global set. Fixed two extractor blind spots the walk exposed: a declaration's own generic parameters and `as const` were being reported as references. Planted-red proofs (wire-mutations.test.ts): 13 cases independently mutate method naming, response serialization, response parsing, auth projection, upload ticket shape, 308 framing, artifact framing, REST error mapping, and progress framing, each asserting the digest moves; 3 probes prove the closure really reaches across a package boundary, a facade re-export, and a plain relative import. Mutations apply inside the declaration's own span — a whole-file replace silently hit a sibling sharing the substring, which is how the first draft of one case passed vacuously. The largest waiver pair (InternalRequestOptions, CommandFlags) rests on ADR 0006's own additive rule: they reach the peer inside DaemonRequest's untyped flags/input bags, and the decision says a new flag needs no bump. Digesting them would fire the gate on every new CLI flag and train reviewers to rubber-stamp acks. * test: list the consumer half of the auxiliary HTTP boundaries (#1432) Addresses the remaining review P1 on #1717. The manifest claimed both sides of response/upload/artifact framing while listing nothing from upload-client.ts, daemon-artifacts.ts, or the health consumer in daemon-client-transport.ts, so those parsers could narrow without moving a listed digest or protocol 2. Now listed (117 -> 141 declarations): - /health consumer: RemoteDaemonHealth, readHealthPayload, readDaemonHttpHealth, readRemoteDaemonHealth. This is the sharpest of the three — narrowing the reader or the comparison disables the very refusal ADR 0006 exists to guarantee, and nothing else in the repo would notice. - /upload consumer: UploadResponse, UploadPreflightResponse, UploadPreflightResult, parseUploadPreflightResult, requestUploadPreflight, uploadDirectArtifact, tryDirectUploadWithResume, shouldRetryDirectUpload, finalizeDirectUpload, uploadLegacyArtifact, ARTIFACT_HASH_ALGORITHM, isStringRecord, and PreparedUploadArtifact — whose sha256/sizeBytes/fileName/artifactType/ contentType fields ARE the preflight body the daemon parses. - /artifacts/* consumer: DaemonArtifactEndpoint, buildDaemonArtifactUrl, isRemoteDaemon, DownloadRemoteArtifactParams, downloadRemoteArtifact, materializeRemoteArtifacts, resolveMaterializedArtifactPath. Running the closure fail-closed over the new files surfaced three more stops, each decided rather than skipped: PreparedUploadArtifact listed (it is payload), UploadProgressSink waived (client-local rendering, never leaves the process), and src/daemon/types.ts#DaemonArtifact waived as a re-export alias of the listed kernel type, matching its DaemonRequest/DaemonResponse siblings. 10 more planted-red mutations cover the new seams: health version-read and mismatch-refusal defeated, RemoteDaemonHealth field dropped, preflight parser narrowed, preflight/legacy response shapes narrowed, finalize body key renamed, ticket field renamed, artifact tenant header dropped, artifact URL moved. A fourth closure probe proves the upload-consumer files are genuinely reached by the walk rather than merely listed. 22 -> 33 tests. The README now states the coverage as a producer/consumer table per boundary, so the claim is checkable at a glance instead of asserted in prose. * test: list the client half of the resumable 308 contract (#1432) Addresses the third review P1 on #1717. Listing the daemon's handleResumableUpload proved it still PRODUCES 308; nothing proved the client still CONSUMES the released one. src/remote/upload-stream.ts owns that half and was entirely outside the manifest, so a newer client could stop accepting `upload-offset`, change how it reads `Range: bytes=0-N`, or emit a different resumed `Content-Range` without moving one of the 141 listed digests. Now listed (141 -> 151): UploadStreamResponse, streamFileToHttpRequest, streamFileToHttpRequestAttempt, buildUploadRequestHeaders, isUploadResumeStatus, isUploadRedirectStatus, parseUploadResumeOffset, parseNonNegativeIntegerHeader, firstHeaderValue, MAX_UPLOAD_REDIRECTS. streamFileToHttpRequestAttempt is listed despite its size, unlike createDaemonHttpServer which stays in `uncovered`. The distinction is stated at the declaration: the HTTP server only dispatches to handlers that are each digested, while the attempt loop IS the resume state machine — it decides whether a 308 continues the upload and what the next request carries, so its sequencing alone can break a released daemon while every helper keeps its digest. 6 new planted-red mutations prove the client half moves the ledger: a dropped `upload-offset` fallback, narrowed Range parsing, a changed resumed Content-Range, 308 no longer treated as continue, a narrowed UploadStreamResponse, and dropped header-value coercion. 33 -> 39 tests. Closure fail-closed surfaced two more stops: UploadStreamProgressOptions waived (local byte-progress rendering) and URL/URLSearchParams added to the global set. README now carries a `/upload` resume row in the producer/consumer table, and names the pattern behind three rounds of review: the coverage sentence kept getting written ahead of the coverage, so the table and the `uncovered` notes are the claims to trust — they are checkable against surface.ts, prose is not. --------- Co-authored-by: Claude <noreply@anthropic.com> |