@cnrai/pave 0.3.33 → 0.3.34

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.
Files changed (83) hide show
  1. package/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -32
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -3
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. package/sandbox/utils/yaml.js +0 -1
@@ -0,0 +1,445 @@
1
+ // Tests for Issue #249: Ctrl+U abort signal wiring to LLM stream
2
+ // Verifies that abort signal is properly passed through the call chain
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const assert = require('assert');
7
+
8
+ let passed = 0;
9
+ let failed = 0;
10
+
11
+ function runTest(name, fn) {
12
+ try {
13
+ fn();
14
+ console.log('\u2705 ' + name);
15
+ passed++;
16
+ } catch (e) {
17
+ console.log('\u274C ' + name + ': ' + e.message);
18
+ failed++;
19
+ }
20
+ }
21
+
22
+ // ============================================================
23
+ // Source inspection tests: verify signal wiring is in place
24
+ // ============================================================
25
+
26
+ const handlerSource = fs.readFileSync(
27
+ path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'prompt', 'handler.js'), 'utf8');
28
+ const providerSource = fs.readFileSync(
29
+ path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'provider', 'index.js'), 'utf8');
30
+
31
+ // --- handler.js tests ---
32
+
33
+ runTest('handler.js: passes signal to streamResponse()', () => {
34
+ // Find the streamResponse call and verify it includes signal
35
+ const streamCallIdx = handlerSource.indexOf('response = await streamResponse({');
36
+ assert(streamCallIdx !== -1, 'should find streamResponse call');
37
+ const callSection = handlerSource.substring(streamCallIdx, streamCallIdx + 500);
38
+ // Match shorthand `signal,` or explicit `signal: signal`
39
+ assert(callSection.indexOf('signal') !== -1,
40
+ 'streamResponse() call should include signal parameter');
41
+ });
42
+
43
+ runTest('handler.js: breaks on aborted finish_reason', () => {
44
+ const pattern = /finishReason\s*===\s*["']aborted["']/;
45
+ assert(pattern.test(handlerSource),
46
+ 'should check for "aborted" finish reason and break the iteration loop');
47
+ assert(handlerSource.indexOf('LLM stream was aborted') !== -1,
48
+ 'should log when stream is aborted');
49
+ });
50
+
51
+ runTest('handler.js: checks signal in executeOneTool', () => {
52
+ // Find executeOneTool function
53
+ const fnIdx = handlerSource.indexOf('async function executeOneTool');
54
+ assert(fnIdx !== -1, 'should find executeOneTool function');
55
+ const fnSection = handlerSource.substring(fnIdx, fnIdx + 300);
56
+ assert(fnSection.indexOf('signal && signal.aborted') !== -1,
57
+ 'executeOneTool should check signal.aborted before executing');
58
+ assert(fnSection.indexOf('Tool execution skipped') !== -1,
59
+ 'should return skip message when aborted');
60
+ });
61
+
62
+ // --- provider/index.js tests ---
63
+
64
+ runTest('provider: processStreamingResponse accepts signal parameter', () => {
65
+ const pattern = /function\s+processStreamingResponse\s*\([^)]*signal[^)]*\)/;
66
+ assert(pattern.test(providerSource),
67
+ 'processStreamingResponse should accept signal as a parameter');
68
+ });
69
+
70
+ runTest('provider: processStreamingResponse checks signal.aborted before starting', () => {
71
+ // Find processStreamingResponse function
72
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
73
+ assert(fnIdx !== -1, 'should find processStreamingResponse');
74
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 2500);
75
+ assert(fnSection.indexOf("signal && signal.aborted") !== -1,
76
+ 'should check signal.aborted early');
77
+ assert(fnSection.indexOf("finish_reason: 'aborted'") !== -1,
78
+ 'should return aborted finish_reason when signal is aborted');
79
+ });
80
+
81
+ runTest('provider: processStreamingResponse listens for abort event', () => {
82
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
83
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 3000);
84
+ assert(fnSection.indexOf("signal.addEventListener('abort'") !== -1,
85
+ 'should add event listener for abort signal');
86
+ assert(fnSection.indexOf("signal.removeEventListener('abort'") !== -1,
87
+ 'should clean up event listener to prevent memory leaks');
88
+ });
89
+
90
+ runTest('provider: processStreamingResponse destroys reader on abort', () => {
91
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
92
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 3000);
93
+ // The onAbort handler should destroy the reader
94
+ const abortHandlerIdx = fnSection.indexOf('onAbort = function ()');
95
+ assert(abortHandlerIdx !== -1, 'should have onAbort handler');
96
+ const abortSection = fnSection.substring(abortHandlerIdx, abortHandlerIdx + 500);
97
+ assert(abortSection.indexOf('reader.destroy()') !== -1,
98
+ 'abort handler should destroy the reader stream');
99
+ });
100
+
101
+ runTest('provider: streamGitHubCopilotResponseInternal early abort return', () => {
102
+ const fnIdx = providerSource.indexOf('function streamGitHubCopilotResponseInternal');
103
+ assert(fnIdx !== -1, 'should find streamGitHubCopilotResponseInternal');
104
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 400);
105
+ assert(fnSection.indexOf('opts.signal && opts.signal.aborted') !== -1,
106
+ 'should check for already-aborted signal before making fetch call');
107
+ assert(fnSection.indexOf("finish_reason: 'aborted'") !== -1,
108
+ 'should return aborted result when signal is already aborted');
109
+ });
110
+
111
+ runTest('provider: fetch() receives signal option', () => {
112
+ // Find the fetch call in streamGitHubCopilotResponseInternal
113
+ const fetchIdx = providerSource.indexOf("return fetch(provider.baseUrl");
114
+ assert(fetchIdx !== -1, 'should find fetch call');
115
+ const fetchSection = providerSource.substring(fetchIdx, fetchIdx + 500);
116
+ assert(fetchSection.indexOf('signal: opts.signal') !== -1,
117
+ 'fetch() should receive signal from opts to cancel HTTP request');
118
+ });
119
+
120
+ runTest('provider: fetch AbortError is caught gracefully', () => {
121
+ // The .catch() handler should handle AbortError
122
+ const catchIdx = providerSource.indexOf("err.name === 'AbortError'");
123
+ assert(catchIdx !== -1, 'should catch AbortError from fetch()');
124
+ const catchSection = providerSource.substring(catchIdx - 50, catchIdx + 300);
125
+ assert(catchSection.indexOf("finish_reason: 'aborted'") !== -1,
126
+ 'catch handler should return aborted result instead of throwing');
127
+ });
128
+
129
+ runTest('provider: processStreamingResponse passes signal to fetch result handler', () => {
130
+ // Verify the call to processStreamingResponse includes the signal
131
+ const callPattern = /processStreamingResponse\s*\(\s*response\s*,\s*opts\.onTextChunk\s*,\s*opts\.signal\s*\)/;
132
+ assert(callPattern.test(providerSource),
133
+ 'processStreamingResponse call should pass opts.signal as third argument');
134
+ });
135
+
136
+ // ============================================================
137
+ // Functional tests with mock AbortController
138
+ // ============================================================
139
+
140
+ runTest('functional: AbortController signal.aborted starts false', () => {
141
+ // Verify AbortController works in this environment
142
+ if (typeof AbortController === 'undefined') {
143
+ console.log(' (AbortController not available, skipping)');
144
+ return;
145
+ }
146
+ const ac = new AbortController();
147
+ assert(ac.signal.aborted === false, 'signal should start not aborted');
148
+ ac.abort();
149
+ assert(ac.signal.aborted === true, 'signal should be aborted after abort()');
150
+ });
151
+
152
+ runTest('functional: abort event fires on AbortController.abort()', () => {
153
+ if (typeof AbortController === 'undefined') {
154
+ console.log(' (AbortController not available, skipping)');
155
+ return;
156
+ }
157
+ const ac = new AbortController();
158
+ let fired = false;
159
+ ac.signal.addEventListener('abort', () => { fired = true; });
160
+ ac.abort();
161
+ assert(fired === true, 'abort event should fire synchronously');
162
+ });
163
+
164
+ // ============================================================
165
+ // Copilot review round 1: resource cleanup tests
166
+ // ============================================================
167
+
168
+ runTest('provider: early-abort path destroys response body stream', () => {
169
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
170
+ assert(fnIdx !== -1, 'should find processStreamingResponse');
171
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 2000);
172
+ // Find the early abort section (signal && signal.aborted)
173
+ const earlyAbortIdx = fnSection.indexOf('signal && signal.aborted');
174
+ assert(earlyAbortIdx !== -1, 'should find early abort check');
175
+ const earlyAbortSection = fnSection.substring(earlyAbortIdx, earlyAbortIdx + 300);
176
+ // Must destroy reader before resolving to release HTTP connection
177
+ assert(earlyAbortSection.indexOf('reader.destroy()') !== -1,
178
+ 'early-abort path should destroy response body stream to release socket');
179
+ });
180
+
181
+ runTest('provider: onAbort handler cleans up signal listener before resolving', () => {
182
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
183
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 3000);
184
+ const onAbortIdx = fnSection.indexOf('onAbort = function ()');
185
+ assert(onAbortIdx !== -1, 'should find onAbort handler');
186
+ const onAbortSection = fnSection.substring(onAbortIdx, onAbortIdx + 400);
187
+ // cleanupSignalListener must be called inside onAbort, before resolve
188
+ assert(onAbortSection.indexOf('cleanupSignalListener()') !== -1,
189
+ 'onAbort handler should call cleanupSignalListener() to prevent leak');
190
+ // Verify cleanup happens before resolve
191
+ const cleanupIdx = onAbortSection.indexOf('cleanupSignalListener()');
192
+ const resolveIdx = onAbortSection.indexOf('resolve(');
193
+ assert(cleanupIdx < resolveIdx,
194
+ 'cleanupSignalListener should be called before resolve in onAbort');
195
+ });
196
+
197
+ runTest('provider: close event also cleans up signal listener', () => {
198
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
199
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 12000);
200
+ // reader.destroy() emits 'close' not 'end', so we need a close handler
201
+ assert(fnSection.indexOf("reader.on('close'") !== -1,
202
+ 'should listen for close event on reader');
203
+ // The close handler should call cleanupSignalListener
204
+ const closeIdx = fnSection.indexOf("reader.on('close'");
205
+ const closeSection = fnSection.substring(closeIdx, closeIdx + 200);
206
+ assert(closeSection.indexOf('cleanupSignalListener()') !== -1,
207
+ 'close event handler should clean up signal listener');
208
+ });
209
+
210
+ // ============================================================
211
+ // Integration: verify signal flow from handler to provider
212
+ // ============================================================
213
+
214
+ runTest('integration: handler.js signal flows to streamResponse then to fetch', () => {
215
+ // Trace the signal through the call chain:
216
+ // 1. handler.js: signal -> streamResponse({...signal})
217
+ // 2. provider/index.js: streamResponse -> streamGitHubCopilotResponse(opts) -> streamGitHubCopilotResponseInternal(opts)
218
+ // 3. streamGitHubCopilotResponseInternal: opts.signal -> fetch({signal}) AND processStreamingResponse(response, cb, signal)
219
+
220
+ // Check streamResponse passes opts through to provider
221
+ const streamResponseIdx = providerSource.indexOf('function streamResponse(opts)');
222
+ assert(streamResponseIdx !== -1, 'should find streamResponse function');
223
+ const streamResponseSection = providerSource.substring(streamResponseIdx, streamResponseIdx + 300);
224
+ // streamResponse calls streamGitHubCopilotResponse(opts) which calls streamGitHubCopilotResponseInternal(opts)
225
+ // so signal in opts is preserved through the chain
226
+ assert(streamResponseSection.indexOf('streamGitHubCopilotResponse(opts)') !== -1,
227
+ 'streamResponse should pass full opts to provider function');
228
+ });
229
+
230
+ // ============================================================
231
+ // Copilot review round 2: definition order + runtime tests
232
+ // ============================================================
233
+
234
+ runTest('provider: cleanupSignalListener defined before onAbort references it', () => {
235
+ const fnIdx = providerSource.indexOf('async function processStreamingResponse');
236
+ const fnSection = providerSource.substring(fnIdx, fnIdx + 3000);
237
+ const cleanupDefIdx = fnSection.indexOf('const cleanupSignalListener = function ()');
238
+ const onAbortDefIdx = fnSection.indexOf('onAbort = function ()');
239
+ assert(cleanupDefIdx !== -1, 'should find cleanupSignalListener definition');
240
+ assert(onAbortDefIdx !== -1, 'should find onAbort definition');
241
+ assert(cleanupDefIdx < onAbortDefIdx,
242
+ 'cleanupSignalListener must be defined before onAbort to avoid calling undefined');
243
+ });
244
+
245
+ runTest('runtime: AbortController signal + listener lifecycle simulation', () => {
246
+ if (typeof AbortController === 'undefined') {
247
+ console.log(' (AbortController not available, skipping)');
248
+ return;
249
+ }
250
+
251
+ // We need to test processStreamingResponse directly
252
+ // It's not exported, so we'll test the behavior by requiring the module
253
+ // and testing through the exported streamResponse, but that requires
254
+ // full provider setup. Instead, let's create a mock test:
255
+
256
+ const Readable = require('stream').Readable;
257
+ const ac = new AbortController();
258
+
259
+ // Create a mock readable stream that emits SSE data
260
+ const stream = new Readable({
261
+ read() {}, // no-op, we push manually
262
+ });
263
+
264
+ // Simulate a response object with body
265
+ const _mockResponse = { body: stream };
266
+
267
+ // Track events
268
+ let destroyed = false; // eslint-disable-line no-unused-vars
269
+ const originalDestroy = stream.destroy.bind(stream);
270
+ stream.destroy = function () {
271
+ destroyed = true;
272
+ originalDestroy();
273
+ };
274
+
275
+ // Load processStreamingResponse by extracting it from the module
276
+ // Since it's not exported, we verify the behavior through the source
277
+ // inspection tests above and test AbortController mechanics here
278
+
279
+ // Test: abort fires synchronously and the signal state changes
280
+ assert(ac.signal.aborted === false, 'signal starts not aborted');
281
+
282
+ let listenerCalled = false;
283
+ let listenerRemoved = false;
284
+
285
+ // Simulate what processStreamingResponse does:
286
+ const onAbort = function () { listenerCalled = true; };
287
+ const cleanupSignalListener = function () {
288
+ ac.signal.removeEventListener('abort', onAbort);
289
+ listenerRemoved = true;
290
+ };
291
+ ac.signal.addEventListener('abort', onAbort);
292
+
293
+ // Abort should fire listener synchronously
294
+ ac.abort();
295
+ assert(listenerCalled === true, 'abort listener should fire');
296
+ assert(ac.signal.aborted === true, 'signal should be aborted');
297
+
298
+ // Cleanup should work
299
+ cleanupSignalListener();
300
+ assert(listenerRemoved === true, 'cleanup should work after abort');
301
+
302
+ // Verify listener was actually removed (second abort doesn't re-fire)
303
+ listenerCalled = false;
304
+ // Can't abort twice, but we can verify removeEventListener was called
305
+ });
306
+
307
+ runTest('runtime: pre-aborted signal returns immediately', () => {
308
+ if (typeof AbortController === 'undefined') {
309
+ console.log(' (AbortController not available, skipping)');
310
+ return;
311
+ }
312
+
313
+ const ac = new AbortController();
314
+ ac.abort(); // Pre-abort
315
+
316
+ // Simulate the early-abort check from processStreamingResponse
317
+ let result = null;
318
+ if (ac.signal.aborted) {
319
+ result = { content: [], tool_calls: [], finish_reason: 'aborted' };
320
+ }
321
+
322
+ assert(result !== null, 'should produce result immediately');
323
+ assert(result.finish_reason === 'aborted', 'finish_reason should be aborted');
324
+ assert(Array.isArray(result.content), 'content should be array');
325
+ assert(result.content.length === 0, 'content should be empty for pre-aborted');
326
+ });
327
+
328
+ runTest('runtime: mock stream destroy + abort lifecycle', () => {
329
+ if (typeof AbortController === 'undefined') {
330
+ console.log(' (AbortController not available, skipping)');
331
+ return;
332
+ }
333
+
334
+ const Readable = require('stream').Readable;
335
+ const ac = new AbortController();
336
+
337
+ // Create mock stream
338
+ const stream = new Readable({ read() {} });
339
+ let destroyCalled = false;
340
+ const origDestroy = stream.destroy.bind(stream);
341
+ stream.destroy = function () { destroyCalled = true; origDestroy(); };
342
+
343
+ // Simulate the full abort handler lifecycle
344
+ let aborted = false;
345
+ const fullContent = 'partial content here';
346
+ let resolvedResult = null;
347
+
348
+ let onAbort = null;
349
+ const cleanupSignalListener = function () {
350
+ ac.signal.removeEventListener('abort', onAbort);
351
+ };
352
+
353
+ onAbort = function () {
354
+ if (aborted) return;
355
+ aborted = true;
356
+ cleanupSignalListener();
357
+ stream.destroy();
358
+ resolvedResult = {
359
+ content: fullContent ? [{ type: 'text', text: fullContent }] : [],
360
+ tool_calls: [],
361
+ finish_reason: 'aborted',
362
+ };
363
+ };
364
+ ac.signal.addEventListener('abort', onAbort);
365
+
366
+ // Push some data (simulating partial stream)
367
+ stream.push('data: {"choices":[{"delta":{"content":"hello"}}]}\n\n');
368
+
369
+ // Abort mid-stream
370
+ ac.abort();
371
+
372
+ assert(destroyCalled === true, 'stream should be destroyed on abort');
373
+ assert(aborted === true, 'aborted flag should be set');
374
+ assert(resolvedResult !== null, 'should have resolved result');
375
+ assert(resolvedResult.finish_reason === 'aborted', 'finish_reason should be aborted');
376
+ assert(resolvedResult.content[0].text === 'partial content here',
377
+ 'should preserve partial content accumulated before abort');
378
+ });
379
+
380
+ // ============================================================
381
+ // Copilot review round 3 tests
382
+ // ============================================================
383
+
384
+ runTest('handler.js: aborted path persists partial text before breaking', () => {
385
+ const abortIdx = handlerSource.indexOf("finishReason === \"aborted\"");
386
+ assert(abortIdx !== -1, 'should find aborted check');
387
+ const abortSection = handlerSource.substring(abortIdx, abortIdx + 1000);
388
+ // Should extract text from response.content before breaking
389
+ assert(abortSection.indexOf('response.content') !== -1,
390
+ 'aborted path should access response.content to extract partial text');
391
+ assert(abortSection.indexOf('partManager.add') !== -1,
392
+ 'aborted path should persist partial text via partManager.add');
393
+ // break should come after persisting
394
+ const addIdx = abortSection.indexOf('partManager.add');
395
+ const breakIdx = abortSection.indexOf('break', addIdx);
396
+ assert(addIdx !== -1 && breakIdx !== -1 && addIdx < breakIdx,
397
+ 'partManager.add should happen before break');
398
+ });
399
+
400
+ runTest('handler.js: aborted path emits message.part.updated event', () => {
401
+ const abortIdx = handlerSource.indexOf("finishReason === \"aborted\"");
402
+ assert(abortIdx !== -1, 'should find aborted check');
403
+ const abortSection = handlerSource.substring(abortIdx, abortIdx + 900);
404
+ // After partManager.add, the abort path should emit onEvent so SSE clients reconcile
405
+ assert(abortSection.indexOf('message.part.updated') !== -1,
406
+ 'aborted path should emit message.part.updated event');
407
+ assert(abortSection.indexOf('onEvent') !== -1,
408
+ 'aborted path should call onEvent to notify SSE clients');
409
+ // Verify the pattern: partManager.add stores to abortedPart, then emits it
410
+ assert(abortSection.indexOf('abortedPart') !== -1,
411
+ 'should capture returned part from partManager.add as abortedPart');
412
+ const partAddIdx = abortSection.indexOf('partManager.add');
413
+ const eventIdx = abortSection.indexOf('message.part.updated');
414
+ assert(partAddIdx < eventIdx,
415
+ 'partManager.add should come before onEvent emission');
416
+ });
417
+
418
+ runTest('provider: log messages use generic AbortSignal wording', () => {
419
+ // Should NOT contain "Ctrl+U" in log messages  provider layer is UI-agnostic
420
+ const abortLogIdx = providerSource.indexOf('Stream aborted via AbortSignal');
421
+ assert(abortLogIdx !== -1, 'should have generic abort log message');
422
+ const fetchAbortIdx = providerSource.indexOf('Fetch aborted via AbortSignal');
423
+ assert(fetchAbortIdx !== -1, 'should have generic fetch abort log message');
424
+ // Verify no Ctrl+U references remain in provider layer
425
+ const ctrlUCount = (providerSource.match(/Ctrl\+U/g) || []).length;
426
+ assert(ctrlUCount === 0, 'provider layer should not reference Ctrl+U (found ' + ctrlUCount + ')');
427
+ });
428
+
429
+ runTest('handler.js: abort comment and log are UI-agnostic', () => {
430
+ // The abort comment and log should not reference any specific keybinding
431
+ const ctrlUCount = (handlerSource.match(/Ctrl\+U/g) || []).length;
432
+ assert(ctrlUCount === 0, 'handler.js should not reference Ctrl+U (found ' + ctrlUCount + ')');
433
+ // Should use generic wording mentioning AbortSignal or request abort
434
+ assert(handlerSource.indexOf('via AbortSignal/request abort') !== -1,
435
+ 'abort comment should mention AbortSignal/request abort');
436
+ assert(handlerSource.indexOf('aborted via request abort') !== -1,
437
+ 'abort log should use generic request abort wording');
438
+ });
439
+
440
+ // ============================================================
441
+ // Summary
442
+ // ============================================================
443
+
444
+ console.log('\nTotal: ' + (passed + failed) + ', Passed: ' + passed + ', Failed: ' + failed);
445
+ if (failed > 0) process.exitCode = 1;