[PR #7150] fix(setup): replace the fixed Hopper download deadline with a stall timeout #7677

Open
opened 2026-11-03 13:39:34 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/7150
Author: @SandstromPL
Created: 10/25/2026
Status: 🔄 Open

Base: main ← Head: fix/hopper-download-stall-timeout


📝 Commits (6)

  • 122ee30 fix(setup): replace the fixed Hopper download deadline with a stall timeout
  • e3b3e5e fix(setup): keep the macOS release feed on the metadata limit
  • b027d36 Merge upstream/main into fix/hopper-download-stall-timeout
  • 8f335d8 Merge upstream/main into fix/hopper-download-stall-timeout
  • d187e4b Merge upstream/main into fix/hopper-download-stall-timeout
  • 9078e40 Merge remote-tracking branch 'upstream/main' into fix/hopper-download-stall-timeout

📊 Changes

8 files changed (+314 additions, -30 deletions)

View changed files

📝 docs/installation.md (+5 -0)
📝 src/application/HopperDownload.ts (+80 -16)
📝 src/application/LinuxHopperPublisher.ts (+3 -4)
📝 src/application/MacHopper.test.ts (+28 -5)
📝 src/application/MacHopper.ts (+8 -2)
📝 tests/boundary/setup/linuxHopperPublisherVerification.test.ts (+42 -1)
➕ tests/process-global/hopperDownloadDeadline.test.ts (+140 -0)
📝 tests/process-global/hopperDownloadHttpFailure.test.ts (+8 -2)

📄 Description

Summary

Refs #7077 (the download-deadline part; the stale feed main_exe_hash is vendor metadata, tracked in #3578).

Hopper setup downloads the vendor package with a fixed 120 s deadline, and downloadHopperPackage refuses any longer timeoutMs. The current Linux demo RPM is 32,261,798 bytes. The reporter measured about 245 KB/s sustained (about 132 s), so rea setup --install-hopper could never finish on that connection. Since #8526, main reports the URL and the 120 s limit, but the download still cannot finish.

After this change a download fails only when no response bytes arrive for 30 s, or when it has not finished within 15 minutes. The failure is #8526's typed timeout download failure, which now also records the limit reached (stalled or deadline) and the receivedBytes. Setup reports it in its download field and remediation.

Problem and expected behavior

  • Smallest trigger: a Hopper package response that keeps delivering bytes but needs more than 120 s in total.
    • Executed with a fake fetch that streams 32,261,798 bytes at 245,000 bytes per second against main's HopperDownload.ts: it fails at 120 s with TimeoutError "The operation was aborted due to timeout". The same run against this branch completes.
  • Expected: a slow but progressing download completes. A stalled or unbounded one fails with a reason that says what stalled, where and how far it got.

Change and scope

  • downloadHopperPackage (src/application/HopperDownload.ts) is the single owner of Hopper download bounds.
    • It replaces AbortSignal.timeout(120_000) with two timers that abort the request. The expired timer's facts are returned as { kind: "timeout", limit, timeoutMs, receivedBytes }.
    • Stall bound: 30 s without the response or its next nonempty chunk. Each chunk resets it.
    • Overall bound: 15 minutes by default, and the largest timeoutMs accepted.
    • Callers passing a shorter timeoutMs get only the overall bound. The release feed on Linux and macOS, and the Linux publisher key, use the shared 10 s HOPPER_METADATA_TIMEOUT_MS.
    • Caller cancellation still rejects with the caller's reason.
  • Setup reporting: #8526 attributes each failed download to its request (release_feed, package or public_key). Setup, doctor and Linux launches project that failure once through describeHopperRequestFailure and hopperRequestRecovery. This PR adds only the stall/deadline facts to the timeout case and its description.
  • Docs: docs/installation.md states both bounds and what a timed-out download reports.
  • Why these bounds: a stall bound follows the real failure, a connection that stopped delivering. The overall bound still caps a connection that trickles just under the stall bound. 15 minutes admits the largest admitted response (100 MB) at about 110 KB/s.
  • No new setting. #7077 suggests a configurable deadline. A stall bound lets slow connections finish without adding a configuration contract.
  • Intentionally not included:
    • The stale feed main_exe_hash (#3578). Hopper 6.6.0 Linux setup still fails integrity verification after a successful download.
    • Download progress output during setup.

Follow the contribution scope.
Unresolved scope, authority, side effects, or required verification block
integration. Link the required maintainer scope decision for new providers or
capabilities involving target execution, injection, mutation, or protection
recovery; ordinary fixes within existing boundaries may state "not applicable".
Draft PRs may leave the scope decision pending and the readiness and merge checklists
unchecked. Maintainers can help resolve open questions.

Contract and boundary impact

  • Semantic owner and earliest changed stage: downloadHopperPackage owns download bounds and produces the timeout fact.
    • Consumers: the macOS and Linux package installers, the macOS release feed fetch, the Linux publisher's metadata, key and verification downloads, and setup's failure remediation.
    • Removed: the fixed AbortSignal.timeout(120_000) and its 120 s ceiling, and the Linux publisher's own copy of the 10 s metadata limit. The earlier HopperDownloadTimeoutError class in this PR is replaced by #8526's returned timeout failure, along with the publisher's converter branch for it.
    • Retained: the byte budget, HTTPS redirect policy and caller cancellation are unchanged.
  • Contribution scope fit and linked maintainer decision: an ordinary fix within existing Hopper setup, so not applicable.
  • CLI and MCP/tool-catalog contract: no tool-catalog change. The setup failure codes are unchanged. For kind: "timeout", setup's download object gains limit and receivedBytes. Its timeoutMs is the limit reached: 30,000 for a stall. The remediation describes which limit was reached.
  • Provider, bridge, target-format, or platform compatibility: Hopper setup on Linux and macOS. Provider analysis is unchanged.
  • Evidence, artifact, provenance, or reconstruction contract: none.
  • Target selection, authority, upstream behavior, and cleanup details (if applicable):
    • Setup's approval, integrity and publisher verification are unchanged.
    • A slow download can now hold setup for up to 15 minutes instead of 120 s, and remains cancellable.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact (CI generates and validates ignored catalogs and skills/; edit authored instructions in .agents/skills/reverse-engineer-anything/ and never commit build outputs): installation docs only.

Expected effects (check known effects; use documentation-only on its own).
Unchecked items do not establish absence; leave uncertainties visible.

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services. (existing Hopper setup downloads; this PR changes only their time bounds)
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:
    • tests/process-global/hopperDownloadDeadline.test.ts (new) uses fake timers and a fake fetch whose body errors with the request signal's reason on abort, as Fetch does. Each case asserts the returned Result:
      • A download that delivers a chunk every 20 s for 180 s completes with all bytes.
      • A response that sends one chunk and then nothing returns timeout after 30 s with limit: "stalled", the URL and receivedBytes: 1024, and the request signal is aborted.
      • A response that never starts returns a stalled timeout with 0 bytes.
      • A download that keeps trickling every 20 s returns limit: "deadline" at 900 s with the bytes received.
    • tests/boundary/setup/linuxHopperPublisherVerification.test.ts:
      • The existing stalled-metadata case expects timeout with limit: "deadline" and receivedBytes: 0.
      • A new case shows currentRelease returning release_metadata with download: { request: "release_feed", kind: "timeout", limit: "deadline", timeoutMs: 10000, receivedBytes: 0 }.
    • src/application/MacHopper.test.ts: the macOS feed fetch passes the 10 s metadata limit, and the package fetch passes none, so it gets the download bounds. main's request-attribution fixture uses the new timeout shape.
    • tests/process-global/hopperDownloadHttpFailure.test.ts (main's): its recovery-class fixture uses the new timeout shape.
    • Removed: this PR's LinuxHopper.test.ts case for a thrown timeout. Timeouts are no longer thrown, and main's typed-failure test covers package-failure attribution.
  • Base reproduction or other evidence (executed; Node 24.22.1, real timers, a fake fetch streaming 32,261,798 bytes at 245,000 bytes per second, the #7077 rate):
    • main (4c8647a70) HopperDownload.ts: rejected after 120 s with TimeoutError "The operation was aborted due to timeout".
    • This branch: ok: true with all 32,261,798 bytes after 133 s.
    • Against main's source, the new fake-timer tests do not reproduce the old failure. main's AbortSignal.timeout runs on real time, so the slow-download case passes vacuously and the stall cases never settle. The real-time run above is the before/after evidence.
  • User-visible CLI/MCP output (if applicable): for a stalled Linux package download, the built describeHopperRequestFailure and hopperRequestRecovery render the setup remediation as:
    The official Hopper package request to https://www.hopperapp.com:443/downloader/public/Hopper-6.6.0-Linux-demo.rpm stalled: no bytes arrived for 30 seconds after 1048576 bytes were received. Check network, DNS, proxy and TLS access to this URL, then retry.
    The input was a hand-built failure; this was not captured from a real setup run.
  • Remaining proof gaps:
    • No real rea setup --install-hopper run and no real vendor download. Tests use a fake fetch and fake installer hosts only.
    • The setup remediation text was rendered from the formatters, not captured from setup.
    • Undici's abort behavior on a real socket is assumed from the Fetch specification: the fetch promise and the body reader both reject with the signal's reason.

For boundary fixes, identify the applicable boundary regressions
and lifecycle regressions
covered by the evidence above, and explain relevant gaps.

  • Independent limits and failures: the byte budget, the stall bound and the overall bound each fail on their own. The existing oversized-chunk and redirect tests still pass unchanged.
  • Lifecycle: both timers are cleared on every exit, and the aborted request releases its body. Caller cancellation keeps its own reason (the existing publisher test). No new resource ownership.
  • Public success and error delivery: not exercised through the CLI. The setup remediation path is covered at the installer level only.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

Run on Linux with Node 24.22.1 (.nvmrc) and npm 11.19.1 (packageManager pins 11.16.0), on upstream/main 4c8647a70:

  • npm run test:focused -- tests/process-global/hopperDownloadDeadline.test.ts tests/process-global/hopperDownloadRedirect.test.ts tests/boundary/setup/linuxHopperPublisherVerification.test.ts: 3 files, 39 tests passed.
  • npm run test:local -- src/application/LinuxHopper.test.ts src/application/MacHopper.test.ts: 2 files, 71 tests passed.
  • The real-time 32 MB at 245 KB/s simulation against main's and this branch's HopperDownload.ts: results above.
  • npx tsc --noEmit -p ., npm run check:fast and npm run docs:check passed.
  • npm run verify:test-discovery: discovered all 2496 test files exactly once.
  • npx oxfmt --check on the changed files and git diff --check: clean.
  • npx oxlint on the changed files: no warnings in the new test file. LinuxHopperPublisher.ts keeps main's existing warnings (max-lines and max-params).
  • Greptile follow-up (e3b3e5e66): the new MacHopper.test.ts case fails on the previous commit, where the feed fetch's timeoutMs is undefined. After the change, npm run test:local -- src/application/MacHopper.test.ts src/application/LinuxHopper.test.ts passes 72 tests, and the 3 download and publisher files pass 39 tests. npx tsc --noEmit -p ., npm run check:fast and npm run docs:check passed. MacHopper.ts keeps main's existing lint warnings.
  • Merge of upstream/main 4557791a2 (b027d362e):
    • Conflict: #7183 had changed downloadHopperPackage in HopperDownload.ts to return the HTTP status of a refused response. The merged function keeps that inside the stall and overall timers.
    • Dependencies: npm ci was rerun for main's zod 4.6.6.
    • Tests: the installer tests passed (72), and the download, publisher and HTTP-failure files passed (4 files, 50 tests). npx tsc --noEmit -p ., npm run check:fast and npm run docs:check passed.
    • Lint: downloadHopperPackage keeps main's complexity warning (23 on main, 24 here).
  • Merge of upstream/main 5b983516a (8f335d815):
    • Conflict: main moved macOS release parsing and the feed URL into HopperReleaseFeed.ts. The macOS feed fetch keeps that ownership and still passes the 10 s metadata limit.
    • Tests: the installer and feed tests passed (3 files, 98 tests), and the download, publisher and HTTP-failure files passed (4 files, 53 tests). npx tsc --noEmit -p ., npm run check:fast and npm run docs:check passed.
  • Merge of upstream/main 37827af59 (d187e4bdd, no conflicts): this picks up main's fixes for the WorkBuddy client-configuration and keyed-archive tests that had failed this PR's CI only because main failed them.
    • After npm ci, the installer and feed tests passed (3 files, 109 tests).
    • The download, publisher and HTTP-failure files plus those two tests passed (6 files, 67 tests).
    • npx tsc --noEmit -p ., npm run check:fast and npm run docs:check passed.
  • Merge of upstream/main cbc69537c (9078e40dd):
    • Conflict: #8526 changed downloadHopperPackage to return typed failures, including { kind: "timeout", timeoutMs }. The merge keeps those failures and the stall and overall timers. The timeout case now carries limit and receivedBytes, replacing this PR's thrown HopperDownloadTimeoutError. The superseded class, its publisher converter branch and its unit test were removed.
    • Tests: the installer and setup tests passed (3 files, 106 tests). The deadline, HTTP-failure, redirect and publisher files passed (4 files, 72 tests).
    • Checks: npx tsc --noEmit -p ., npm run docs:check, the pre-push checks and npx oxfmt --check passed. npx oxlint on HopperDownload.ts is clean.
  • Not run: npm run check:pr, or any real Hopper setup or vendor download.

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
    • downloadHopperPackage now accepts timeoutMs up to 900,000 instead of 120,000.
    • A timeout download failure gains limit and receivedBytes, and its timeoutMs reports the limit reached. All producers and consumers of the type are updated in this PR.
    • The macOS release feed fetch now has the 10 s metadata limit that Linux already used, instead of sharing the 120 s package deadline.
  • Real Hopper/Ghidra, browser, or OS coverage: none; fake fetch and fake installer hosts on Linux.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review:
    • Setup may hold an open vendor connection for up to 15 minutes instead of 120 s, bounded by the existing 100 MB response budget.
    • The timeout message includes the vendor URL, which setup already prints.

Author readiness

Human owner: @SandstromPL

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/7150 **Author:** [@SandstromPL](https://github.com/SandstromPL) **Created:** 10/25/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `fix/hopper-download-stall-timeout` --- ### 📝 Commits (6) - [`122ee30`](https://github.com/morluto/rea/commit/122ee30fad45c513f0c1110a3f4fb655edcc19dc) fix(setup): replace the fixed Hopper download deadline with a stall timeout - [`e3b3e5e`](https://github.com/morluto/rea/commit/e3b3e5e6647f82d60f5809df409aff095cf778c3) fix(setup): keep the macOS release feed on the metadata limit - [`b027d36`](https://github.com/morluto/rea/commit/b027d362e40ad9c0d5505ef5bd608d2f7906596d) Merge upstream/main into fix/hopper-download-stall-timeout - [`8f335d8`](https://github.com/morluto/rea/commit/8f335d815c353c4d1793e9a6c36fae9fa4d69b10) Merge upstream/main into fix/hopper-download-stall-timeout - [`d187e4b`](https://github.com/morluto/rea/commit/d187e4bdddcf772e16600fe2a4947b5f3362c203) Merge upstream/main into fix/hopper-download-stall-timeout - [`9078e40`](https://github.com/morluto/rea/commit/9078e40ddd5d79f8b6608e36fb16c63474bf0449) Merge remote-tracking branch 'upstream/main' into fix/hopper-download-stall-timeout ### 📊 Changes **8 files changed** (+314 additions, -30 deletions) <details> <summary>View changed files</summary> 📝 `docs/installation.md` (+5 -0) 📝 `src/application/HopperDownload.ts` (+80 -16) 📝 `src/application/LinuxHopperPublisher.ts` (+3 -4) 📝 `src/application/MacHopper.test.ts` (+28 -5) 📝 `src/application/MacHopper.ts` (+8 -2) 📝 `tests/boundary/setup/linuxHopperPublisherVerification.test.ts` (+42 -1) ➕ `tests/process-global/hopperDownloadDeadline.test.ts` (+140 -0) 📝 `tests/process-global/hopperDownloadHttpFailure.test.ts` (+8 -2) </details> ### 📄 Description ## Summary Refs #7077 (the download-deadline part; the stale feed `main_exe_hash` is vendor metadata, tracked in #3578). Hopper setup downloads the vendor package with a fixed 120 s deadline, and `downloadHopperPackage` refuses any longer `timeoutMs`. The current Linux demo RPM is 32,261,798 bytes. The reporter measured about 245 KB/s sustained (about 132 s), so `rea setup --install-hopper` could never finish on that connection. Since #8526, `main` reports the URL and the 120 s limit, but the download still cannot finish. After this change a download fails only when no response bytes arrive for 30 s, or when it has not finished within 15 minutes. The failure is #8526's typed `timeout` download failure, which now also records the `limit` reached (`stalled` or `deadline`) and the `receivedBytes`. Setup reports it in its `download` field and remediation. ## Problem and expected behavior - **Smallest trigger:** a Hopper package response that keeps delivering bytes but needs more than 120 s in total. - Executed with a fake `fetch` that streams 32,261,798 bytes at 245,000 bytes per second against `main`'s `HopperDownload.ts`: it fails at 120 s with `TimeoutError` "The operation was aborted due to timeout". The same run against this branch completes. - **Expected:** a slow but progressing download completes. A stalled or unbounded one fails with a reason that says what stalled, where and how far it got. ## Change and scope <!-- Explain the chosen approach, why it fits REA's architecture, and what is intentionally not included. If adding complexity for a speculative or vanishingly unlikely race or security edge case, record the concrete failure mode, likelihood, complexity cost, and the user's buy-in obtained before implementation. Do not silently expand scope for theoretical robustness. --> - **`downloadHopperPackage`** (`src/application/HopperDownload.ts`) is the single owner of Hopper download bounds. - It replaces `AbortSignal.timeout(120_000)` with two timers that abort the request. The expired timer's facts are returned as `{ kind: "timeout", limit, timeoutMs, receivedBytes }`. - **Stall bound:** 30 s without the response or its next nonempty chunk. Each chunk resets it. - **Overall bound:** 15 minutes by default, and the largest `timeoutMs` accepted. - Callers passing a shorter `timeoutMs` get only the overall bound. The release feed on Linux and macOS, and the Linux publisher key, use the shared 10 s `HOPPER_METADATA_TIMEOUT_MS`. - Caller cancellation still rejects with the caller's reason. - **Setup reporting:** #8526 attributes each failed download to its request (`release_feed`, `package` or `public_key`). Setup, doctor and Linux launches project that failure once through `describeHopperRequestFailure` and `hopperRequestRecovery`. This PR adds only the stall/deadline facts to the `timeout` case and its description. - **Docs:** `docs/installation.md` states both bounds and what a timed-out download reports. - **Why these bounds:** a stall bound follows the real failure, a connection that stopped delivering. The overall bound still caps a connection that trickles just under the stall bound. 15 minutes admits the largest admitted response (100 MB) at about 110 KB/s. - **No new setting.** #7077 suggests a configurable deadline. A stall bound lets slow connections finish without adding a configuration contract. - **Intentionally not included:** - The stale feed `main_exe_hash` (#3578). Hopper 6.6.0 Linux setup still fails integrity verification after a successful download. - Download progress output during setup. Follow the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope). Unresolved scope, authority, side effects, or required verification block integration. Link the required maintainer scope decision for new providers or capabilities involving target execution, injection, mutation, or protection recovery; ordinary fixes within existing boundaries may state "not applicable". Draft PRs may leave the scope decision pending and the readiness and merge checklists unchecked. Maintainers can help resolve open questions. ## Contract and boundary impact <!-- Short answers are welcome. Complete applicable lines; mark unresolved facts as "unknown". Use "none" or "not applicable" only when established. --> - Semantic owner and earliest changed stage: `downloadHopperPackage` owns download bounds and produces the timeout fact. - **Consumers:** the macOS and Linux package installers, the macOS release feed fetch, the Linux publisher's metadata, key and verification downloads, and setup's failure remediation. - **Removed:** the fixed `AbortSignal.timeout(120_000)` and its 120 s ceiling, and the Linux publisher's own copy of the 10 s metadata limit. The earlier `HopperDownloadTimeoutError` class in this PR is replaced by #8526's returned `timeout` failure, along with the publisher's converter branch for it. - **Retained:** the byte budget, HTTPS redirect policy and caller cancellation are unchanged. - Contribution scope fit and linked maintainer decision: an ordinary fix within existing Hopper setup, so not applicable. - CLI and MCP/tool-catalog contract: no tool-catalog change. The setup failure codes are unchanged. For `kind: "timeout"`, setup's `download` object gains `limit` and `receivedBytes`. Its `timeoutMs` is the limit reached: 30,000 for a stall. The remediation describes which limit was reached. - Provider, bridge, target-format, or platform compatibility: Hopper setup on Linux and macOS. Provider analysis is unchanged. - Evidence, artifact, provenance, or reconstruction contract: none. - Target selection, authority, upstream behavior, and cleanup details (if applicable): - Setup's approval, integrity and publisher verification are unchanged. - A slow download can now hold setup for up to 15 minutes instead of 120 s, and remains cancellable. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `.agents/skills/reverse-engineer-anything/` and never commit build outputs): installation docs only. Expected effects (check known effects; use documentation-only on its own). Unchecked items do not establish absence; leave uncertainties visible. - [ ] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [x] Contact network services. (existing Hopper setup downloads; this PR changes only their time bounds) - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: - `tests/process-global/hopperDownloadDeadline.test.ts` (new) uses fake timers and a fake `fetch` whose body errors with the request signal's reason on abort, as Fetch does. Each case asserts the returned `Result`: - A download that delivers a chunk every 20 s for 180 s completes with all bytes. - A response that sends one chunk and then nothing returns `timeout` after 30 s with `limit: "stalled"`, the URL and `receivedBytes: 1024`, and the request signal is aborted. - A response that never starts returns a stalled `timeout` with 0 bytes. - A download that keeps trickling every 20 s returns `limit: "deadline"` at 900 s with the bytes received. - `tests/boundary/setup/linuxHopperPublisherVerification.test.ts`: - The existing stalled-metadata case expects `timeout` with `limit: "deadline"` and `receivedBytes: 0`. - A new case shows `currentRelease` returning `release_metadata` with `download: { request: "release_feed", kind: "timeout", limit: "deadline", timeoutMs: 10000, receivedBytes: 0 }`. - `src/application/MacHopper.test.ts`: the macOS feed fetch passes the 10 s metadata limit, and the package fetch passes none, so it gets the download bounds. `main`'s request-attribution fixture uses the new `timeout` shape. - `tests/process-global/hopperDownloadHttpFailure.test.ts` (`main`'s): its recovery-class fixture uses the new `timeout` shape. - Removed: this PR's `LinuxHopper.test.ts` case for a thrown timeout. Timeouts are no longer thrown, and `main`'s typed-failure test covers package-failure attribution. - Base reproduction or other evidence (executed; Node 24.22.1, real timers, a fake `fetch` streaming 32,261,798 bytes at 245,000 bytes per second, the #7077 rate): - `main` (4c8647a70) `HopperDownload.ts`: rejected after 120 s with `TimeoutError` "The operation was aborted due to timeout". - This branch: `ok: true` with all 32,261,798 bytes after 133 s. - Against `main`'s source, the new fake-timer tests do not reproduce the old failure. `main`'s `AbortSignal.timeout` runs on real time, so the slow-download case passes vacuously and the stall cases never settle. The real-time run above is the before/after evidence. - User-visible CLI/MCP output (if applicable): for a stalled Linux package download, the built `describeHopperRequestFailure` and `hopperRequestRecovery` render the setup remediation as: `The official Hopper package request to https://www.hopperapp.com:443/downloader/public/Hopper-6.6.0-Linux-demo.rpm stalled: no bytes arrived for 30 seconds after 1048576 bytes were received. Check network, DNS, proxy and TLS access to this URL, then retry.` The input was a hand-built failure; this was not captured from a real setup run. - Remaining proof gaps: - No real `rea setup --install-hopper` run and no real vendor download. Tests use a fake `fetch` and fake installer hosts only. - The setup remediation text was rendered from the formatters, not captured from setup. - Undici's abort behavior on a real socket is assumed from the Fetch specification: the fetch promise and the body reader both reject with the signal's reason. For boundary fixes, identify the applicable [boundary regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#boundary-regressions) and [lifecycle regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#lifecycle-regressions) covered by the evidence above, and explain relevant gaps. - **Independent limits and failures:** the byte budget, the stall bound and the overall bound each fail on their own. The existing oversized-chunk and redirect tests still pass unchanged. - **Lifecycle:** both timers are cleared on every exit, and the aborted request releases its body. Caller cancellation keeps its own reason (the existing publisher test). No new resource ownership. - **Public success and error delivery:** not exercised through the CLI. The setup remediation path is covered at the installer level only. For evidence-bearing changes: - [ ] Observed, derived, and inferred claims remain distinguishable. - [ ] Artifact identity, source provenance, and failed attempts remain preserved. - [ ] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed Run on Linux with Node 24.22.1 (`.nvmrc`) and npm 11.19.1 (`packageManager` pins 11.16.0), on `upstream/main` 4c8647a70: - `npm run test:focused -- tests/process-global/hopperDownloadDeadline.test.ts tests/process-global/hopperDownloadRedirect.test.ts tests/boundary/setup/linuxHopperPublisherVerification.test.ts`: 3 files, 39 tests passed. - `npm run test:local -- src/application/LinuxHopper.test.ts src/application/MacHopper.test.ts`: 2 files, 71 tests passed. - The real-time 32 MB at 245 KB/s simulation against `main`'s and this branch's `HopperDownload.ts`: results above. - `npx tsc --noEmit -p .`, `npm run check:fast` and `npm run docs:check` passed. - `npm run verify:test-discovery`: discovered all 2496 test files exactly once. - `npx oxfmt --check` on the changed files and `git diff --check`: clean. - `npx oxlint` on the changed files: no warnings in the new test file. `LinuxHopperPublisher.ts` keeps `main`'s existing warnings (`max-lines` and `max-params`). - Greptile follow-up (`e3b3e5e66`): the new `MacHopper.test.ts` case fails on the previous commit, where the feed fetch's `timeoutMs` is `undefined`. After the change, `npm run test:local -- src/application/MacHopper.test.ts src/application/LinuxHopper.test.ts` passes 72 tests, and the 3 download and publisher files pass 39 tests. `npx tsc --noEmit -p .`, `npm run check:fast` and `npm run docs:check` passed. `MacHopper.ts` keeps `main`'s existing lint warnings. - Merge of `upstream/main` 4557791a2 (`b027d362e`): - **Conflict:** #7183 had changed `downloadHopperPackage` in `HopperDownload.ts` to return the HTTP status of a refused response. The merged function keeps that inside the stall and overall timers. - **Dependencies:** `npm ci` was rerun for `main`'s zod 4.6.6. - **Tests:** the installer tests passed (72), and the download, publisher and HTTP-failure files passed (4 files, 50 tests). `npx tsc --noEmit -p .`, `npm run check:fast` and `npm run docs:check` passed. - **Lint:** `downloadHopperPackage` keeps `main`'s complexity warning (23 on `main`, 24 here). - Merge of `upstream/main` 5b983516a (`8f335d815`): - **Conflict:** `main` moved macOS release parsing and the feed URL into `HopperReleaseFeed.ts`. The macOS feed fetch keeps that ownership and still passes the 10 s metadata limit. - **Tests:** the installer and feed tests passed (3 files, 98 tests), and the download, publisher and HTTP-failure files passed (4 files, 53 tests). `npx tsc --noEmit -p .`, `npm run check:fast` and `npm run docs:check` passed. - Merge of `upstream/main` 37827af59 (`d187e4bdd`, no conflicts): this picks up `main`'s fixes for the WorkBuddy client-configuration and keyed-archive tests that had failed this PR's CI only because `main` failed them. - After `npm ci`, the installer and feed tests passed (3 files, 109 tests). - The download, publisher and HTTP-failure files plus those two tests passed (6 files, 67 tests). - `npx tsc --noEmit -p .`, `npm run check:fast` and `npm run docs:check` passed. - Merge of `upstream/main` cbc69537c (`9078e40dd`): - **Conflict:** #8526 changed `downloadHopperPackage` to return typed failures, including `{ kind: "timeout", timeoutMs }`. The merge keeps those failures and the stall and overall timers. The `timeout` case now carries `limit` and `receivedBytes`, replacing this PR's thrown `HopperDownloadTimeoutError`. The superseded class, its publisher converter branch and its unit test were removed. - **Tests:** the installer and setup tests passed (3 files, 106 tests). The deadline, HTTP-failure, redirect and publisher files passed (4 files, 72 tests). - **Checks:** `npx tsc --noEmit -p .`, `npm run docs:check`, the pre-push checks and `npx oxfmt --check` passed. `npx oxlint` on `HopperDownload.ts` is clean. - Not run: `npm run check:pr`, or any real Hopper setup or vendor download. ## Compatibility, safety, and release - Breaking changes or migration steps: none. - `downloadHopperPackage` now accepts `timeoutMs` up to 900,000 instead of 120,000. - A `timeout` download failure gains `limit` and `receivedBytes`, and its `timeoutMs` reports the limit reached. All producers and consumers of the type are updated in this PR. - The macOS release feed fetch now has the 10 s metadata limit that Linux already used, instead of sharing the 120 s package deadline. - Real Hopper/Ghidra, browser, or OS coverage: none; fake `fetch` and fake installer hosts on Linux. - Package or release metadata impact: none. - Security, privacy, process, or containment review: - Setup may hold an open vendor connection for up to 15 minutes instead of 120 s, bounded by the existing 100 MB response budget. - The timeout message includes the vendor URL, which setup already prints. ## Author readiness <!-- The human owner completes these declarations before requesting review. The PR author is the owner by default. A different owner must explicitly accept ownership on this PR. AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA. Draft PRs may leave them unchecked. These declarations record initial readiness when opening the PR for review; later pushes do not require a new human sign-off. Keep the description and validation evidence current as the work changes. See CONTRIBUTING.md#pull-request-ownership-and-readiness. --> Human owner: @SandstromPL - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge <!-- Draft PRs may leave pending items unchecked. Resolve material scope and authority questions before integration. --> - [ ] This PR addresses a concrete problem or an agreed enhancement. - [ ] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [ ] The PR has one focused outcome and the title follows `type(scope): outcome`. - [ ] Related issue is linked, or the reason for not linking one is stated above. - [ ] Tests cover changed observable behavior and meaningful failure paths. - [ ] Owning docs, contracts, and generated metadata are updated where needed. - [ ] User-visible CLI/MCP changes include representative output. - [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#7677
No description provided.