@cnrai/pave 0.3.35 → 0.3.51

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/LICENSE +21 -0
  2. package/README.md +21 -218
  3. package/package.json +32 -35
  4. package/pave.js +3 -0
  5. package/sandbox/SandboxRunner.js +1 -0
  6. package/sandbox/pave-run.js +2 -0
  7. package/sandbox/permission.js +1 -0
  8. package/sandbox/utils/yaml.js +1 -0
  9. package/MARKETPLACE.md +0 -406
  10. package/build-binary.js +0 -591
  11. package/build-npm.js +0 -537
  12. package/build.js +0 -230
  13. package/check-binary.js +0 -26
  14. package/deploy.sh +0 -95
  15. package/index.js +0 -5776
  16. package/lib/agent-registry.js +0 -1037
  17. package/lib/args-parser.js +0 -837
  18. package/lib/blessed-widget-patched.js +0 -93
  19. package/lib/cli-markdown.js +0 -590
  20. package/lib/compaction.js +0 -153
  21. package/lib/duration.js +0 -94
  22. package/lib/hash.js +0 -22
  23. package/lib/marketplace.js +0 -866
  24. package/lib/memory-config.js +0 -166
  25. package/lib/skill-manager.js +0 -891
  26. package/lib/soul.js +0 -31
  27. package/lib/tool-output-formatter.js +0 -180
  28. package/start-pave.sh +0 -149
  29. package/status.js +0 -271
  30. package/test/abort-stream.test.js +0 -445
  31. package/test/agent-auto-compaction.test.js +0 -552
  32. package/test/agent-comm-abort.test.js +0 -95
  33. package/test/agent-comm.test.js +0 -598
  34. package/test/agent-inbox.test.js +0 -576
  35. package/test/agent-init.test.js +0 -264
  36. package/test/agent-interrupt.test.js +0 -314
  37. package/test/agent-lifecycle.test.js +0 -520
  38. package/test/agent-log-files.test.js +0 -349
  39. package/test/agent-mode.manual-test.js +0 -392
  40. package/test/agent-parsing.test.js +0 -228
  41. package/test/agent-post-stream-idle.test.js +0 -762
  42. package/test/agent-registry.test.js +0 -359
  43. package/test/agent-rm.test.js +0 -442
  44. package/test/agent-spawn.test.js +0 -933
  45. package/test/agent-status-api.test.js +0 -624
  46. package/test/agent-update.test.js +0 -435
  47. package/test/args-parser.test.js +0 -391
  48. package/test/auto-compaction-chat.manual-test.js +0 -227
  49. package/test/auto-compaction.test.js +0 -941
  50. package/test/build-config.test.js +0 -120
  51. package/test/build-npm.test.js +0 -388
  52. package/test/chat-command.test.js +0 -137
  53. package/test/chat-leading-lines.test.js +0 -159
  54. package/test/config-flag.test.js +0 -272
  55. package/test/cursor-drift.test.js +0 -135
  56. package/test/debug-require.js +0 -23
  57. package/test/dir-migration.test.js +0 -323
  58. package/test/duration.test.js +0 -229
  59. package/test/ghostty-term.test.js +0 -202
  60. package/test/http500-backoff.test.js +0 -854
  61. package/test/integration.test.js +0 -86
  62. package/test/memory-guard-env.test.js +0 -220
  63. package/test/pr233-fixes.test.js +0 -259
  64. package/test/run-agent-init.js +0 -297
  65. package/test/run-all.js +0 -64
  66. package/test/run-config-flag.js +0 -159
  67. package/test/run-cursor-drift.js +0 -82
  68. package/test/run-session-path.js +0 -154
  69. package/test/run-tests.js +0 -643
  70. package/test/sandbox-redirect.test.js +0 -202
  71. package/test/session-path.test.js +0 -132
  72. package/test/shebang-strip.test.js +0 -241
  73. package/test/soul-reinject.test.js +0 -1027
  74. package/test/soul-reread.test.js +0 -281
  75. package/test/tool-output-formatter.test.js +0 -486
  76. package/test/tool-output-gating.test.js +0 -143
  77. package/test/tool-states.test.js +0 -167
  78. package/test/tools-flag.test.js +0 -65
  79. package/test/tui-attach.test.js +0 -1255
  80. package/test/tui-compaction.test.js +0 -354
  81. package/test/tui-wrap.test.js +0 -568
  82. package/test-binary.js +0 -52
  83. package/test-binary2.js +0 -36
