@cnrai/pave 0.3.32 → 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 -33
  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 -2
  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,354 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for TUI attached mode compaction fix (Issue #255)
4
+ *
5
+ * Verifies:
6
+ * - STATUS_POLL_INTERVAL reduced to 2000ms
7
+ * - fetchHistory() called after session compaction detection
8
+ * - chatBox.setContent('') called before fetchHistory (clears old messages)
9
+ * - SSE reconnection still happens on compaction
10
+ *
11
+ * Designed to run standalone: node test/tui-compaction.test.js
12
+ */
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+
20
+ function assert(condition, msg) {
21
+ if (!condition) throw new Error('Assertion failed: ' + msg);
22
+ }
23
+
24
+ function runTest(name, fn) {
25
+ try {
26
+ fn();
27
+ console.log('\u2705 ' + name);
28
+ passed++;
29
+ } catch (e) {
30
+ console.log('\u274C ' + name + ': ' + e.message);
31
+ failed++;
32
+ }
33
+ }
34
+
35
+ // Read the attached.js source for inspection tests
36
+ const attachedPath = path.join(__dirname, '..', '..', 'tui', 'attached.js');
37
+ const attachedSource = fs.readFileSync(attachedPath, 'utf8');
38
+
39
+ // ============================================================
40
+ // 1. STATUS_POLL_INTERVAL
41
+ // ============================================================
42
+
43
+ runTest('STATUS_POLL_INTERVAL is 2000ms (not 5000ms)', () => {
44
+ assert(attachedSource.indexOf('STATUS_POLL_INTERVAL = 2000') !== -1,
45
+ 'should set STATUS_POLL_INTERVAL to 2000');
46
+ assert(attachedSource.indexOf('STATUS_POLL_INTERVAL = 5000') === -1,
47
+ 'should not have old 5000ms value');
48
+ });
49
+
50
+ runTest('STATUS_POLL_INTERVAL has explanatory comment', () => {
51
+ const idx = attachedSource.indexOf('STATUS_POLL_INTERVAL');
52
+ const section = attachedSource.substring(Math.max(0, idx - 100), idx + 50);
53
+ assert(section.indexOf('compaction') !== -1 || section.indexOf('responsive') !== -1,
54
+ 'should have a comment explaining why 2s was chosen');
55
+ });
56
+
57
+ // ============================================================
58
+ // 2. Compaction detection block
59
+ // ============================================================
60
+
61
+ runTest('compaction block detects session change', () => {
62
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
63
+ assert(idx !== -1, 'should compare sessionId to currentSessionId');
64
+ // The compaction marker should exist in the block
65
+ const section = attachedSource.substring(idx, idx + 2000);
66
+ assert(section.indexOf('Session compacted') !== -1,
67
+ 'should have Session compacted message');
68
+ });
69
+
70
+ runTest('compaction block disconnects then reconnects SSE', () => {
71
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
72
+ assert(idx !== -1, 'should find compaction block');
73
+ const section = attachedSource.substring(idx, idx + 2500);
74
+ assert(section.indexOf('sseConnection.destroy()') !== -1,
75
+ 'should destroy existing SSE connection');
76
+ assert(section.indexOf('connectSSE()') !== -1,
77
+ 'should call connectSSE() to reconnect');
78
+ });
79
+
80
+ runTest('compaction block clears chat before fetching history', () => {
81
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
82
+ assert(idx !== -1, 'should find compaction block');
83
+ const section = attachedSource.substring(idx, idx + 2000);
84
+
85
+ let clearIdx = section.indexOf("chatBox.setContent('')");
86
+ if (clearIdx === -1) clearIdx = section.indexOf('chatBox.setContent("")');
87
+ assert(clearIdx !== -1, 'should call chatBox.setContent("") to clear old messages');
88
+
89
+ const fetchIdx = section.indexOf('fetchHistory(');
90
+ assert(fetchIdx !== -1, 'should call fetchHistory to load new session content');
91
+
92
+ // setContent must come BEFORE fetchHistory
93
+ assert(clearIdx < fetchIdx,
94
+ 'chatBox.setContent should be called before fetchHistory()');
95
+ });
96
+
97
+ runTest('compaction block calls connectSSE inside fetchHistory callback', () => {
98
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
99
+ assert(idx !== -1, 'should find compaction block');
100
+ const section = attachedSource.substring(idx, idx + 2500);
101
+
102
+ // connectSSE should be inside the fetchHistory callback
103
+ const fetchIdx = section.indexOf('fetchHistory(');
104
+ const hasCallback = section.indexOf('fetchHistory(function') !== -1
105
+ || section.indexOf('fetchHistory((') !== -1
106
+ || section.indexOf('fetchHistory(() =>') !== -1;
107
+ const sseIdx = section.indexOf('connectSSE()', fetchIdx);
108
+ assert(hasCallback, 'should call fetchHistory with onComplete callback');
109
+ assert(sseIdx !== -1, 'should call connectSSE inside the callback');
110
+ assert(fetchIdx < sseIdx,
111
+ 'connectSSE should be inside fetchHistory callback to wait for history to load');
112
+ });
113
+
114
+ runTest('compaction block updates currentSessionId before SSE disconnect', () => {
115
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
116
+ assert(idx !== -1, 'should find compaction block');
117
+ const section = attachedSource.substring(idx, idx + 1000);
118
+
119
+ const updateIdx = section.indexOf('currentSessionId = newSessionId');
120
+ const destroyIdx = section.indexOf('sseConnection.destroy()');
121
+ assert(updateIdx !== -1, 'should update currentSessionId');
122
+ assert(destroyIdx !== -1, 'should destroy old SSE');
123
+ assert(updateIdx < destroyIdx,
124
+ 'currentSessionId should be updated before SSE disconnect');
125
+ });
126
+
127
+ runTest('compaction marker added inside fetchHistory callback (after history loads)', () => {
128
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
129
+ assert(idx !== -1, 'should find compaction block');
130
+ const section = attachedSource.substring(idx, idx + 2000);
131
+
132
+ // The marker should be inside the fetchHistory callback, not before it
133
+ const callbackIdx = section.indexOf('fetchHistory(');
134
+ const hasCallback = section.indexOf('fetchHistory(function') !== -1
135
+ || section.indexOf('fetchHistory((') !== -1
136
+ || section.indexOf('fetchHistory(() =>') !== -1;
137
+ assert(hasCallback, 'should have fetchHistory with callback');
138
+ const callbackSection = section.substring(callbackIdx);
139
+ assert(callbackSection.indexOf('Session compacted') !== -1,
140
+ 'compaction marker should be inside fetchHistory callback so it persists after history render');
141
+ });
142
+
143
+ runTest('compaction block resets streaming state', () => {
144
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
145
+ assert(idx !== -1, 'should find compaction block');
146
+ const section = attachedSource.substring(idx, idx + 2000);
147
+ assert(section.indexOf('streamingActive = false') !== -1,
148
+ 'should reset streamingActive to false');
149
+ assert(section.indexOf("streamingRawText = ''") !== -1,
150
+ 'should clear streamingRawText');
151
+ assert(section.indexOf("streamingContentStart = ''") !== -1,
152
+ 'should clear streamingContentStart');
153
+ });
154
+
155
+ runTest('compaction block resets streaming before clearing chat', () => {
156
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
157
+ assert(idx !== -1, 'should find compaction block');
158
+ const section = attachedSource.substring(idx, idx + 2000);
159
+ const resetIdx = section.indexOf('streamingActive = false');
160
+ let clearIdx = section.indexOf("chatBox.setContent('')");
161
+ if (clearIdx === -1) clearIdx = section.indexOf('chatBox.setContent("")');
162
+ assert(resetIdx !== -1 && clearIdx !== -1,
163
+ 'both streamingActive reset and chatBox clear should exist');
164
+ assert(resetIdx < clearIdx,
165
+ 'streaming state should be reset before clearing chat content');
166
+ });
167
+
168
+ // ============================================================
169
+ // 3. fetchHistory function exists and loads messages
170
+ // ============================================================
171
+
172
+ runTest('fetchHistory function exists with onComplete callback', () => {
173
+ assert(attachedSource.indexOf('function fetchHistory(onComplete)') !== -1,
174
+ 'should define fetchHistory with onComplete callback parameter');
175
+ });
176
+
177
+ runTest('fetchHistory fetches from session/message endpoint', () => {
178
+ const idx = attachedSource.indexOf('function fetchHistory(onComplete)');
179
+ assert(idx !== -1, 'should find fetchHistory');
180
+ const section = attachedSource.substring(idx, idx + 500);
181
+ assert(section.indexOf('/session/') !== -1 && section.indexOf('/message') !== -1,
182
+ 'should fetch from /session/<id>/message endpoint');
183
+ });
184
+
185
+ runTest('fetchHistory invokes onComplete callback on both paths', () => {
186
+ const idx = attachedSource.indexOf('function fetchHistory(onComplete)');
187
+ assert(idx !== -1, 'should find fetchHistory');
188
+ const section = attachedSource.substring(idx, idx + 2000);
189
+ let callCount = 0;
190
+ let searchIdx = 0;
191
+ while (true) {
192
+ const found = section.indexOf('onComplete', searchIdx);
193
+ if (found === -1) break;
194
+ callCount++;
195
+ searchIdx = found + 10;
196
+ }
197
+ // onComplete should appear at least 3 times: param, error path, success path
198
+ assert(callCount >= 3,
199
+ 'onComplete should appear in param + error path + success path (found ' + callCount + ')');
200
+ });
201
+
202
+ runTest('fetchHistory clears fetching message and renders history', () => {
203
+ const idx = attachedSource.indexOf('function fetchHistory(onComplete)');
204
+ assert(idx !== -1, 'should find fetchHistory');
205
+ const section = attachedSource.substring(idx, idx + 2000);
206
+ assert(section.indexOf("chatBox.setContent('')") !== -1 || section.indexOf('chatBox.setContent("")') !== -1,
207
+ 'should clear chat before rendering messages');
208
+ });
209
+
210
+ // ============================================================
211
+ // 4. serverUrl change does NOT call fetchHistory (only compaction does)
212
+ // ============================================================
213
+
214
+ runTest('serverUrl change block does not call fetchHistory', () => {
215
+ // Find the serverUrl change block
216
+ const idx = attachedSource.indexOf('st.serverUrl && st.serverUrl !== currentServerUrl');
217
+ assert(idx !== -1, 'should find serverUrl change detection');
218
+ // Get section until next closing brace block
219
+ const section = attachedSource.substring(idx, idx + 300);
220
+ // Check for any form of fetchHistory call (with or without args)
221
+ const fetchIdx = section.indexOf('fetchHistory(');
222
+ // fetchHistory should NOT be in the serverUrl block
223
+ assert(fetchIdx === -1,
224
+ 'serverUrl change should not call fetchHistory (only compaction triggers history reload)');
225
+ });
226
+
227
+ // ============================================================
228
+ // 5. Comment quality
229
+ // ============================================================
230
+
231
+ runTest('compaction block has explanatory comments about SSE timing', () => {
232
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
233
+ assert(idx !== -1, 'should find compaction block');
234
+ const section = attachedSource.substring(idx, idx + 2000);
235
+ assert(section.indexOf('SSE') !== -1,
236
+ 'should have comment mentioning SSE reconnection timing');
237
+ assert(section.indexOf('history') !== -1 || section.indexOf('fetchHistory') !== -1,
238
+ 'should mention history loading in comments');
239
+ });
240
+
241
+ // ============================================================
242
+ // 6. Reconnect timer cancellation on compaction
243
+ // ============================================================
244
+
245
+ runTest('reconnectTimer variable declared', () => {
246
+ assert(attachedSource.indexOf('let reconnectTimer = null') !== -1,
247
+ 'should declare reconnectTimer initialized to null');
248
+ });
249
+
250
+ runTest('scheduleReconnect stores timer handle', () => {
251
+ const idx = attachedSource.indexOf('function scheduleReconnect()');
252
+ assert(idx !== -1, 'should find scheduleReconnect function');
253
+ const section = attachedSource.substring(idx, idx + 500);
254
+ assert(section.indexOf('reconnectTimer = setTimeout') !== -1,
255
+ 'should store setTimeout handle in reconnectTimer');
256
+ });
257
+
258
+ runTest('scheduleReconnect clears timer handle on fire', () => {
259
+ const idx = attachedSource.indexOf('function scheduleReconnect()');
260
+ assert(idx !== -1, 'should find scheduleReconnect function');
261
+ const section = attachedSource.substring(idx, idx + 500);
262
+ assert(section.indexOf('reconnectTimer = null') !== -1,
263
+ 'should set reconnectTimer to null when timer fires');
264
+ });
265
+
266
+ runTest('compaction cancels pending reconnect timer', () => {
267
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
268
+ assert(idx !== -1, 'should find compaction block');
269
+ const section = attachedSource.substring(idx, idx + 800);
270
+ assert(section.indexOf('clearTimeout(reconnectTimer)') !== -1,
271
+ 'should clearTimeout reconnectTimer in compaction block');
272
+ assert(section.indexOf('reconnecting = false') !== -1,
273
+ 'should reset reconnecting flag when cancelling timer');
274
+ });
275
+
276
+ runTest('compaction timer cancel comes before SSE disconnect', () => {
277
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
278
+ assert(idx !== -1, 'should find compaction block');
279
+ const section = attachedSource.substring(idx, idx + 1000);
280
+ const cancelIdx = section.indexOf('clearTimeout(reconnectTimer)');
281
+ const destroyIdx = section.indexOf('sseConnection.destroy()');
282
+ assert(cancelIdx !== -1 && destroyIdx !== -1,
283
+ 'both timer cancel and SSE destroy should exist');
284
+ assert(cancelIdx < destroyIdx,
285
+ 'timer cancel should happen before SSE disconnect');
286
+ });
287
+
288
+ // ============================================================
289
+ // 7. Race-condition guard: captured newSessionId + stale callback skip
290
+ // ============================================================
291
+
292
+ runTest('compaction captures newSessionId before async fetch', () => {
293
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
294
+ assert(idx !== -1, 'should find compaction block');
295
+ const section = attachedSource.substring(idx, idx + 1600);
296
+ assert(section.indexOf('const newSessionId = st.sessionId') !== -1
297
+ || section.indexOf('let newSessionId = st.sessionId') !== -1
298
+ || section.indexOf('var newSessionId = st.sessionId') !== -1,
299
+ 'should capture st.sessionId into local newSessionId variable');
300
+ const captureIdx = section.indexOf('newSessionId = st.sessionId');
301
+ const fetchIdx = section.indexOf('fetchHistory(');
302
+ assert(captureIdx < fetchIdx,
303
+ 'newSessionId should be captured before fetchHistory is called');
304
+ });
305
+
306
+ runTest('fetchHistory callback guards against stale session', () => {
307
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
308
+ assert(idx !== -1, 'should find compaction block');
309
+ const section = attachedSource.substring(idx, idx + 1800);
310
+ assert(section.indexOf('currentSessionId !== newSessionId') !== -1,
311
+ 'callback should check if currentSessionId still matches newSessionId');
312
+ assert(section.indexOf('return') !== -1,
313
+ 'should return early if session has moved on');
314
+ });
315
+
316
+ runTest('compaction marker uses captured newSessionId (not st.sessionId)', () => {
317
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
318
+ assert(idx !== -1, 'should find compaction block');
319
+ const section = attachedSource.substring(idx, idx + 2200);
320
+ // The marker text should use newSessionId, not st.sessionId
321
+ const markerIdx = section.indexOf('newSessionId.substring(0, 8)');
322
+ assert(markerIdx !== -1,
323
+ 'compaction marker should use captured newSessionId, not st.sessionId');
324
+ });
325
+
326
+ // ============================================================
327
+ // 8. Early return prevents fall-through to serverUrl block
328
+ // ============================================================
329
+
330
+ runTest('compaction block returns early (no fall-through to serverUrl)', () => {
331
+ const idx = attachedSource.indexOf('st.sessionId !== currentSessionId');
332
+ assert(idx !== -1, 'should find compaction block');
333
+ // Find the end of the compaction if-block
334
+ const section = attachedSource.substring(idx, idx + 2800);
335
+ // The block should end with 'return;' before the serverUrl check
336
+ const returnIdx = section.indexOf('return;');
337
+ const serverUrlIdx = section.indexOf('st.serverUrl && st.serverUrl !== currentServerUrl');
338
+ assert(returnIdx !== -1, 'should have return statement');
339
+ assert(serverUrlIdx !== -1, 'should find serverUrl block');
340
+ assert(returnIdx < serverUrlIdx,
341
+ 'compaction block should return before serverUrl check to prevent concurrent SSE reconnects');
342
+ });
343
+
344
+ // ============================================================
345
+ // Done
346
+ // ============================================================
347
+
348
+ console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
349
+ if (failed > 0) {
350
+ console.log(failed + ' test(s) FAILED');
351
+ process.exitCode = 1;
352
+ } else {
353
+ console.log('All tests passed!');
354
+ }