Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .scratch/batch-b/issues/03-transport-midstream-stall.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@
**Evidence:** `.scratch/batch-b/evidence.md#transport-mid-stream-stall`
**Branch:** `test/midstream-timeout`
**Blocked by:** None(02 已完成)
**Status:** ready-for-agent
**Status:** closed

- [ ] 用 fence 证明 timeout 前合法首帧已经交付给消费者
- [ ] 用 TestClock 越过下一帧间隔,随后得到现有 Transport/Timeout
- [ ] 不引入真实墙钟 sleep,不改变 timeout 默认值与错误词汇
- [ ] 完整 `transport-timeout.test.ts` 覆盖保持绿色
- [ ] 在 `packages/llm` 运行目标测试与 `bun typecheck`
- [x] 用 fence 证明 timeout 前合法首帧已经交付给消费者
- [x] 用 TestClock 越过下一帧间隔,随后得到现有 Transport/Timeout
- [x] 不引入真实墙钟 sleep,不改变 timeout 默认值与错误词汇
- [x] 完整 `transport-timeout.test.ts` 覆盖保持绿色
- [x] 在 `packages/llm` 运行目标测试与 `bun typecheck`

## 验证证据

- 基线:`dev@1a8635400f3f4b7c4985b06f0776e13d6f2e5e05`;分支:`test/midstream-timeout`。
- 实现提交:`1d5d08f76`;PR:[LeXwDeX/OpenCode-GraphAgent#194](https://github.com/LeXwDeX/OpenCode-GraphAgent/pull/194) → `dev`。
- 公开 seam:`LLMClient.stream(...)` 消费合法 SSE text delta;`Deferred` fence 只在 `text-delta` 的 `text === "Hello"` 已交付时解除,随后 `TestClock` 将 1000ms 帧间 timeout 推进到 2000ms。
- mutation 红灯:临时绕过 response stream 的 `Stream.timeoutOrElse` 后,首帧 fence 仍解除,但新增场景因 stream 未结束而 0 pass / 1 fail;mutation 已恢复,生产文件无 diff。
- `cd packages/llm && bun test test/transport-timeout.test.ts`:连续 3 次均为 8 pass、0 fail、16 expect;`bun typecheck`:`tsgo --noEmit`,exit 0。
- 现有生产实现满足 OpenSpec Requirement 3;无生产代码、timeout 默认值、错误词汇或 provider protocol 修改。
4 changes: 2 additions & 2 deletions .scratch/batch-b/issues/04-f3-subscription-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

**Evidence:** `.scratch/batch-b/evidence.md#f3--固定订阅-settle-sleep`
**Branch:** `test/goal-readiness`
**Blocked by:** 03(批次串行;代码写集独立)
**Status:** blocked
**Blocked by:** None(03 已完成;代码写集独立)
**Status:** ready-for-agent

- [ ] 先证明每个 sleep 等待的具体事件/状态,不用另一个超时数值替换 200ms
- [ ] 8 个固定 settle sleeps 全部删除或由同一确定性同步机制取代
Expand Down
46 changes: 44 additions & 2 deletions packages/llm/test/transport-timeout.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { Cause, Duration, Effect, Exit, Fiber, Option, Stream } from "effect"
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Option, Stream } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import * as TestClock from "effect/testing/TestClock"
import { LLM, LLMError, LLMEvent } from "../src"
Expand All @@ -9,7 +9,7 @@ import { LLMClient } from "../src/route"
import { testEffect } from "./lib/effect"
import { dynamicResponse, fixedResponse, runtimeLayer } from "./lib/http"
import { deltaChunk } from "./lib/openai-chunks"
import { sseEvents } from "./lib/sse"
import { sseEvents, sseRaw } from "./lib/sse"

const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })

Expand All @@ -35,6 +35,23 @@ const hangingBody = dynamicResponse((input) =>
),
)

const stalledAfterFirstFrame = dynamicResponse((input) =>
Effect.sync(() =>
input.respond(
new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
sseRaw(`data: ${JSON.stringify(deltaChunk({ role: "assistant", content: "Hello" }))}`),
),
)
},
}),
{ headers: { "content-type": "text/event-stream" } },
),
),
)

const expectTimeoutExit = (exit: Exit.Exit<readonly LLMEvent[], LLMError>) => {
if (Exit.isSuccess(exit)) {
throw new Error(`expected a Timeout failure, stream completed with ${exit.value.length} events`)
Expand Down Expand Up @@ -118,6 +135,31 @@ describe("http transport timeout", () => {
}),
)

testEffect(stalledAfterFirstFrame).effect(
"delivers the first frame before timing out the next inter-frame gap",
() =>
Effect.gen(function* () {
const firstFrameDelivered = yield* Deferred.make<void>()
const fiber = yield* LLMClient.stream(request(1000)).pipe(
Stream.tap((event) =>
LLMEvent.is.textDelta(event) && event.text === "Hello"
? Deferred.succeed(firstFrameDelivered, undefined)
: Effect.void,
),
Stream.runCollect,
Effect.forkChild,
)

yield* Deferred.await(firstFrameDelivered)
yield* TestClock.adjust(2000)
yield* Effect.yieldNow

const exit = fiber.pollUnsafe()
if (exit === undefined) throw new Error("expected the stalled stream to time out")
expectTimeoutExit(exit)
}),
)

testEffect(fixedResponse(sseEvents(deltaChunk({ role: "assistant", content: "Hello" })))).effect(
"completes normally when the stream finishes within the timeout",
() =>
Expand Down
Loading