@@ -1,323 +0,0 @@
1
- /**
2
- * Test directory structure migration (Issue #174)
3
- *
4
- * Verifies:
5
- * - .pave-session.json → .pave/session.json migration
6
- * - SOUL file resolution order: --soul > .pave/AGENTS.md > ./AGENTS.md
7
- * - --config no longer redirects skills/marketplace
8
- * - pave agent init: reserved path check, tips
9
- */
10
-
11
- const path = require('path');
12
- const fs = require('fs');
13
-
14
- const paveIndexPath = path.join(__dirname, '..', 'index.js');
15
- const tuiIndexPath = path.join(__dirname, '..', '..', 'tui', 'index.js');
16
-
17
- function runDirMigrationTests() {
18
- const paveSource = fs.readFileSync(paveIndexPath, 'utf8');
19
- const tuiSource = fs.readFileSync(tuiIndexPath, 'utf8');
20
-
21
- let passed = 0;
22
- let failed = 0;
23
-
24
- function test(name, fn) {
25
- try {
26
- fn();
27
- console.log('\u2705 ' + name);
28
- passed++;
29
- } catch (e) {
30
- console.log('\u274c ' + name);
31
- console.log(' ' + e.message);
32
- failed++;
33
- }
34
- }
35
-
36
- function assert(cond, msg) {
37
- if (!cond) throw new Error(msg || 'Assertion failed');
38
- }
39
-
40
- function assertIncludes(str, substr, msg) {
41
- if (!str.includes(substr)) {
42
- throw new Error((msg || 'Assertion failed') + ': expected to include ' + JSON.stringify(substr));
43
- }
44
- }
45
-
46
- function assertNotIncludes(str, substr, msg) {
47
- if (str.includes(substr)) {
48
- throw new Error((msg || 'Assertion failed') + ': should NOT include ' + JSON.stringify(substr));
49
- }
50
- }
51
-
52
- console.log('\nTesting directory structure migration (Issue #174)\n');
53
- console.log('============================================================\n');
54
-
55
- // ── resolveSessionFile helper ──
56
-
57
- console.log('--- Session file migration ---\n');
58
-
59
- test('pave has resolveSessionFile helper function', () => {
60
- assertIncludes(paveSource, 'function resolveSessionFile(', 'Should have resolveSessionFile function');
61
- });
62
-
63
- test('resolveSessionFile reads old .pave-session.json path', () => {
64
- const idx = paveSource.indexOf('function resolveSessionFile(');
65
- assert(idx > -1, 'Should find resolveSessionFile');
66
- const block = paveSource.substring(idx, idx + 600);
67
- assertIncludes(block, '.pave-session.json', 'Should reference old session file');
68
- });
69
-
70
- test('resolveSessionFile writes new session.json path', () => {
71
- const idx = paveSource.indexOf('function resolveSessionFile(');
72
- assert(idx > -1, 'Should find resolveSessionFile');
73
- const block = paveSource.substring(idx, idx + 600);
74
- assertIncludes(block, 'session.json', 'Should reference new session.json');
75
- });
76
-
77
- test('resolveSessionFile copies old to new', () => {
78
- const idx = paveSource.indexOf('function resolveSessionFile(');
79
- assert(idx > -1, 'Should find resolveSessionFile');
80
- const block = paveSource.substring(idx, idx + 600);
81
- assertIncludes(block, 'copyFileSync', 'Should copy old file to new location');
82
- });
83
-
84
- test('resolveSessionFile deletes old file after migration', () => {
85
- const idx = paveSource.indexOf('function resolveSessionFile(');
86
- assert(idx > -1, 'Should find resolveSessionFile');
87
- const block = paveSource.substring(idx, idx + 600);
88
- assertIncludes(block, 'unlinkSync', 'Should delete old file after copy');
89
- });
90
-
91
- test('resolveSessionFile creates directory recursively', () => {
92
- const idx = paveSource.indexOf('function resolveSessionFile(');
93
- assert(idx > -1, 'Should find resolveSessionFile');
94
- const block = paveSource.substring(idx, idx + 600);
95
- assertIncludes(block, 'recursive: true', 'Should create directory recursively');
96
- });
97
-
98
- test('resolveSessionFile migration mkdir uses mode 0o700', () => {
99
- const idx = paveSource.indexOf('function resolveSessionFile(');
100
- assert(idx > -1, 'Should find resolveSessionFile');
101
- const block = paveSource.substring(idx, idx + 800);
102
- // Find the migration if-block (between existsSync checks and 'return oldPath' fallback)
103
- const migrationStart = block.indexOf('copyFileSync');
104
- const migrationEnd = block.indexOf('return oldPath');
105
- assert(migrationStart > -1, 'Should find migration copy');
106
- assert(migrationEnd > -1, 'Should find migration fallback');
107
- const migrationBlock = block.substring(0, migrationEnd);
108
- // The mkdirSync INSIDE the migration block should also use mode: 0o700
109
- const mkdirIdx = migrationBlock.indexOf('mkdirSync');
110
- assert(mkdirIdx > -1, 'Migration block should have mkdirSync');
111
- const mkdirLine = migrationBlock.substring(mkdirIdx, mkdirIdx + 80);
112
- assertIncludes(mkdirLine, '0o700', 'Migration mkdirSync should use mode 0o700');
113
- });
114
-
115
- test('chat command uses resolveSessionFile', () => {
116
- const idx = paveSource.indexOf('async function handleChatCommand');
117
- assert(idx > -1, 'Should find handleChatCommand');
118
- const block = paveSource.substring(idx, idx + 30000);
119
- assertIncludes(block, 'resolveSessionFile(configDir)', 'Chat should use resolveSessionFile');
120
- });
121
-
122
- test('agent command uses resolveSessionFile', () => {
123
- const idx = paveSource.indexOf('async function handleAgentCommand');
124
- assert(idx > -1, 'Should find handleAgentCommand');
125
- const block = paveSource.substring(idx, idx + 30000);
126
- assertIncludes(block, 'resolveSessionFile(configDir)', 'Agent should use resolveSessionFile');
127
- });
128
-
129
- test('history command uses resolveSessionFile', () => {
130
- // pave history should use the new path
131
- const idx = paveSource.indexOf('async function handleHistoryCommand');
132
- assert(idx > -1, 'Should find handleHistoryCommand');
133
- const block = paveSource.substring(idx, idx + 30000);
134
- assertIncludes(block, 'resolveSessionFile(configDir)', 'History should use resolveSessionFile(configDir)');
135
- });
136
-
137
- test('resolveSessionFile creates directory on fresh projects', () => {
138
- const idx = paveSource.indexOf('function resolveSessionFile(');
139
- assert(idx > -1, 'Should find resolveSessionFile');
140
- const block = paveSource.substring(idx, idx + 800);
141
- // Should have a mkdirSync call OUTSIDE the migration if-block
142
- const migrationEnd = block.indexOf('return oldPath');
143
- assert(migrationEnd > -1, 'Should find migration fallback');
144
- const afterMigration = block.substring(migrationEnd);
145
- assertIncludes(afterMigration, 'mkdirSync', 'Should ensure directory exists on fresh projects');
146
- assertIncludes(afterMigration, 'recursive: true', 'Should create recursively');
147
- });
148
-
149
- // ── TUI session file migration ──
150
-
151
- console.log('\n--- TUI session file ---\n');
152
-
153
- test('TUI migrates .pave-session.json to .pave/session.json', () => {
154
- assertIncludes(tuiSource, '.pave-session.json', 'TUI should reference old path for migration');
155
- assertIncludes(tuiSource, "session.json", 'TUI should reference new session.json');
156
- });
157
-
158
- test('TUI session migration is inline IIFE', () => {
159
- // TUI does inline migration since it can't import pave's helper
160
- assertIncludes(tuiSource, 'issue #174', 'Should reference issue #174');
161
- });
162
-
163
- // ── SOUL file resolution ──
164
-
165
- console.log('\n--- SOUL file resolution ---\n');
166
-
167
- test('soul resolution checks .pave/AGENTS.md', () => {
168
- const idx = paveSource.indexOf('resolvedSoulPath');
169
- assert(idx > -1, 'Should find resolvedSoulPath');
170
- const block = paveSource.substring(idx, idx + 500);
171
- assertIncludes(block, "AGENTS.md", 'Should reference AGENTS.md in config dir');
172
- assertIncludes(block, 'configDir', 'Should use configDir for .pave/AGENTS.md lookup');
173
- });
174
-
175
- test('explicit --soul flag has highest priority', () => {
176
- const idx = paveSource.indexOf('resolvedSoulPath');
177
- assert(idx > -1, 'Should find resolvedSoulPath');
178
- const block = paveSource.substring(idx, idx + 500);
179
- // soul (explicit flag) should be checked first
180
- const soulIdx = block.indexOf('if (soul)');
181
- const paveIdx = block.indexOf('paveAgents');
182
- assert(soulIdx > -1, 'Should check explicit soul flag');
183
- assert(paveIdx > -1, 'Should check paveAgents path');
184
- assert(soulIdx < paveIdx, 'Explicit --soul should have higher priority than .pave/AGENTS.md');
185
- });
186
-
187
- test('.pave/AGENTS.md checked before ./AGENTS.md', () => {
188
- const idx = paveSource.indexOf('resolvedSoulPath');
189
- assert(idx > -1, 'Should find resolvedSoulPath');
190
- const block = paveSource.substring(idx, idx + 800);
191
- assertIncludes(block, 'fs.existsSync(paveAgents)', 'Should check if .pave/AGENTS.md exists');
192
- });
193
-
194
- test('custom positional SOUL path not overridden by .pave/AGENTS.md', () => {
195
- const idx = paveSource.indexOf('resolvedSoulPath');
196
- assert(idx > -1, 'Should find resolvedSoulPath');
197
- const block = paveSource.substring(idx, idx + 800);
198
- assertIncludes(block, 'usingDefaultAgents', 'Should check if using default AGENTS.md');
199
- // When using custom path (not AGENTS.md), should use rootAgents directly
200
- assertIncludes(block, 'resolvedSoulPath = rootAgents', 'Custom positional path should be used as-is');
201
- });
202
-
203
- test('TUI mkdir uses mode 0o700 for .pave directory', () => {
204
- const idx = tuiSource.indexOf('Ensure directory exists even on fresh');
205
- assert(idx > -1, 'Should find fresh project mkdir');
206
- const block = tuiSource.substring(idx, idx + 200);
207
- assertIncludes(block, 'mode: 0o700', 'Should use restrictive permissions on .pave/');
208
- });
209
-
210
- // ── --config behavior ──
211
-
212
- console.log('\n--- --config behavior ---\n');
213
-
214
- test('--config does not redirect skillManager', () => {
215
- // Should NOT have skillManager.setPaveHome(configPath)
216
- assertNotIncludes(paveSource, 'skillManager.setPaveHome(configPath)',
217
- 'Skills should always use ~/.pave/, not --config path');
218
- });
219
-
220
- test('--config does not redirect marketplace', () => {
221
- assertNotIncludes(paveSource, 'marketplace.setPaveHome(configPath)',
222
- 'Marketplace should always use ~/.pave/, not --config path');
223
- });
224
-
225
- test('--config still sets Permission config dir', () => {
226
- assertIncludes(paveSource, 'Permission.setConfigDir(configPath)',
227
- 'Permissions should follow --config');
228
- });
229
-
230
- // ── pave agent init ──
231
-
232
- console.log('\n--- pave agent init ---\n');
233
-
234
- test('agent init rejects reserved paths using path.relative', () => {
235
- const idx = paveSource.indexOf('function handleAgentInit');
236
- assert(idx > -1, 'Should find handleAgentInit');
237
- const block = paveSource.substring(idx, idx + 5000);
238
- assertIncludes(block, 'reservedDirs', 'Should check reserved directories');
239
- assertIncludes(block, 'path.relative', 'Should use path.relative for boundary check');
240
- assertIncludes(block, 'homedir()', 'Should use homedir() for cross-platform home directory');
241
- });
242
-
243
- test('agent init reserved-path error uses resolved paveHome path', () => {
244
- const idx = paveSource.indexOf('function handleAgentInit');
245
- assert(idx > -1, 'Should find handleAgentInit');
246
- const block = paveSource.substring(idx, idx + 5000);
247
- // Error message should use paveHome variable (cross-platform), not hardcoded ~/.pave/
248
- assertIncludes(block, 'paveHome', 'Reserved-path error should use resolved paveHome');
249
- assert(!block.includes("~/.pave/"), 'Should NOT hardcode ~/.pave/ in error message (not cross-platform)');
250
- });
251
-
252
- test('agent init shows AGENTS.md tip with configPath', () => {
253
- const idx = paveSource.indexOf('function handleAgentInit');
254
- assert(idx > -1, 'Should find handleAgentInit');
255
- const block = paveSource.substring(idx, idx + 5000);
256
- assertIncludes(block, 'AGENTS.md', 'Should show AGENTS.md tip');
257
- assertIncludes(block, 'configRelPath', 'Should use relative path for tips');
258
- });
259
-
260
- test('agent init shows .gitignore tip with configPath', () => {
261
- const idx = paveSource.indexOf('function handleAgentInit');
262
- assert(idx > -1, 'Should find handleAgentInit');
263
- const block = paveSource.substring(idx, idx + 5000);
264
- assertIncludes(block, '.gitignore', 'Should show .gitignore tip');
265
- assertIncludes(block, 'session.json', 'Should mention session.json');
266
- assertIncludes(block, 'configRelPath', 'Should use relative path for session tip');
267
- });
268
-
269
- // ── No stale references ──
270
-
271
- console.log('\n--- No stale references ---\n');
272
-
273
- test('--config threads SESSION_FILE env var to TUI before require', () => {
274
- // runTUI should set process.env.SESSION_FILE from configPath before loading TUI
275
- let tuiRequireIdx = paveSource.indexOf("require(\"../tui/index.js\")");
276
- if (tuiRequireIdx === -1) tuiRequireIdx = paveSource.indexOf("require('../tui/index.js')");
277
- assert(tuiRequireIdx > -1, 'Should find TUI require');
278
- // SESSION_FILE env var should be set BEFORE the TUI require
279
- const beforeTuiRequire = paveSource.substring(0, tuiRequireIdx);
280
- assert(beforeTuiRequire.includes('process.env.SESSION_FILE') && beforeTuiRequire.includes('configPath'),
281
- 'Should set process.env.SESSION_FILE from configPath before requiring TUI');
282
- });
283
-
284
- test('no direct .pave-session.json path construction outside migration', () => {
285
- // Count occurrences - should only be in resolveSessionFile helper + comments
286
- const lines = paveSource.split('\n');
287
- let directConstructions = 0;
288
- for (let i = 0; i < lines.length; i++) {
289
- const line = lines[i];
290
- if (line.includes('.pave-session.json') &&
291
- !line.includes('Migrat') &&
292
- !line.includes('migrat') &&
293
- !line.includes('oldPath') &&
294
- !line.includes('//') &&
295
- !line.includes('*')) {
296
- directConstructions++;
297
- }
298
- }
299
- assert(directConstructions === 0,
300
- 'Should not directly construct .pave-session.json path outside migration (found ' + directConstructions + ')');
301
- });
302
-
303
- // ============================================================
304
-
305
- console.log('\n============================================================');
306
- console.log('Results: ' + passed + ' passed, ' + failed + ' failed, ' + (passed + failed) + ' total');
307
- console.log('============================================================\n');
308
-
309
- if (failed > 0) {
310
- if (require.main === module) {
311
- process.exit(1);
312
- } else {
313
- throw new Error(failed + ' test(s) failed in dir-migration');
314
- }
315
- }
316
-
317
- return { passed, failed, total: passed + failed };
318
- }
319
-
320
- // Auto-execute tests on load
321
- runDirMigrationTests();
322
-
323
- module.exports = { runDirMigrationTests };
@@ -1,229 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Automated tests for duration.js - parseSleepDuration and formatDuration
4
- * This file runs under `npm test` via test/run-tests.js
5
- */
6
-
7
- const { parseSleepDuration, formatDuration, MAX_SETTIMEOUT_MS, MIN_SLEEP_MS } = require('../lib/duration');
8
-
9
- function runTest(name, testFn) {
10
- try {
11
- testFn();
12
- console.log(`✅ ${name}`);
13
- } catch (error) {
14
- console.log(`❌ ${name}: ${error.message}`);
15
- process.exitCode = 1;
16
- }
17
- }
18
-
19
- function assertEqual(actual, expected, message) {
20
- if (actual !== expected) {
21
- throw new Error(`${message || 'Assertion'}: expected ${expected}, got ${actual}`);
22
- }
23
- }
24
-
25
- function assertThrows(fn, expectedMessage) {
26
- try {
27
- fn();
28
- throw new Error(`Expected to throw but did not`);
29
- } catch (error) {
30
- if (expectedMessage && !error.message.includes(expectedMessage)) {
31
- throw new Error(`Expected error containing "${expectedMessage}", got "${error.message}"`);
32
- }
33
- }
34
- }
35
-
36
- function assertLessThanOrEqual(actual, expected, message) {
37
- if (actual > expected) {
38
- throw new Error(`${message || 'Assertion'}: expected ${actual} <= ${expected}`);
39
- }
40
- }
41
-
42
- // ============================================================================
43
- // parseSleepDuration Tests - Default Value
44
- // ============================================================================
45
-
46
- runTest('parseSleepDuration: null returns 5 minutes', () => {
47
- assertEqual(parseSleepDuration(null), 5 * 60 * 1000);
48
- });
49
-
50
- runTest('parseSleepDuration: undefined returns 5 minutes', () => {
51
- assertEqual(parseSleepDuration(undefined), 5 * 60 * 1000);
52
- });
53
-
54
- // ============================================================================
55
- // parseSleepDuration Tests - Seconds
56
- // ============================================================================
57
-
58
- runTest('parseSleepDuration: 5s = 5000ms', () => {
59
- assertEqual(parseSleepDuration('5s'), 5000);
60
- });
61
-
62
- runTest('parseSleepDuration: 30sec = 30000ms', () => {
63
- assertEqual(parseSleepDuration('30sec'), 30000);
64
- });
65
-
66
- // ============================================================================
67
- // parseSleepDuration Tests - Minutes
68
- // ============================================================================
69
-
70
- runTest('parseSleepDuration: 5m = 5 minutes', () => {
71
- assertEqual(parseSleepDuration('5m'), 5 * 60 * 1000);
72
- });
73
-
74
- runTest('parseSleepDuration: 30min = 30 minutes', () => {
75
- assertEqual(parseSleepDuration('30min'), 30 * 60 * 1000);
76
- });
77
-
78
- // ============================================================================
79
- // parseSleepDuration Tests - Hours
80
- // ============================================================================
81
-
82
- runTest('parseSleepDuration: 1h = 1 hour', () => {
83
- assertEqual(parseSleepDuration('1h'), 60 * 60 * 1000);
84
- });
85
-
86
- runTest('parseSleepDuration: 2hr = 2 hours', () => {
87
- assertEqual(parseSleepDuration('2hr'), 2 * 60 * 60 * 1000);
88
- });
89
-
90
- runTest('parseSleepDuration: 3hour = 3 hours', () => {
91
- assertEqual(parseSleepDuration('3hour'), 3 * 60 * 60 * 1000);
92
- });
93
-
94
- // ============================================================================
95
- // parseSleepDuration Tests - Days
96
- // ============================================================================
97
-
98
- runTest('parseSleepDuration: 1d = 1 day', () => {
99
- assertEqual(parseSleepDuration('1d'), 24 * 60 * 60 * 1000);
100
- });
101
-
102
- runTest('parseSleepDuration: 2day = 2 days', () => {
103
- assertEqual(parseSleepDuration('2day'), 2 * 24 * 60 * 60 * 1000);
104
- });
105
-
106
- // ============================================================================
107
- // parseSleepDuration Tests - Fractional Values
108
- // ============================================================================
109
-
110
- runTest('parseSleepDuration: 1.5m = 1.5 minutes', () => {
111
- assertEqual(parseSleepDuration('1.5m'), 1.5 * 60 * 1000);
112
- });
113
-
114
- runTest('parseSleepDuration: 0.5h = 30 minutes', () => {
115
- assertEqual(parseSleepDuration('0.5h'), 0.5 * 60 * 60 * 1000);
116
- });
117
-
118
- // ============================================================================
119
- // parseSleepDuration Tests - Default Unit (minutes)
120
- // ============================================================================
121
-
122
- runTest('parseSleepDuration: 10 (no unit) = 10 minutes', () => {
123
- assertEqual(parseSleepDuration('10'), 10 * 60 * 1000);
124
- });
125
-
126
- // ============================================================================
127
- // parseSleepDuration Tests - Validation Errors
128
- // ============================================================================
129
-
130
- runTest('parseSleepDuration: 0s throws error', () => {
131
- assertThrows(() => parseSleepDuration('0s'), 'must be greater than 0');
132
- });
133
-
134
- runTest('parseSleepDuration: -5m throws error', () => {
135
- assertThrows(() => parseSleepDuration('-5m'), 'Invalid sleep duration');
136
- });
137
-
138
- runTest('parseSleepDuration: abc throws error', () => {
139
- assertThrows(() => parseSleepDuration('abc'), 'Invalid sleep duration');
140
- });
141
-
142
- runTest('parseSleepDuration: 0.5s throws (< 1 second)', () => {
143
- assertThrows(() => parseSleepDuration('0.5s'), 'at least 1 second');
144
- });
145
-
146
- runTest('parseSleepDuration: 30d throws (exceeds max)', () => {
147
- assertThrows(() => parseSleepDuration('30d'), 'exceeds maximum');
148
- });
149
-
150
- runTest('parseSleepDuration: 24d is within limit', () => {
151
- const result = parseSleepDuration('24d');
152
- assertLessThanOrEqual(result, MAX_SETTIMEOUT_MS);
153
- });
154
-
155
- // ============================================================================
156
- // formatDuration Tests - Milliseconds
157
- // ============================================================================
158
-
159
- runTest('formatDuration: 500ms', () => {
160
- assertEqual(formatDuration(500), '500ms');
161
- });
162
-
163
- // ============================================================================
164
- // formatDuration Tests - Seconds
165
- // ============================================================================
166
-
167
- runTest('formatDuration: 5000ms = 5s', () => {
168
- assertEqual(formatDuration(5000), '5s');
169
- });
170
-
171
- runTest('formatDuration: 30000ms = 30s', () => {
172
- assertEqual(formatDuration(30000), '30s');
173
- });
174
-
175
- runTest('formatDuration: 1500ms = 1.5s', () => {
176
- assertEqual(formatDuration(1500), '1.5s');
177
- });
178
-
179
- // ============================================================================
180
- // formatDuration Tests - Minutes
181
- // ============================================================================
182
-
183
- runTest('formatDuration: 60000ms = 1m', () => {
184
- assertEqual(formatDuration(60000), '1m');
185
- });
186
-
187
- runTest('formatDuration: 300000ms = 5m', () => {
188
- assertEqual(formatDuration(300000), '5m');
189
- });
190
-
191
- runTest('formatDuration: 90000ms = 1.5m', () => {
192
- assertEqual(formatDuration(90000), '1.5m');
193
- });
194
-
195
- // ============================================================================
196
- // formatDuration Tests - Hours
197
- // ============================================================================
198
-
199
- runTest('formatDuration: 3600000ms = 1.0h', () => {
200
- assertEqual(formatDuration(3600000), '1.0h');
201
- });
202
-
203
- runTest('formatDuration: 7200000ms = 2.0h', () => {
204
- assertEqual(formatDuration(7200000), '2.0h');
205
- });
206
-
207
- // ============================================================================
208
- // formatDuration Tests - Days
209
- // ============================================================================
210
-
211
- runTest('formatDuration: 86400000ms = 1.0d', () => {
212
- assertEqual(formatDuration(86400000), '1.0d');
213
- });
214
-
215
- runTest('formatDuration: 172800000ms = 2.0d', () => {
216
- assertEqual(formatDuration(172800000), '2.0d');
217
- });
218
-
219
- // ============================================================================
220
- // Constants Tests
221
- // ============================================================================
222
-
223
- runTest('MAX_SETTIMEOUT_MS is 2147483647', () => {
224
- assertEqual(MAX_SETTIMEOUT_MS, 2147483647);
225
- });
226
-
227
- runTest('MIN_SLEEP_MS is 1000', () => {
228
- assertEqual(MIN_SLEEP_MS, 1000);
229
- });