From 9e67e04e4a80f35feb4133a2c0b8f778c77f9b17 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Mon, 4 May 2026 22:37:23 -0500 Subject: [PATCH] fix(install,tests): fmt nit + downloadText flowing-mode bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on PR #684 caught two real issues that local checks missed: **Lint failure (cargo fmt).** A regression test landed with a multi-line `let ContentBlock::Text { text, .. } = real_user.content...` pattern that local rustfmt accepted but CI's pinned toolchain collapsed onto a single line. Reformatted to match. **npm wrapper smoke failure ("Checksum manifest is missing deepseek-").** Subtle Node.js streams interaction in `install.js` introduced by the network-resilience cluster: * `httpRequest` attaches a `data` event listener on the response to re-arm the stall timer. * Attaching a `data` listener on a `Readable` puts the stream into flowing mode immediately. * `downloadText` then ran `for await (const chunk of response)` to collect the body — the async iterator expects paused-mode and silently misses chunks that flow before / between iteration ticks. * For small bodies (the ~100-byte SHA256 manifest), the entire response could flow through the stall listener before the async iterator's `read()` calls landed, leaving the joined body empty. * Result: `parseChecksumManifest("")` returned an empty Map → `verifyChecksum` saw no entries → "manifest is missing X" after the actual binary download succeeded. Binary downloads were unaffected because `download()` uses `response.pipe(sink)` plus a `data` listener for progress — both consume chunks via `data` events, no async iterator involved. Fix: collect the response body in `downloadText` via direct `data`/ `end` event subscription. `data` listeners stack — both the stall re-arm and the body collector fire on every chunk, no flowing-vs- paused conflict. Stall detection still works. Verified locally: `node scripts/release/npm-wrapper-smoke.js` "npm wrapper smoke passed with local assets from ". Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/tui/src/core/engine/tests.rs | 5 +---- npm/deepseek-tui/scripts/install.js | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 00a82681..d6177f8b 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -635,10 +635,7 @@ fn turn_metadata_skips_tool_result_messages() { // The earlier real user message receives the turn_meta prefix. let real_user = messages.first().expect("first user message"); assert_eq!(real_user.role, "user"); - let ContentBlock::Text { text, .. } = real_user - .content - .first() - .expect("user text content") + let ContentBlock::Text { text, .. } = real_user.content.first().expect("user text content") else { panic!("expected Text block on real user message"); }; diff --git a/npm/deepseek-tui/scripts/install.js b/npm/deepseek-tui/scripts/install.js index 52e0219e..9bfd1795 100644 --- a/npm/deepseek-tui/scripts/install.js +++ b/npm/deepseek-tui/scripts/install.js @@ -778,12 +778,26 @@ async function downloadText(url) { stallMs: downloadStallMs(), }); const response = result.response; - const chunks = []; response.setEncoding("utf8"); - for await (const chunk of response) { - chunks.push(chunk); - } - return chunks.join(""); + // NOTE: do NOT use `for await (const chunk of response)` here. + // `httpRequest` attaches a `data` listener on the response to re-arm + // the stall timer, which puts the stream in flowing mode. The async + // iterator expects paused mode and will silently miss every chunk — + // this manifested as an empty checksum manifest in the npm wrapper + // smoke test ("Checksum manifest is missing "). Subscribing + // to `data` events directly stacks alongside the stall listener and + // both fire per chunk, so we collect the body correctly without + // disturbing the stall detection. + return new Promise((resolve, reject) => { + const chunks = []; + response.on("data", (chunk) => { + chunks.push(chunk); + }); + response.on("end", () => { + resolve(chunks.join("")); + }); + response.on("error", reject); + }); }); }