@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.
- package/MARKETPLACE.md +406 -0
- package/README.md +218 -21
- package/build-binary.js +591 -0
- package/build-npm.js +537 -0
- package/build.js +230 -0
- package/check-binary.js +26 -0
- package/deploy.sh +95 -0
- package/index.js +5775 -0
- package/lib/agent-registry.js +1037 -0
- package/lib/args-parser.js +837 -0
- package/lib/blessed-widget-patched.js +93 -0
- package/lib/cli-markdown.js +590 -0
- package/lib/compaction.js +153 -0
- package/lib/duration.js +94 -0
- package/lib/hash.js +22 -0
- package/lib/marketplace.js +866 -0
- package/lib/memory-config.js +166 -0
- package/lib/skill-manager.js +891 -0
- package/lib/soul.js +31 -0
- package/lib/tool-output-formatter.js +180 -0
- package/package.json +35 -33
- package/start-pave.sh +149 -0
- package/status.js +271 -0
- package/test/abort-stream.test.js +445 -0
- package/test/agent-auto-compaction.test.js +552 -0
- package/test/agent-comm-abort.test.js +95 -0
- package/test/agent-comm.test.js +598 -0
- package/test/agent-inbox.test.js +576 -0
- package/test/agent-init.test.js +264 -0
- package/test/agent-interrupt.test.js +314 -0
- package/test/agent-lifecycle.test.js +520 -0
- package/test/agent-log-files.test.js +349 -0
- package/test/agent-mode.manual-test.js +392 -0
- package/test/agent-parsing.test.js +228 -0
- package/test/agent-post-stream-idle.test.js +762 -0
- package/test/agent-registry.test.js +359 -0
- package/test/agent-rm.test.js +442 -0
- package/test/agent-spawn.test.js +933 -0
- package/test/agent-status-api.test.js +624 -0
- package/test/agent-update.test.js +435 -0
- package/test/args-parser.test.js +391 -0
- package/test/auto-compaction-chat.manual-test.js +227 -0
- package/test/auto-compaction.test.js +941 -0
- package/test/build-config.test.js +120 -0
- package/test/build-npm.test.js +388 -0
- package/test/chat-command.test.js +137 -0
- package/test/chat-leading-lines.test.js +159 -0
- package/test/config-flag.test.js +272 -0
- package/test/cursor-drift.test.js +135 -0
- package/test/debug-require.js +23 -0
- package/test/dir-migration.test.js +323 -0
- package/test/duration.test.js +229 -0
- package/test/ghostty-term.test.js +202 -0
- package/test/http500-backoff.test.js +854 -0
- package/test/integration.test.js +86 -0
- package/test/memory-guard-env.test.js +220 -0
- package/test/pr233-fixes.test.js +259 -0
- package/test/run-agent-init.js +297 -0
- package/test/run-all.js +64 -0
- package/test/run-config-flag.js +159 -0
- package/test/run-cursor-drift.js +82 -0
- package/test/run-session-path.js +154 -0
- package/test/run-tests.js +643 -0
- package/test/sandbox-redirect.test.js +202 -0
- package/test/session-path.test.js +132 -0
- package/test/shebang-strip.test.js +241 -0
- package/test/soul-reinject.test.js +1027 -0
- package/test/soul-reread.test.js +281 -0
- package/test/tool-output-formatter.test.js +486 -0
- package/test/tool-output-gating.test.js +143 -0
- package/test/tool-states.test.js +167 -0
- package/test/tools-flag.test.js +65 -0
- package/test/tui-attach.test.js +1255 -0
- package/test/tui-compaction.test.js +354 -0
- package/test/tui-wrap.test.js +568 -0
- package/test-binary.js +52 -0
- package/test-binary2.js +36 -0
- package/LICENSE +0 -21
- package/pave.js +0 -2
- package/sandbox/SandboxRunner.js +0 -1
- package/sandbox/pave-run.js +0 -2
- package/sandbox/permission.js +0 -1
- package/sandbox/utils/yaml.js +0 -1
|
@@ -0,0 +1,323 @@
|
|
|
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 };
|
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
});
|