@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,349 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for agent log file redirection and session file location (Issue #99).
4
+ *
5
+ * Verifies that:
6
+ * 1. Agent command writes pave.log to --config directory (not silencing console)
7
+ * 2. Agent command writes debug.log when DEBUG=1
8
+ * 3. Agent session file is stored in cwd as .pave-session.json (not config dir, see #108)
9
+ * 4. Log directory defaults to $(cwd)/.pave when --config is not specified
10
+ *
11
+ * Runs under `npm test` via test/run-tests.js
12
+ */
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+
18
+ let passed = 0;
19
+ let failed = 0;
20
+
21
+ function runTest(name, testFn) {
22
+ try {
23
+ testFn();
24
+ passed++;
25
+ console.log(`✅ ${name}`);
26
+ } catch (error) {
27
+ failed++;
28
+ console.log(`❌ ${name}: ${error.message}`);
29
+ }
30
+ }
31
+
32
+ function assertEqual(actual, expected, message) {
33
+ if (actual !== expected) {
34
+ throw new Error(`${message || 'Assertion'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
35
+ }
36
+ }
37
+
38
+ function assertContains(str, substr, message) {
39
+ if (typeof str !== 'string' || str.indexOf(substr) < 0) {
40
+ throw new Error(`${message || 'Assertion'}: expected ${JSON.stringify(str)} to contain ${JSON.stringify(substr)}`);
41
+ }
42
+ }
43
+
44
+ console.log('\n=== Agent Log Files Tests (Issue #99) ===\n');
45
+
46
+ // ============================================================================
47
+ // Test: Log directory resolution logic
48
+ // ============================================================================
49
+ console.log('--- Log directory resolution ---');
50
+
51
+ runTest('Default log directory should resolve ./.pave (args-parser default)', () => {
52
+ // args.config defaults to './.pave' from args-parser (always truthy)
53
+ const argsConfig = './.pave';
54
+ const logDir = path.resolve(argsConfig);
55
+ assertEqual(logDir, path.resolve('./.pave'), 'log dir');
56
+ });
57
+
58
+ runTest('Log directory should use --config path when specified', () => {
59
+ const argsConfig = '/tmp/myproject/.pave';
60
+ const logDir = path.resolve(argsConfig);
61
+ assertEqual(logDir, path.resolve('/tmp/myproject/.pave'), 'log dir');
62
+ });
63
+
64
+ runTest('Log directory should resolve relative --config paths', () => {
65
+ const argsConfig = './custom-config';
66
+ const logDir = path.resolve(argsConfig);
67
+ assertEqual(logDir, path.resolve('./custom-config'), 'log dir');
68
+ });
69
+
70
+ runTest('pave.log path should be inside log directory', () => {
71
+ const logDir = '/tmp/test-config';
72
+ const logFile = path.join(logDir, 'pave.log');
73
+ assertEqual(logFile, path.join('/tmp/test-config', 'pave.log'), 'pave.log path');
74
+ });
75
+
76
+ runTest('debug.log path should be inside log directory', () => {
77
+ const logDir = '/tmp/test-config';
78
+ const debugLog = path.join(logDir, 'debug.log');
79
+ assertEqual(debugLog, path.join('/tmp/test-config', 'debug.log'), 'debug.log path');
80
+ });
81
+
82
+ // ============================================================================
83
+ // Test: Session file location
84
+ // ============================================================================
85
+ console.log('\n--- Session file location ---');
86
+
87
+ runTest('Session file should use resolveSessionFile (not direct path, #108 + #174)', () => {
88
+ // Verify from source code that agent SESSION_FILE uses resolveSessionFile
89
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
90
+ const agentBody = extractHandleAgentCommand(source);
91
+
92
+ // Agent should use resolveSessionFile(configDir) not path.join(process.cwd(), ...)
93
+ assertContains(agentBody, 'resolveSessionFile(configDir)', 'agent uses resolveSessionFile');
94
+ if (agentBody.indexOf("path.join(process.cwd(), '.pave-session.json')") >= 0) {
95
+ throw new Error('agent SESSION_FILE should not use direct .pave-session.json path');
96
+ }
97
+ });
98
+
99
+ runTest('Session file resolves to .pave/session.json', () => {
100
+ // The new session file is inside the config directory
101
+ const configDir = path.join(process.cwd(), '.pave');
102
+ const sessionFile = path.join(configDir, 'session.json');
103
+ assertContains(sessionFile, 'session.json', 'config dir session file');
104
+ assertEqual(path.basename(sessionFile), 'session.json', 'filename');
105
+ });
106
+
107
+ runTest('Only one session file path (no dual writes, #108)', () => {
108
+ // After #108, there should be no CWD_SESSION_FILE — session is now at .pave/session.json
109
+ // (migrated from .pave-session.json in #174, resolved via resolveSessionFile helper)
110
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
111
+ if (source.indexOf('CWD_SESSION_FILE') >= 0) {
112
+ throw new Error('CWD_SESSION_FILE should not exist after #108');
113
+ }
114
+ });
115
+
116
+ // ============================================================================
117
+ // Test: Debug logging enablement
118
+ // ============================================================================
119
+ console.log('\n--- Debug logging enablement ---');
120
+
121
+ // Helper that mirrors the actual debugEnabled logic in handleAgentCommand
122
+ function isDebugEnabled(envDebug, envPaveDebug, argsDebug) {
123
+ return envDebug === '1' || envPaveDebug === '1' || Boolean(argsDebug);
124
+ }
125
+
126
+ runTest('Debug is enabled when DEBUG=1', () => {
127
+ assertEqual(isDebugEnabled('1', undefined, false), true, 'DEBUG=1');
128
+ });
129
+
130
+ runTest('Debug is enabled when PAVE_DEBUG=1', () => {
131
+ assertEqual(isDebugEnabled(undefined, '1', false), true, 'PAVE_DEBUG=1');
132
+ });
133
+
134
+ runTest('Debug is enabled when --debug flag is set', () => {
135
+ assertEqual(isDebugEnabled(undefined, undefined, true), true, '--debug');
136
+ });
137
+
138
+ runTest('Debug is disabled when no debug flag', () => {
139
+ assertEqual(isDebugEnabled(undefined, undefined, false), false, 'no debug');
140
+ });
141
+
142
+ runTest('Debug is disabled when env vars are not "1"', () => {
143
+ assertEqual(isDebugEnabled('0', '0', false), false, 'DEBUG=0');
144
+ assertEqual(isDebugEnabled('true', 'true', false), false, 'DEBUG=true (not "1")');
145
+ });
146
+
147
+ // ============================================================================
148
+ // Test: Log stream writeToAgentLog format
149
+ // ============================================================================
150
+ console.log('\n--- Log format ---');
151
+
152
+ runTest('Log format should include timestamp, level, and message', () => {
153
+ // Simulate the writeToAgentLog format
154
+ const ts = new Date().toISOString();
155
+ const level = "[LOG]";
156
+ const msg = "test message";
157
+ const line = "[" + ts + "] " + level + " " + msg + "\n";
158
+
159
+ assertContains(line, "[LOG]", 'contains level');
160
+ assertContains(line, "test message", 'contains message');
161
+ assertContains(line, "T", 'contains ISO timestamp T separator');
162
+ if (!line.endsWith("\n")) {
163
+ throw new Error('log line should end with newline');
164
+ }
165
+ });
166
+
167
+ runTest('Debug log format should include [agent] prefix', () => {
168
+ const ts = new Date().toISOString();
169
+ const msg = "Starting iteration 1";
170
+ const line = "[" + ts + "] [agent] " + msg + "\n";
171
+
172
+ assertContains(line, "[agent]", 'contains agent prefix');
173
+ assertContains(line, "Starting iteration 1", 'contains message');
174
+ });
175
+
176
+ // ============================================================================
177
+ // Test: Actual file I/O (write and read log files)
178
+ // ============================================================================
179
+ console.log('\n--- File I/O ---');
180
+
181
+ runTest('Should create log directory and write pave.log', () => {
182
+ const tmpDir = path.join(os.tmpdir(), 'pave-test-log-' + Date.now());
183
+ try {
184
+ fs.mkdirSync(tmpDir, { recursive: true });
185
+ const logFile = path.join(tmpDir, 'pave.log');
186
+ fs.appendFileSync(logFile, "[2026-01-01T00:00:00.000Z] [LOG] test log entry\n");
187
+ fs.appendFileSync(logFile, "[2026-01-01T00:00:01.000Z] [INF] second entry\n");
188
+ const content = fs.readFileSync(logFile, 'utf8');
189
+ assertContains(content, 'second entry', 'log file contains entry');
190
+ // Cleanup
191
+ try { fs.unlinkSync(logFile); fs.rmdirSync(tmpDir); } catch (e) { /* ignore */ }
192
+ } catch (e) {
193
+ if (e.message && e.message.indexOf('not a function') >= 0) {
194
+ throw new Error('SKIP: fs.mkdirSync not available in sandbox');
195
+ }
196
+ throw e;
197
+ }
198
+ });
199
+
200
+ runTest('Should write debug.log when debug is enabled', () => {
201
+ const tmpDir = path.join(os.tmpdir(), 'pave-test-debug-' + Date.now());
202
+ try {
203
+ fs.mkdirSync(tmpDir, { recursive: true });
204
+ const debugLog = path.join(tmpDir, 'debug.log');
205
+ const ts = new Date().toISOString();
206
+ fs.appendFileSync(debugLog, "[" + ts + "] [agent] Starting iteration 1\n");
207
+ fs.appendFileSync(debugLog, "[" + ts + "] [exit] agent cleanup called\n");
208
+
209
+ const content = fs.readFileSync(debugLog, 'utf8');
210
+ assertContains(content, '[agent] Starting iteration 1', 'debug log agent entry');
211
+ assertContains(content, '[exit] agent cleanup called', 'debug log exit entry');
212
+ // Cleanup
213
+ try { fs.unlinkSync(debugLog); fs.rmdirSync(tmpDir); } catch (e) { /* ignore */ }
214
+ } catch (e) {
215
+ if (e.message && e.message.indexOf('not a function') >= 0) {
216
+ throw new Error('SKIP: fs.mkdirSync not available in sandbox');
217
+ }
218
+ throw e;
219
+ }
220
+ });
221
+
222
+ runTest('Should write session.json to .pave/ directory (#174)', () => {
223
+ const tmpDir = path.join(os.tmpdir(), 'pave-test-session-' + Date.now());
224
+ try {
225
+ const configDir = path.join(tmpDir, '.pave');
226
+ fs.mkdirSync(configDir, { recursive: true });
227
+ // Session file goes in .pave/ directory
228
+ const sessionFile = path.join(configDir, 'session.json');
229
+ fs.writeFileSync(sessionFile, JSON.stringify({ sessionId: 'test-123', agentName: 'test' }, null, 2));
230
+
231
+ const data = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
232
+ assertEqual(data.sessionId, 'test-123', 'session ID');
233
+ assertEqual(data.agentName, 'test', 'agent name');
234
+ // Verify session file IS inside the .pave subdirectory
235
+ assertEqual(path.basename(path.dirname(sessionFile)), '.pave', 'in .pave subdir');
236
+ // Cleanup
237
+ try { fs.unlinkSync(sessionFile); fs.rmdirSync(configDir); fs.rmdirSync(tmpDir); } catch (e) { /* ignore */ }
238
+ } catch (e) {
239
+ if (e.message && e.message.indexOf('not a function') >= 0) {
240
+ throw new Error('SKIP: fs.mkdirSync not available in sandbox');
241
+ }
242
+ throw e;
243
+ }
244
+ });
245
+
246
+ // ============================================================================
247
+ // Test: Source code verification
248
+ // ============================================================================
249
+ console.log('\n--- Source code verification ---');
250
+
251
+ runTest('handleAgentCommand should reference AGENT_LOG_DIR and pave.log', () => {
252
+ try {
253
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
254
+ assertContains(source, 'AGENT_LOG_DIR', 'source contains AGENT_LOG_DIR');
255
+ assertContains(source, 'AGENT_LOG_FILE', 'source contains AGENT_LOG_FILE');
256
+ assertContains(source, "pave.log", 'source references pave.log');
257
+ assertContains(source, 'agentLogStream', 'source contains agentLogStream');
258
+ } catch (e) {
259
+ if (e.code === 'ENOENT') {
260
+ throw new Error('Could not read index.js for verification');
261
+ }
262
+ throw e;
263
+ }
264
+ });
265
+
266
+ runTest('handleAgentCommand should reference DEBUG_LOG_FILE and debug.log', () => {
267
+ try {
268
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
269
+ assertContains(source, 'DEBUG_LOG_FILE', 'source contains DEBUG_LOG_FILE');
270
+ assertContains(source, "debug.log", 'source references debug.log');
271
+ assertContains(source, 'debugLog(', 'source contains debugLog helper calls');
272
+ assertContains(source, 'debugEnabled', 'source contains debugEnabled check');
273
+ } catch (e) {
274
+ if (e.code === 'ENOENT') {
275
+ throw new Error('Could not read index.js for verification');
276
+ }
277
+ throw e;
278
+ }
279
+ });
280
+
281
+ // Helper: extract the full body of handleAgentCommand by brace matching
282
+ function extractHandleAgentCommand(source) {
283
+ const funcStart = source.indexOf('async function handleAgentCommand');
284
+ if (funcStart < 0) throw new Error('handleAgentCommand not found in source');
285
+ // Find the opening brace of the function
286
+ const braceStart = source.indexOf('{', funcStart);
287
+ if (braceStart < 0) throw new Error('Could not find opening brace of handleAgentCommand');
288
+ let depth = 0;
289
+ let end = braceStart;
290
+ for (let i = braceStart; i < source.length; i++) {
291
+ if (source[i] === '{') depth++;
292
+ else if (source[i] === '}') {
293
+ depth--;
294
+ if (depth === 0) { end = i + 1; break; }
295
+ }
296
+ }
297
+ return source.substring(funcStart, end);
298
+ }
299
+
300
+ runTest('handleAgentCommand should NOT use crypto for session hash', () => {
301
+ try {
302
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
303
+ const agentBody = extractHandleAgentCommand(source);
304
+ // Remove comments before checking for crypto
305
+ const codeOnly = agentBody.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
306
+ if (codeOnly.indexOf("require(\"crypto\")") >= 0 || codeOnly.indexOf("require('crypto')") >= 0) {
307
+ throw new Error('handleAgentCommand should not require crypto (session hash removed)');
308
+ }
309
+ } catch (e) {
310
+ if (e.message && (e.message.indexOf('handleAgentCommand') >= 0 || e.message.indexOf('crypto') >= 0)) throw e;
311
+ if (e.code === 'ENOENT') throw new Error('Could not read index.js');
312
+ throw e;
313
+ }
314
+ });
315
+
316
+ runTest('Session file should use resolveSessionFile not agent-session-*.json', () => {
317
+ try {
318
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
319
+ const agentBody = extractHandleAgentCommand(source);
320
+
321
+ // Check that resolveSessionFile is used
322
+ assertContains(agentBody, 'resolveSessionFile', 'uses resolveSessionFile');
323
+
324
+ // Check that agent-session-*.json is NOT used in code (only in comments)
325
+ const codeOnly = agentBody.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
326
+ if (codeOnly.indexOf('agent-session-') >= 0) {
327
+ throw new Error('Should not reference agent-session-*.json pattern in code');
328
+ }
329
+ } catch (e) {
330
+ if (e.message && (e.message.indexOf('agent-session') >= 0 || e.message.indexOf('resolveSessionFile') >= 0 || e.message.indexOf('handleAgentCommand') >= 0)) throw e;
331
+ if (e.code === 'ENOENT') throw new Error('Could not read index.js');
332
+ throw e;
333
+ }
334
+ });
335
+
336
+ // ============================================================================
337
+ // Summary
338
+ // ============================================================================
339
+ console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ===\n');
340
+ if (failed > 0) process.exitCode = 1;
341
+
342
+ // Export for run-tests.js integration
343
+ if (typeof module !== 'undefined') {
344
+ module.exports = {
345
+ runAgentLogTests() {
346
+ return { passed, failed, total: passed + failed };
347
+ },
348
+ };
349
+ }