@nathapp/nax 0.22.1 → 0.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112,17 +112,15 @@ afterEach(() => {
112
112
  resetLogger();
113
113
  });
114
114
 
115
- describe("BUG-039: callLlmOnce stream drain on timeout", () => {
116
- test("cancels stdout and stderr before proc.kill() on timeout", async () => {
117
- const { proc, stdoutCancelled, stderrCancelled, killCalled, killCalledAfterCancel } = makeHangingProc();
115
+ describe("BUG-039/BUG-040: stream cleanup on timeout", () => {
116
+ test("kills process on timeout without calling cancel() on locked streams", async () => {
117
+ const { proc, stdoutCancelled, stderrCancelled, killCalled } = makeHangingProc();
118
118
 
119
119
  const originalSpawn = _deps.spawn;
120
120
  _deps.spawn = mock(() => proc as PipedProc);
121
121
 
122
122
  const config = makeConfig({ timeoutMs: 30 });
123
123
 
124
- // Import callLlmOnce indirectly through llmStrategy to trigger the private function.
125
- // We test via the exported llmStrategy.route() which calls callLlm → callLlmOnce.
126
124
  const { llmStrategy } = await import("../../../../src/routing/strategies/llm");
127
125
 
128
126
  const story = {
@@ -147,15 +145,72 @@ describe("BUG-039: callLlmOnce stream drain on timeout", () => {
147
145
  // Should resolve promptly — within 500ms of the 30ms timeout
148
146
  expect(elapsed).toBeLessThan(500);
149
147
 
150
- expect(stdoutCancelled.value).toBe(true);
151
- expect(stderrCancelled.value).toBe(true);
148
+ // BUG-040: cancel() must NOT be called on locked streams — it returns a rejected
149
+ // Promise (per Web Streams spec) which becomes an unhandled rejection crash.
150
+ expect(stdoutCancelled.value).toBe(false);
151
+ expect(stderrCancelled.value).toBe(false);
152
152
  expect(killCalled.value).toBe(true);
153
- // kill() was called after both streams were cancelled
154
- expect(killCalledAfterCancel.value).toBe(true);
155
153
 
156
154
  _deps.spawn = originalSpawn;
157
155
  });
158
156
 
157
+ test("no unhandled rejection when Response.text() locks streams and proc is killed", async () => {
158
+ // Simulate the exact BUG-040 scenario:
159
+ // 1. Spawn proc with piped streams
160
+ // 2. Response(proc.stdout).text() locks the streams
161
+ // 3. Timeout fires, proc.kill() called
162
+ // 4. No unhandled rejection should occur
163
+
164
+ const unhandledRejections: Error[] = [];
165
+ const handler = (event: PromiseRejectionEvent) => {
166
+ unhandledRejections.push(event.reason as Error);
167
+ event.preventDefault();
168
+ };
169
+
170
+ // biome-ignore lint/suspicious/noGlobalAssign: test-only override
171
+ globalThis.addEventListener("unhandledrejection", handler);
172
+
173
+ // Create a proc where streams are locked by Response readers
174
+ const stdout = new ReadableStream({ start() {} });
175
+ const stderr = new ReadableStream({ start() {} });
176
+ const proc = {
177
+ stdout,
178
+ stderr,
179
+ exited: new Promise<number>(() => {}),
180
+ kill: mock(() => {}),
181
+ };
182
+
183
+ const originalSpawn = _deps.spawn;
184
+ _deps.spawn = mock(() => proc as PipedProc);
185
+
186
+ const config = makeConfig({ timeoutMs: 20, retries: 0 });
187
+
188
+ const { llmStrategy } = await import("../../../../src/routing/strategies/llm");
189
+ const story = {
190
+ id: "BUG040",
191
+ title: "Bug test",
192
+ description: "Test",
193
+ acceptanceCriteria: ["AC1"],
194
+ tags: [],
195
+ dependencies: [],
196
+ status: "pending" as const,
197
+ passes: false,
198
+ escalations: [],
199
+ attempts: 0,
200
+ };
201
+
202
+ await expect(llmStrategy.route(story, { config })).rejects.toThrow(/timeout/i);
203
+
204
+ // Give microtasks time to settle
205
+ await Bun.sleep(50);
206
+
207
+ globalThis.removeEventListener("unhandledrejection", handler);
208
+ _deps.spawn = originalSpawn;
209
+
210
+ // No unhandled rejections should have occurred
211
+ expect(unhandledRejections).toHaveLength(0);
212
+ });
213
+
159
214
  test("clearTimeout is called on success path (no resource leak)", async () => {
160
215
  const originalSpawn = _deps.spawn;
161
216