Skip to content

fix(replay): bound detached stream memory (#1408) - #1413

Closed
ding113 wants to merge 2 commits into
devfrom
fix/1408-replay-memory-leak
Closed

fix(replay): bound detached stream memory (#1408)#1413
ding113 wants to merge 2 commits into
devfrom
fix/1408-replay-memory-leak

Conversation

@ding113

@ding113 ding113 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

背景

Replay owner 在客户端断线后可能同时保留完整的本地 parts[] payload, Redis write-behind batch 和长达 300 秒的 detached transport. 持续断线流会在最早请求回收前叠加并触发 Node heap OOM.

修复

  • 删除活跃 ReplaySpool.parts[], Redis LIST 成为流块的唯一长期正文副本.
  • 完成时通过 owner-fenced Lua 原子校验 token 和 LIST generation, 从 Redis chunks 重建 durable payload.
  • 将大 payload 的读取, 拼接和 PG 持久化限制为最多 2 个并发操作.
  • rebuild slot 等待超过 30 秒会从队列移除并 fail-open, 不会无限占用 spool 或吞掉后续 permit.
  • 为等待在当前 Redis write 后面的 backlog 设置 512 KiB 上限; bootstrap owning-meta write 也计入前置操作, Redis 变慢时 fail-open 关闭当前 spool.
  • 按实际保留文本的 UTF-8 大小计数, 使用 Buffer.byteLength 避免逐 chunk 临时编码副本, 保留单个健康的大 batch, 并在 final append 后立即清空本地 batch.
  • spool disable, halt, abort 或 complete 后, detached drain 按断线起点收紧到普通 60 秒, 且不会延长更短的显式配置.
  • hard deadline 会 abort upstream, cancel ReadableStream source 并结束后台 processing task.

验证

  • 聚焦 Vitest: 4 files, 177 tests passed.
  • 40 个并发 spool, 每个累计 7.5 MiB: 不存在完整本地 parts[], pendingBytesqueuedBytes 回到 0, 进程存活.
  • 覆盖 slow Redis backlog, bootstrap 阻塞首批, 513 KiB 单 batch, 跨 chunk UTF-8, LIST generation mismatch, 2-slot rebuild limiter 与 waiter timeout, Redis payload null, final batch release 和 10/60 秒 detached deadline.
  • 全量 Vitest: 860 files passed, 2 skipped; 8469 tests passed, 13 skipped.
  • bun run lint:fix, bun run lint, bun run typecheck, bun run buildgit diff --check 通过.

运行时回归边界

调查报告列出的 /private/tmp/cch1408-* 容器压测夹具不在当前 Linux 主机, 因此本 PR 不声明新的正式 heap/RSS 对比数据. 合并前建议在保留原 PostgreSQL, Redis, mock upstream 和 1 GiB app 容器夹具的隔离环境, 对最终镜像重跑同一 40+ 请求波次.

关联

Greptile Summary

The PR bounds memory retained by detached replay streams while preserving fenced replay completion.

  • Removes the long-lived in-process replay payload copy and rebuilds completed payloads from owner-fenced Redis chunks.
  • Caps queued Redis write-behind data and concurrent large-payload reconstruction.
  • Tightens detached-stream deadlines after replay termination and explicitly cancels upstream processing at the deadline.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/replay/replay-spool.ts Replaces the retained payload copy with bounded write-behind accounting, fenced Redis reconstruction, and a two-slot large-payload completion limiter.
src/app/v1/_lib/proxy/replay/replay-store.ts Adds atomic LIST-generation checks to owner writes and owner-fenced reads of complete replay chunks.
src/app/v1/_lib/proxy/response-handler.ts Re-arms detached-drain deadlines from the original disconnect time when the replay spool becomes terminal and cancels all stream processing at expiry.
tests/unit/proxy/replay-spool.test.ts Expands coverage for memory bounds, UTF-8 accounting, generation fencing, reconstruction concurrency, completion cleanup, and terminal notifications.
tests/unit/proxy/replay-store.test.ts Verifies atomic owner and LIST-length fencing for replay writes and payload reads.
tests/unit/proxy/response-handler-client-abort-drain.test.ts Covers detached-drain deadline tightening and replay-terminal listener lifecycle.
tests/unit/proxy/response-handler-stream-terminal.test.ts Exercises terminal stream behavior under the revised cancellation and completion lifecycle.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Handler as ResponseHandler
  participant Spool as ReplaySpool
  participant Redis
  participant PG as PostgreSQL
  participant Upstream

  Client->>Handler: Start streaming request
  Handler->>Spool: observe(chunk)
  Spool->>Redis: Fenced append with expected LIST length
  Client--xHandler: Disconnect
  Handler->>Handler: Start bounded detached drain
  Upstream-->>Handler: Remaining chunks
  Handler->>Spool: completeAfterBilling()
  Spool->>Redis: Append final batch
  Spool->>Redis: Fenced read of complete chunk LIST
  Spool->>PG: Persist reconstructed payload
  Spool->>Redis: Mark replay completed
  Spool-->>Handler: Terminal notification
  Handler->>Handler: Cap drain deadline to 60 seconds
  Handler--xUpstream: Abort and cancel if deadline expires
Loading

Reviews (2): Last reviewed commit: "fix(replay): bound bootstrap backlog and..." | Re-trigger Greptile

Context used:

@github-actions github-actions Bot added bug Something isn't working area:core size/L Large PR (< 1000 lines) labels Aug 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c36678adb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +547 to +553
private trackQueuedBatch(batch: QueuedReplayBatch): void {
this.queuedBatches.add(batch);
if (this.activeWriteBatch) {
this.queuedBytes += batch.byteSize;
} else {
this.activeWriteBatch = batch;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count batches queued behind bootstrap

In the normal owner path, bootstrap() first places an untracked Redis write on writeChain, but the first response batch is still marked as activeWriteBatch here and excluded from queuedBytes. If the bootstrap Redis call stalls, that batch is actually waiting behind it and can retain up to REPLAY_MAX_PAYLOAD_BYTES because the write-behind check exempts it; with the configured concurrent-spool limit, slow Redis can therefore recreate the large detached-stream heap growth this change is intended to prevent. Only exempt the batch once its write callback actually starts, or account for it while any preceding chain operation is in flight.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Replay 生命周期

Layer / File(s) Summary
Redis chunk fencing and snapshot reads
src/app/v1/_lib/proxy/replay/replay-store.ts, tests/unit/proxy/replay-store.test.ts
ReplayStore 在写入和读取时校验 owner token 与预期 chunk 数,并支持原子读取完整 chunk 快照。测试覆盖 mismatch、stale owner 和数据保留行为。
Bounded spool buffering and terminal cleanup
src/app/v1/_lib/proxy/replay/replay-spool.ts, tests/unit/proxy/replay-spool.test.ts
ReplaySpool 跟踪 UTF-8 字节数、活动批次和 pending 写入。超过缓存上限或 chunk 数不匹配时禁用 spool,并清理批次状态。
Durable payload reconstruction and persistence
src/app/v1/_lib/proxy/replay/replay-spool.ts, tests/unit/proxy/replay-spool.test.ts
完成流程从 Redis 读取 durable chunks,重建 payload 后写入 PG。大 payload 重建受并发限制,读取失败时不执行 PG 持久化。
Terminal-aware detached drain
src/app/v1/_lib/proxy/response-handler.ts, tests/unit/proxy/response-handler-client-abort-drain.test.ts, tests/unit/proxy/response-handler-stream-terminal.test.ts
客户端断开处理使用独立的有界 drain 定时器。ReplaySpool 进入终态后重新计算剩余窗口,并移除终态监听器。

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: brisbanehuang

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed PR 针对 [#1408] 的 Replay/流式响应内存失控问题,移除本地 payload 副本并限制 backlog、并发和 detached drain。
Out of Scope Changes check ✅ Passed 代码和测试变更均支持 [#1408] 的内存控制目标,未发现与关联问题无关的代码改动。
Title check ✅ Passed 标题准确概括了限制 detached stream 内存的主要修复,内容简洁且与变更相关。
Description check ✅ Passed 描述详细说明了 ReplaySpool、Redis、detached drain 和并发限制的变更,且与变更集完全相关。
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1408-replay-memory-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/app/v1/_lib/proxy/replay/replay-spool.ts (1)

132-132: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

热路径上为计算字节数分配了完整副本。

this.encoder.encode(text) 会为每个 chunk 分配一份与文本等长的 Uint8Array,随后立即丢弃。该调用位于流式热路径,且本 PR 的目标是降低堆压力。改用 Buffer.byteLength(text, "utf8") 可以只计算长度,不分配缓冲区。同样的替换也适用于 Line 302 的尾部计量。

请确认该模块只在 Node.js runtime 执行(Buffer 在 edge runtime 不可用)。

♻️ 建议改动
-      this.pendingBytes += this.encoder.encode(text).byteLength;
+      this.pendingBytes += Buffer.byteLength(text, "utf8");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/replay/replay-spool.ts` at line 132, Replace the
per-chunk encoder allocation used to update pendingBytes with
Buffer.byteLength(text, "utf8"), and apply the same change to the trailing byte
accounting near the second measurement site. Confirm replay-spool.ts runs only
in the Node.js runtime before relying on Buffer, preserving UTF-8 byte counts
without creating Uint8Array copies.
src/app/v1/_lib/proxy/replay/replay-store.ts (1)

84-97: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

可选:避免在 Lua 中复制整个 chunks 表。

当前实现先 LRANGE 得到 chunks,再逐项复制到 result。对大 payload,这会在 Redis 的 Lua 栈内短暂占用两份完整正文。使用 table.insert(chunks, 1, 1) 可以原地加前缀,减少一份副本。该改动不改变返回结构。

♻️ 建议改动
 local chunks = redis.call('LRANGE', KEYS[2], 0, -1)
-local result = {1}
-for i = 1, `#chunks` do
-  result[`#result` + 1] = chunks[i]
-end
-return result`;
+table.insert(chunks, 1, 1)
+return chunks`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/replay/replay-store.ts` around lines 84 - 97, Update
LUA_READ_OWNED_CHUNKS to prepend the success marker directly to the table
returned by LRANGE using an in-place insertion, then return that table. Remove
the separate result table and chunk-copying loop while preserving the existing
ownership and length checks and the {1, ...chunks} response structure.
tests/unit/proxy/replay-spool.test.ts (1)

691-692: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

可选:补充 readOwnedChunks 返回 null 的用例。

当前测试覆盖了 reject(异常)与 false(token 失效或数量不完整)两条分支。实现中还有 chunks === null 分支,对应 Redis 不可用,错误信息为 "final replay payload read failed"。该分支决定了 Redis 短暂不可用时不会误写 PG,值得单独覆盖。

💚 建议补充用例
+  it("Redis 不可用导致 payload 读取返回 null 时不持久化 PG", async () => {
+    storeControl.store.readOwnedChunks.mockResolvedValueOnce(null);
+    const spool = makeSpool();
+    spool.observe(encoder.encode("data: partial\n\n"));
+
+    await spool.completeAfterBilling(12);
+
+    expect(storeControl.store.persistCompleted).not.toHaveBeenCalled();
+    expect(storeControl.store.abortOwned).toHaveBeenCalled();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/replay-spool.test.ts` around lines 691 - 692, 在 replay-spool
测试中补充 `readOwnedChunks` 返回 `null` 的独立用例,覆盖实现中的 `chunks === null` 分支。参考现有 fenced
payload 读取失败测试,验证 Redis 暂时不可用时记录 `"final replay payload read failed"`,不会误写
PG,并保持 heartbeat 与并发配额按预期释放。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/v1/_lib/proxy/replay/replay-spool.ts`:
- Around line 45-67: 为 withPayloadRebuildSlot 增加有界等待,避免
largePayloadRebuildWaiters 中的请求无限期排队;等待超过配置的超时时间后应放弃重建,并按现有 disable/abort 的
fail-open 路径继续处理。确保超时、操作失败或正常完成时都不会错误增加 activeLargePayloadRebuilds,也不会遗留等待队列项。

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 4930-4931: 将 replay spool 终态监听器的清理从正常完成路径移至 finally 块,确保
activeResponsePump.completion 抛错或进入 catch 时也会执行
cleanupReplaySpoolTerminalListener();同时保持与 cleanupClientAbortListener 一致,并移除原
try 块中的重复清理。

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/replay/replay-spool.ts`:
- Line 132: Replace the per-chunk encoder allocation used to update pendingBytes
with Buffer.byteLength(text, "utf8"), and apply the same change to the trailing
byte accounting near the second measurement site. Confirm replay-spool.ts runs
only in the Node.js runtime before relying on Buffer, preserving UTF-8 byte
counts without creating Uint8Array copies.

In `@src/app/v1/_lib/proxy/replay/replay-store.ts`:
- Around line 84-97: Update LUA_READ_OWNED_CHUNKS to prepend the success marker
directly to the table returned by LRANGE using an in-place insertion, then
return that table. Remove the separate result table and chunk-copying loop while
preserving the existing ownership and length checks and the {1, ...chunks}
response structure.

In `@tests/unit/proxy/replay-spool.test.ts`:
- Around line 691-692: 在 replay-spool 测试中补充 `readOwnedChunks` 返回 `null`
的独立用例,覆盖实现中的 `chunks === null` 分支。参考现有 fenced payload 读取失败测试,验证 Redis 暂时不可用时记录
`"final replay payload read failed"`,不会误写 PG,并保持 heartbeat 与并发配额按预期释放。
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 391ad142-08b5-4f98-91a2-a5216f3f155a

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe3225 and c36678a.

📒 Files selected for processing (7)
  • src/app/v1/_lib/proxy/replay/replay-spool.ts
  • src/app/v1/_lib/proxy/replay/replay-store.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • tests/unit/proxy/replay-spool.test.ts
  • tests/unit/proxy/replay-store.test.ts
  • tests/unit/proxy/response-handler-client-abort-drain.test.ts
  • tests/unit/proxy/response-handler-stream-terminal.test.ts

Comment thread src/app/v1/_lib/proxy/replay/replay-spool.ts
Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR eliminates the replay/streaming memory amplification that caused the ~30 GiB OOM in #1408 by removing the in-process parts[] body copy (Redis LIST becomes the sole long-term body), bounding the write-behind backlog to 512 KiB with fail-open, limiting concurrent large-payload rebuilds to 2 slots, and tightening the detached drain window to 60 s once the owning spool reaches a terminal state. The concurrency reasoning (write-chain serialization, owner-fenced Lua with LIST-generation checks, slot-transfer accounting in the rebuild limiter, idempotent batch release) is internally consistent and every error path is logged or surfaced.

PR Size: L

  • Lines changed: 874 (713 additions, 161 deletions)
  • Files changed: 7 (3 source, 4 test)

Split considerations (required for size L): The change is cohesive around a single incident. The only cleanly separable unit is the detached-drain tightening in response-handler.ts (the onTerminal listener + armClientAbortDrainTimer refactor, ~40 lines), which is independent of the replay-spool memory work and could land separately. The remaining replay-spool/store changes (Redis-as-source-of-truth, write-behind limit, rebuild limiter, fenced read) are mutually dependent and do not split further without breaking invariants.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

No issues at or above the 80% confidence threshold.

Notes (below threshold, FYI only — not action items)

  • onTerminal invokes the listener synchronously without the try/catch guard that notifyTerminal uses, but the sole registered listener (Math.min + armClientAbortDrainTimer) cannot throw in practice.
  • observe() now calls this.encoder.encode(text).byteLength per chunk to count retained UTF-8 bytes correctly across split multi-byte sequences; the transient allocation is immediately GC-eligible and does not undermine the PR's memory-bounding goal.

Verification

  • Ran the 4 changed test files locally: 173 tests passed (83 replay-spool/store + 90 response-handler), matching the PR description.
  • Confirmed withPayloadRebuildSlot slot-transfer accounting holds under error, bypass, and wait-queue interleavings.
  • Confirmed releaseQueuedBatch is idempotent (early clear in completeAfterBilling + finally no-op) and that clearQueuedBatches cannot corrupt the in-flight write-chain callback's already-sent args.
  • Confirmed every terminal-state transition (completeAfterBilling, abort, teardown) calls notifyTerminal exactly once, and the drain re-arm clears the prior timer before setting a new one.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Claude AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

No significant issues identified in this PR.

PR Size: L

  • Lines changed: 874
  • Files changed: 7
  • Split suggestion: If this area changes again, consider separating replay storage/spool invariants from response-handler drain-window behavior to keep future review surfaces smaller.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Reviewed PR #1413 end-to-end across logic, security, error handling, types, docs, tests, and simplification, with validation against full file context for candidate concerns.
  • Applied the size/L label and posted the required GitHub review summary.
  • No diff-line issues survived the false-positive and confidence filters, so I did not submit any inline review comments.
  • I also tried to run the focused Vitest files, but this runner does not have bun/bunx installed, so the final review is based on code/context inspection rather than local test execution.

Track pending write operations so the write-behind byte limit also applies while the bootstrap owning-meta write is in flight. Add a 30s timeout that removes stalled large-payload rebuild waiters without consuming a future permit. Move replay terminal listener cleanup into the stream processing finally block, replace per-chunk TextEncoder allocations with Buffer.byteLength, and simplify the fenced Redis chunk-read Lua result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/proxy/response-handler-client-abort-drain.test.ts`:
- Around line 1253-1255: 在该测试中补充对 session.releaseAgent 的调用断言,确保异常处理路径确实执行了该
hook。使用现有的 session.releaseAgent mock,在触发生产代码后断言其已被调用。
- Line 1262: Update the async task cleanup at the Promise.allSettled call to
avoid discarding rejected background-task results: use Promise.all so any
unexpected rejection fails the test, or explicitly inspect and assert every
settled result, separately documenting any intentionally expected rejection.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: de758c3b-6b7e-499a-a1f4-cb48df850ad7

📥 Commits

Reviewing files that changed from the base of the PR and between c36678a and defecfc.

📒 Files selected for processing (5)
  • src/app/v1/_lib/proxy/replay/replay-spool.ts
  • src/app/v1/_lib/proxy/replay/replay-store.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • tests/unit/proxy/replay-spool.test.ts
  • tests/unit/proxy/response-handler-client-abort-drain.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/replay/replay-store.ts
  • src/app/v1/_lib/proxy/replay/replay-spool.ts

Comment on lines +1253 to +1255
vi.mocked(session.releaseAgent).mockImplementationOnce(() => {
throw new Error("release agent failed");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

断言 session.releaseAgent 确实被调用。

当前测试只配置了 mockImplementationOnce,但没有验证该 hook 被执行。如果生产代码未调用 session.releaseAgent,异常路径不会执行,测试仍可能通过。请补充 expect(session.releaseAgent).toHaveBeenCalled()

建议补充调用断言
     await Promise.allSettled(asyncTasks.splice(0, asyncTasks.length));

+    expect(session.releaseAgent).toHaveBeenCalled();
     expect(replayControl.state.unsubscribeCalls).toBe(1);

Also applies to: 1264-1264

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-client-abort-drain.test.ts` around lines
1253 - 1255, 在该测试中补充对 session.releaseAgent 的调用断言,确保异常处理路径确实执行了该 hook。使用现有的
session.releaseAgent mock,在触发生产代码后断言其已被调用。

const processingTask = getRegisteredTask("stream-processing");
expect(processingTask).toBeDefined();
await expect(processingTask).resolves.toBeUndefined();
await Promise.allSettled(asyncTasks.splice(0, asyncTasks.length));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

不要丢弃后台任务的 rejected 结果。

Promise.allSettled(...) 的结果未被检查。任一后台任务以 rejected 结束时,测试仍可能通过并隐藏回归。请改用 Promise.all(...),或断言每个结果的状态;如果某个拒绝是预期行为,请单独断言该结果。

建议检查后台任务结果
-    await Promise.allSettled(asyncTasks.splice(0, asyncTasks.length));
+    await Promise.all(asyncTasks.splice(0, asyncTasks.length));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await Promise.allSettled(asyncTasks.splice(0, asyncTasks.length));
await Promise.all(asyncTasks.splice(0, asyncTasks.length));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-client-abort-drain.test.ts` at line 1262,
Update the async task cleanup at the Promise.allSettled call to avoid discarding
rejected background-task results: use Promise.all so any unexpected rejection
fails the test, or explicitly inspect and assert every settled result,
separately documenting any intentionally expected rejection.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant