@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,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Behavioral simulation tests for leading empty lines fix (Issue #191)
|
|
4
|
+
*
|
|
5
|
+
* These tests simulate the leading-newline stripping logic used in
|
|
6
|
+
* sendMessageAndStream() to verify correctness of the approach.
|
|
7
|
+
* They exercise a local reimplementation of the strip logic -- not
|
|
8
|
+
* the actual production code -- to validate expected behavior for
|
|
9
|
+
* various delta patterns (LF, CRLF, empty, multi-part).
|
|
10
|
+
*
|
|
11
|
+
* Run with: node test/chat-leading-lines.test.js
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
let failed = 0;
|
|
16
|
+
|
|
17
|
+
function runTest(name, testFn) {
|
|
18
|
+
try {
|
|
19
|
+
testFn();
|
|
20
|
+
console.log(`✅ ${name}`);
|
|
21
|
+
passed++;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.log(`❌ ${name}: ${error.message}`);
|
|
24
|
+
failed++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assertEqual(actual, expected, message) {
|
|
29
|
+
if (actual !== expected) {
|
|
30
|
+
throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================
|
|
35
|
+
// Behavioral verification
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Simulate the streaming output accumulation with the fix applied.
|
|
40
|
+
* This mirrors the logic in sendMessageAndStream() (issue #191).
|
|
41
|
+
* Uses /^[\r\n]+/ to handle both LF and CRLF.
|
|
42
|
+
*/
|
|
43
|
+
function simulateStreamOutput(deltas) {
|
|
44
|
+
let fullResponse = '';
|
|
45
|
+
const output = [];
|
|
46
|
+
|
|
47
|
+
for (const delta of deltas) {
|
|
48
|
+
let outputText = delta;
|
|
49
|
+
// Strip leading newlines from first output (issue #191)
|
|
50
|
+
if (fullResponse.length === 0) {
|
|
51
|
+
outputText = outputText.replace(/^[\r\n]+/, '');
|
|
52
|
+
}
|
|
53
|
+
if (outputText) {
|
|
54
|
+
output.push(outputText);
|
|
55
|
+
fullResponse += outputText;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { fullResponse, output };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Simulate the final render/JSON trim path.
|
|
64
|
+
*/
|
|
65
|
+
function simulateRenderTrim(fullResponse) {
|
|
66
|
+
return fullResponse.replace(/^[\r\n]+/, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Streaming delta tests ---
|
|
70
|
+
|
|
71
|
+
runTest('single delta with leading LF newlines: newlines stripped', () => {
|
|
72
|
+
const result = simulateStreamOutput(['\n\nHello world']);
|
|
73
|
+
assertEqual(result.fullResponse, 'Hello world', 'fullResponse');
|
|
74
|
+
assertEqual(result.output.length, 1, 'output chunk count');
|
|
75
|
+
assertEqual(result.output[0], 'Hello world', 'output chunk');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
runTest('single delta with leading CRLF: stripped', () => {
|
|
79
|
+
const result = simulateStreamOutput(['\r\n\r\nHello world']);
|
|
80
|
+
assertEqual(result.fullResponse, 'Hello world', 'fullResponse');
|
|
81
|
+
assertEqual(result.output[0], 'Hello world', 'output chunk');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
runTest('multiple deltas with leading newlines on first: only first stripped', () => {
|
|
85
|
+
const result = simulateStreamOutput(['\n\n', 'Hello', '\n\n', 'world']);
|
|
86
|
+
// First delta is all newlines -> stripped to empty -> skipped
|
|
87
|
+
// Second delta: fullResponse is still empty, no leading newlines -> added
|
|
88
|
+
assertEqual(result.fullResponse, 'Hello\n\nworld', 'fullResponse');
|
|
89
|
+
assertEqual(result.output.length, 3, 'output chunk count');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
runTest('delta without leading newlines: unchanged', () => {
|
|
93
|
+
const result = simulateStreamOutput(['Hello world']);
|
|
94
|
+
assertEqual(result.fullResponse, 'Hello world', 'fullResponse');
|
|
95
|
+
assertEqual(result.output[0], 'Hello world', 'output chunk');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
runTest('empty delta: skipped', () => {
|
|
99
|
+
const result = simulateStreamOutput(['', 'Hello']);
|
|
100
|
+
assertEqual(result.fullResponse, 'Hello', 'fullResponse');
|
|
101
|
+
assertEqual(result.output.length, 1, 'output chunk count');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
runTest('newlines after first content: preserved', () => {
|
|
105
|
+
const result = simulateStreamOutput(['Hello', '\n\n', 'World']);
|
|
106
|
+
assertEqual(result.fullResponse, 'Hello\n\nWorld', 'fullResponse');
|
|
107
|
+
assertEqual(result.output.length, 3, 'output chunk count');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
runTest('leading newlines only on first delta of multi-part: stripped', () => {
|
|
111
|
+
const result = simulateStreamOutput(['\nFirst line', '\nSecond part']);
|
|
112
|
+
assertEqual(result.fullResponse, 'First line\nSecond part', 'fullResponse');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
runTest('all newlines delta followed by text: newlines skipped, text preserved', () => {
|
|
116
|
+
const result = simulateStreamOutput(['\n\n\n', 'Content here']);
|
|
117
|
+
assertEqual(result.fullResponse, 'Content here', 'fullResponse');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
runTest('content with only newlines: results in empty output', () => {
|
|
121
|
+
const result = simulateStreamOutput(['\n\n\n']);
|
|
122
|
+
assertEqual(result.fullResponse, '', 'fullResponse should be empty');
|
|
123
|
+
assertEqual(result.output.length, 0, 'no output chunks');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
runTest('mixed CR and LF: all stripped from first delta', () => {
|
|
127
|
+
const result = simulateStreamOutput(['\r\n\n\r\nContent']);
|
|
128
|
+
assertEqual(result.fullResponse, 'Content', 'fullResponse');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// --- Final render/JSON trim tests ---
|
|
132
|
+
|
|
133
|
+
runTest('render trim: fullResponse with leading LF newlines trimmed', () => {
|
|
134
|
+
assertEqual(simulateRenderTrim('\n\nRendered content'), 'Rendered content', 'trimmed');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
runTest('render trim: fullResponse with leading CRLF trimmed', () => {
|
|
138
|
+
assertEqual(simulateRenderTrim('\r\n\r\nRendered content'), 'Rendered content', 'trimmed');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
runTest('render trim: no leading newlines unchanged', () => {
|
|
142
|
+
assertEqual(simulateRenderTrim('Already clean'), 'Already clean', 'unchanged');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
runTest('JSON output trim: response field has no leading newlines', () => {
|
|
146
|
+
const output = { response: simulateRenderTrim('\n\nJSON response content') };
|
|
147
|
+
assertEqual(output.response, 'JSON response content', 'JSON response');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
runTest('render trim: preserves internal newlines', () => {
|
|
151
|
+
assertEqual(simulateRenderTrim('\n\nLine 1\n\nLine 2'), 'Line 1\n\nLine 2', 'internal preserved');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ============================================================
|
|
155
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
156
|
+
|
|
157
|
+
if (failed > 0) {
|
|
158
|
+
throw new Error(`${failed} chat-leading-lines test(s) failed`);
|
|
159
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for --config flag effectiveness across all pave commands (issue #126).
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* 1. Legacy constants removed from skill-manager.js and marketplace.js
|
|
6
|
+
* 2. parseArgs() commandArgs doesn't leak --config values (behavioral)
|
|
7
|
+
* 3. setPaveHome correctly updates getPaths()/getCachePaths() (behavioral)
|
|
8
|
+
* 4. No hardcoded ~/.pave in token configuration message
|
|
9
|
+
*
|
|
10
|
+
* Uses checkmark/X format so run-all.js correctly counts results.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
|
|
16
|
+
function assert(cond, msg) {
|
|
17
|
+
if (cond) {
|
|
18
|
+
passed++;
|
|
19
|
+
console.log('\u2705 ' + msg);
|
|
20
|
+
} else {
|
|
21
|
+
failed++;
|
|
22
|
+
console.log('\u274c ' + msg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const child_process = require('child_process');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
|
|
31
|
+
const projDir = path.resolve(__dirname, '../../../..');
|
|
32
|
+
|
|
33
|
+
// Helper to run a script via temp file (sandbox blocks node -e)
|
|
34
|
+
function runScript(code) {
|
|
35
|
+
const tmp = path.join(projDir, '_test_tmp_' + Date.now() + '.js');
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(tmp, code, 'utf8');
|
|
38
|
+
const out = child_process.execFileSync(process.execPath, [tmp], {
|
|
39
|
+
cwd: projDir, encoding: 'utf8', timeout: 5000,
|
|
40
|
+
}).trim();
|
|
41
|
+
return JSON.parse(out);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Log the error so regressions aren't silently hidden
|
|
44
|
+
console.log('SKIP (subprocess error): ' + (e.stderr || e.message || '').slice(0, 120));
|
|
45
|
+
return null;
|
|
46
|
+
} finally {
|
|
47
|
+
try { fs.unlinkSync(tmp); } catch (e2) {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- 1. Behavioral test: parseArgs() commandArgs handling ----
|
|
52
|
+
function testParseArgs(argv) {
|
|
53
|
+
return runScript(
|
|
54
|
+
'var p = require("./src/packages/pave/lib/args-parser").parseArgs;\n' +
|
|
55
|
+
'var r = p(' + JSON.stringify(argv) + ');\n' +
|
|
56
|
+
'console.log(JSON.stringify({command:r.command,config:r.config,commandArgs:r.commandArgs}));\n',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const r1 = testParseArgs(['list', '--config', './.pave']);
|
|
61
|
+
if (r1) {
|
|
62
|
+
assert(r1.command === 'list', 'parseArgs: list command detected');
|
|
63
|
+
assert(r1.config === './.pave', 'parseArgs: --config value parsed');
|
|
64
|
+
assert(r1.commandArgs.indexOf('./.pave') === -1, 'parseArgs: ./.pave NOT in commandArgs');
|
|
65
|
+
assert(r1.commandArgs.length === 0, 'parseArgs: commandArgs empty for list --config');
|
|
66
|
+
} else {
|
|
67
|
+
console.log('SKIP: parseArgs test via subprocess not available');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tmpPave = path.join(os.tmpdir(), '.pave');
|
|
71
|
+
const r2 = testParseArgs(['install', 'gmail', '--config', tmpPave]);
|
|
72
|
+
if (r2) {
|
|
73
|
+
assert(r2.config === tmpPave, 'parseArgs: --config value parsed for install');
|
|
74
|
+
assert(r2.commandArgs.indexOf(tmpPave) === -1, 'parseArgs: config path NOT in install commandArgs');
|
|
75
|
+
assert(r2.commandArgs.length === 1 && r2.commandArgs[0] === 'gmail',
|
|
76
|
+
'parseArgs: install commandArgs = [gmail]');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const r3 = testParseArgs(['remove', 'my-skill', '--config', './.pave']);
|
|
80
|
+
if (r3) {
|
|
81
|
+
assert(r3.commandArgs.indexOf('./.pave') === -1, 'parseArgs: ./.pave NOT in remove commandArgs');
|
|
82
|
+
assert(r3.commandArgs[0] === 'my-skill', 'parseArgs: remove commandArgs = [my-skill]');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const r4 = testParseArgs(['search', 'query', '--category', 'tools', '--config', '/custom']);
|
|
86
|
+
if (r4) {
|
|
87
|
+
assert(r4.commandArgs.indexOf('/custom') === -1, 'parseArgs: /custom NOT in search commandArgs');
|
|
88
|
+
assert(r4.commandArgs.indexOf('tools') === -1, 'parseArgs: --category value NOT in commandArgs');
|
|
89
|
+
assert(r4.commandArgs[0] === 'query', 'parseArgs: search commandArgs = [query]');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const r5 = testParseArgs(['update', '--config', './.pave']);
|
|
93
|
+
if (r5) {
|
|
94
|
+
assert(r5.commandArgs.length === 0, 'parseArgs: update commandArgs empty');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Test flags Copilot specifically requested coverage for
|
|
98
|
+
const r6 = testParseArgs(['list', '--bind', '0.0.0.0', '--config', './.pave']);
|
|
99
|
+
if (r6) {
|
|
100
|
+
assert(r6.commandArgs.indexOf('0.0.0.0') === -1, 'parseArgs: --bind value NOT in commandArgs');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const r7 = testParseArgs(['list', '-b', '0.0.0.0']);
|
|
104
|
+
if (r7) {
|
|
105
|
+
assert(r7.commandArgs.indexOf('0.0.0.0') === -1, 'parseArgs: -b value NOT in commandArgs');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const r8 = testParseArgs(['list', '--shell', '/bin/bash']);
|
|
109
|
+
if (r8) {
|
|
110
|
+
assert(r8.commandArgs.indexOf('/bin/bash') === -1, 'parseArgs: --shell value NOT in commandArgs');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const r9 = testParseArgs(['list', '--last', '5']);
|
|
114
|
+
if (r9) {
|
|
115
|
+
assert(r9.commandArgs.indexOf('5') === -1, 'parseArgs: --last value NOT in commandArgs');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const r10 = testParseArgs(['list', '--export', 'out.md']);
|
|
119
|
+
if (r10) {
|
|
120
|
+
assert(r10.commandArgs.indexOf('out.md') === -1, 'parseArgs: --export value NOT in commandArgs');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const r11 = testParseArgs(['install', 'a', 'b', '--config', './.pave']);
|
|
124
|
+
if (r11) {
|
|
125
|
+
assert(r11.commandArgs.length === 2, 'parseArgs: install a b -> 2 commandArgs');
|
|
126
|
+
assert(r11.commandArgs[0] === 'a' && r11.commandArgs[1] === 'b',
|
|
127
|
+
'parseArgs: install commandArgs = [a, b]');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Optional-value flags: --ish-mode and --memory-monitor should not leak values
|
|
131
|
+
const r13 = testParseArgs(['list', '--ish-mode', 'true']);
|
|
132
|
+
if (r13) {
|
|
133
|
+
assert(r13.commandArgs.indexOf('true') === -1, 'parseArgs: --ish-mode value NOT in commandArgs');
|
|
134
|
+
assert(r13.commandArgs.length === 0, 'parseArgs: list --ish-mode true -> empty commandArgs');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const r14 = testParseArgs(['list', '--memory-monitor', 'off']);
|
|
138
|
+
if (r14) {
|
|
139
|
+
assert(r14.commandArgs.indexOf('off') === -1, 'parseArgs: --memory-monitor value NOT in commandArgs');
|
|
140
|
+
assert(r14.commandArgs.length === 0, 'parseArgs: list --memory-monitor off -> empty commandArgs');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --ish-mode without a recognized value should not consume next token
|
|
144
|
+
const r15 = testParseArgs(['install', '--ish-mode', 'gmail']);
|
|
145
|
+
if (r15) {
|
|
146
|
+
assert(r15.commandArgs.length === 1 && r15.commandArgs[0] === 'gmail',
|
|
147
|
+
'parseArgs: --ish-mode without value -> gmail stays in commandArgs');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --config at end (missing value) - must run in subprocess since parseArgs calls process.exit(1)
|
|
151
|
+
const configMissingScript = [
|
|
152
|
+
'var p = require("./src/packages/pave/lib/args-parser").parseArgs;',
|
|
153
|
+
'p(["list", "--config"]);',
|
|
154
|
+
'console.log(JSON.stringify({ok:true}));',
|
|
155
|
+
].join('\n');
|
|
156
|
+
const tmpMissing = path.join(projDir, '_test_tmp_missing_' + Date.now() + '.js');
|
|
157
|
+
try {
|
|
158
|
+
fs.writeFileSync(tmpMissing, configMissingScript, 'utf8');
|
|
159
|
+
const child = child_process.spawnSync(process.execPath, [tmpMissing], {
|
|
160
|
+
cwd: projDir, encoding: 'utf8', timeout: 5000, stdio: 'pipe',
|
|
161
|
+
});
|
|
162
|
+
assert(child.status !== 0, 'parseArgs: --config at end -> non-zero exit');
|
|
163
|
+
// When running in a real Node.js environment (not sandbox), verify error message
|
|
164
|
+
const stderr = child.stderr || '';
|
|
165
|
+
if (stderr.indexOf('--config') !== -1) {
|
|
166
|
+
assert(true, 'parseArgs: --config at end -> error mentions --config');
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
assert(false, 'parseArgs: --config at end -> subprocess failed: ' + e.message);
|
|
170
|
+
} finally {
|
|
171
|
+
try { fs.unlinkSync(tmpMissing); } catch (e2) {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- 2. Behavioral test: setPaveHome / getPaths ----
|
|
175
|
+
const ph = runScript([
|
|
176
|
+
'var os = require("os");',
|
|
177
|
+
'var path = require("path");',
|
|
178
|
+
'var sm = require("./src/packages/pave/lib/skill-manager");',
|
|
179
|
+
'var mp = require("./src/packages/pave/lib/marketplace");',
|
|
180
|
+
'var testDir = path.join(os.tmpdir(), "test-pave-cfg");',
|
|
181
|
+
'sm.setPaveHome(testDir);',
|
|
182
|
+
'mp.setPaveHome(testDir);',
|
|
183
|
+
'var p = sm.getPaths();',
|
|
184
|
+
'var c = mp.getCachePaths();',
|
|
185
|
+
'console.log(JSON.stringify({',
|
|
186
|
+
' skillsDir: p.skillsDir,',
|
|
187
|
+
' lockFile: p.lockFile,',
|
|
188
|
+
' permissionsFile: p.permissionsFile,',
|
|
189
|
+
' cacheDir: c.cacheDir,',
|
|
190
|
+
' cacheFile: c.cacheFile,',
|
|
191
|
+
' testDir: testDir,',
|
|
192
|
+
' hasSKILLS_DIR: typeof sm.SKILLS_DIR !== "undefined",',
|
|
193
|
+
' hasLOCK_FILE: typeof sm.LOCK_FILE !== "undefined",',
|
|
194
|
+
' hasCACHE_DIR: typeof mp.CACHE_DIR !== "undefined",',
|
|
195
|
+
' hasCACHE_FILE: typeof mp.CACHE_FILE !== "undefined",',
|
|
196
|
+
' skillsDirIsDynamic: sm.SKILLS_DIR === sm.getPaths().skillsDir,',
|
|
197
|
+
' lockFileIsDynamic: sm.LOCK_FILE === sm.getPaths().lockFile,',
|
|
198
|
+
' cacheDirIsDynamic: mp.CACHE_DIR === mp.getCachePaths().cacheDir,',
|
|
199
|
+
' cacheFileIsDynamic: mp.CACHE_FILE === mp.getCachePaths().cacheFile',
|
|
200
|
+
'}));',
|
|
201
|
+
].join('\n'));
|
|
202
|
+
|
|
203
|
+
if (ph) {
|
|
204
|
+
assert(ph.skillsDir.indexOf(ph.testDir) !== -1,
|
|
205
|
+
'setPaveHome: skillsDir uses custom path');
|
|
206
|
+
assert(ph.lockFile.indexOf(ph.testDir) !== -1,
|
|
207
|
+
'setPaveHome: lockFile uses custom path');
|
|
208
|
+
assert(ph.permissionsFile.indexOf(ph.testDir) !== -1,
|
|
209
|
+
'setPaveHome: permissionsFile uses custom path');
|
|
210
|
+
assert(ph.cacheDir.indexOf(ph.testDir) !== -1,
|
|
211
|
+
'setPaveHome: cacheDir uses custom path');
|
|
212
|
+
assert(ph.cacheFile.indexOf(ph.testDir) !== -1,
|
|
213
|
+
'setPaveHome: cacheFile uses custom path');
|
|
214
|
+
assert(ph.hasSKILLS_DIR === true, 'export: SKILLS_DIR available as deprecated getter');
|
|
215
|
+
assert(ph.hasLOCK_FILE === true, 'export: LOCK_FILE available as deprecated getter');
|
|
216
|
+
assert(ph.hasCACHE_DIR === true, 'export: CACHE_DIR available as deprecated getter');
|
|
217
|
+
assert(ph.hasCACHE_FILE === true, 'export: CACHE_FILE available as deprecated getter');
|
|
218
|
+
assert(ph.skillsDirIsDynamic === true, 'export: SKILLS_DIR delegates to getPaths()');
|
|
219
|
+
assert(ph.lockFileIsDynamic === true, 'export: LOCK_FILE delegates to getPaths()');
|
|
220
|
+
assert(ph.cacheDirIsDynamic === true, 'export: CACHE_DIR delegates to getCachePaths()');
|
|
221
|
+
assert(ph.cacheFileIsDynamic === true, 'export: CACHE_FILE delegates to getCachePaths()');
|
|
222
|
+
} else {
|
|
223
|
+
console.log('SKIP: setPaveHome subprocess not available');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- 3. Source check: no hardcoded ~/.pave in token message ----
|
|
227
|
+
const ixSrc = fs.readFileSync(path.resolve(__dirname, '../index.js'), 'utf8');
|
|
228
|
+
assert(!/Configure tokens in ~\/.pave/.test(ixSrc),
|
|
229
|
+
'index.js: no hardcoded ~/.pave in token message');
|
|
230
|
+
assert(/getPaveHome\(\).*tokens\.yaml/.test(ixSrc),
|
|
231
|
+
'index.js: getPaveHome() used for tokens path');
|
|
232
|
+
|
|
233
|
+
// ---- 4. Source check: legacy constants removed ----
|
|
234
|
+
const smSrc = fs.readFileSync(path.resolve(__dirname, '../lib/skill-manager.js'), 'utf8');
|
|
235
|
+
const mpSrc = fs.readFileSync(path.resolve(__dirname, '../lib/marketplace.js'), 'utf8');
|
|
236
|
+
|
|
237
|
+
assert(!/^const SKILLS_DIR\b/m.test(smSrc),
|
|
238
|
+
'source: no hardcoded SKILLS_DIR constant in skill-manager.js');
|
|
239
|
+
assert(!/^const LOCK_FILE\b/m.test(smSrc),
|
|
240
|
+
'source: no hardcoded LOCK_FILE constant in skill-manager.js');
|
|
241
|
+
assert(!/^const CACHE_DIR\b/m.test(mpSrc),
|
|
242
|
+
'source: no hardcoded CACHE_DIR constant in marketplace.js');
|
|
243
|
+
assert(!/^const CACHE_FILE\b/m.test(mpSrc),
|
|
244
|
+
'source: no hardcoded CACHE_FILE constant in marketplace.js');
|
|
245
|
+
assert(!/^const CACHE_META_FILE\b/m.test(mpSrc),
|
|
246
|
+
'source: no hardcoded CACHE_META_FILE constant in marketplace.js');
|
|
247
|
+
// Verify deprecated getters delegate to getPaths()/getCachePaths()
|
|
248
|
+
assert(/get SKILLS_DIR/.test(smSrc),
|
|
249
|
+
'source: SKILLS_DIR is a deprecated getter in skill-manager.js');
|
|
250
|
+
assert(/get LOCK_FILE/.test(smSrc),
|
|
251
|
+
'source: LOCK_FILE is a deprecated getter in skill-manager.js');
|
|
252
|
+
assert(/get CACHE_DIR/.test(mpSrc),
|
|
253
|
+
'source: CACHE_DIR is a deprecated getter in marketplace.js');
|
|
254
|
+
assert(/get CACHE_FILE/.test(mpSrc),
|
|
255
|
+
'source: CACHE_FILE is a deprecated getter in marketplace.js');
|
|
256
|
+
|
|
257
|
+
// ---- 5. Source check: flagsWithValues completeness ----
|
|
258
|
+
const apSrc = fs.readFileSync(path.resolve(__dirname, '../lib/args-parser.js'), 'utf8');
|
|
259
|
+
assert(/FLAGS_WITH_VALUES/.test(apSrc), 'args-parser: FLAGS_WITH_VALUES set exists at module scope');
|
|
260
|
+
assert(/'--bind'/.test(apSrc) && /'-b'/.test(apSrc), 'args-parser: --bind/-b in FLAGS_WITH_VALUES');
|
|
261
|
+
assert(/'--shell'/.test(apSrc), 'args-parser: --shell in FLAGS_WITH_VALUES');
|
|
262
|
+
assert(/'--last'/.test(apSrc), 'args-parser: --last in FLAGS_WITH_VALUES');
|
|
263
|
+
assert(/'--export'/.test(apSrc), 'args-parser: --export in FLAGS_WITH_VALUES');
|
|
264
|
+
assert(/OPTIONAL_VALUE_FLAGS/.test(apSrc), 'args-parser: OPTIONAL_VALUE_FLAGS set exists');
|
|
265
|
+
assert(/'--ish-mode'/.test(apSrc), 'args-parser: --ish-mode in OPTIONAL_VALUE_FLAGS');
|
|
266
|
+
assert(/'--memory-monitor'/.test(apSrc), 'args-parser: --memory-monitor in OPTIONAL_VALUE_FLAGS');
|
|
267
|
+
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log(passed + ' passed, ' + failed + ' failed');
|
|
270
|
+
if (failed > 0) throw new Error(failed + ' test(s) failed');
|
|
271
|
+
|
|
272
|
+
// trigger
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for chatbar cursor drift fix (issue #112).
|
|
3
|
+
*
|
|
4
|
+
* Cursor math simulation verifying that wrapping at wrapWidth (widgetWidth-1)
|
|
5
|
+
* matching blessed's textarea margin eliminates cursor drift.
|
|
6
|
+
*
|
|
7
|
+
* Blessed's _wrapContent only wraps when line.length > width (not >=),
|
|
8
|
+
* so at exact multiples of wrapWidth the cursor stays on the same line
|
|
9
|
+
* at col === wrapWidth (the textarea margin column).
|
|
10
|
+
*
|
|
11
|
+
* Uses checkmark/X format so run-all.js correctly counts results.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
let failed = 0;
|
|
16
|
+
|
|
17
|
+
function assert(cond, msg) {
|
|
18
|
+
if (cond) {
|
|
19
|
+
passed++;
|
|
20
|
+
console.log('\u2705 ' + msg);
|
|
21
|
+
} else {
|
|
22
|
+
failed++;
|
|
23
|
+
console.log('\u274c ' + msg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---- Cursor math simulation ----
|
|
28
|
+
// Blessed wraps when length > width, NOT when length >= width.
|
|
29
|
+
// So at exact multiples, cursor stays at col=wrapWidth on previous line.
|
|
30
|
+
function simWrap(textLen, wrapWidth) {
|
|
31
|
+
if (textLen <= 0) return { wraps: 0, col: 0 };
|
|
32
|
+
const wraps = Math.floor((textLen - 1) / wrapWidth);
|
|
33
|
+
const col = ((textLen - 1) % wrapWidth) + 1;
|
|
34
|
+
return { wraps, col };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Simulate a widget with raw width 42 (xl-xi=42, iwidth=2 -> widgetWidth=40)
|
|
38
|
+
// wrapWidth = widgetWidth - 1 = 39 (matches blessed textarea margin)
|
|
39
|
+
const widgetWidth = 40;
|
|
40
|
+
const wrapWidth = widgetWidth - 1; // 39
|
|
41
|
+
|
|
42
|
+
// Page 1: within first page
|
|
43
|
+
const r20 = simWrap(20, wrapWidth);
|
|
44
|
+
assert(r20.col === 20 && r20.wraps === 0, 'page 1: col 20 at 20 chars');
|
|
45
|
+
|
|
46
|
+
// Right at wrapWidth boundary (39 chars) blessed does NOT wrap here
|
|
47
|
+
const r39 = simWrap(39, wrapWidth);
|
|
48
|
+
assert(r39.col === 39 && r39.wraps === 0, 'at wrapWidth: col 39, no wrap (blessed textarea margin)');
|
|
49
|
+
|
|
50
|
+
// One past wrapWidth (40 chars) blessed wraps here
|
|
51
|
+
const r40 = simWrap(40, wrapWidth);
|
|
52
|
+
assert(r40.col === 1 && r40.wraps === 1, 'past wrapWidth: col 1, wraps to line 2');
|
|
53
|
+
|
|
54
|
+
// At 2*wrapWidth (78 chars) exact multiple, no extra wrap
|
|
55
|
+
const r78 = simWrap(78, wrapWidth);
|
|
56
|
+
assert(r78.col === 39 && r78.wraps === 1, 'at 2*wrapWidth: col 39, stays on line 2');
|
|
57
|
+
|
|
58
|
+
// At 2*wrapWidth + 1 (79 chars) wraps
|
|
59
|
+
const r79 = simWrap(79, wrapWidth);
|
|
60
|
+
assert(r79.col === 1 && r79.wraps === 2, 'past 2*wrapWidth: col 1, wraps to line 3');
|
|
61
|
+
|
|
62
|
+
// 80 chars
|
|
63
|
+
const r80 = simWrap(80, wrapWidth);
|
|
64
|
+
assert(r80.col === 2 && r80.wraps === 2, '80 chars: col 2, 2 wraps');
|
|
65
|
+
|
|
66
|
+
// ---- Verify old code drifts but new code does not ----
|
|
67
|
+
// Old code: wraps at widgetWidth (40), uses Math.floor(len/40)
|
|
68
|
+
function simOld(textLen, w) {
|
|
69
|
+
return { wraps: Math.floor(textLen / w), col: textLen % w };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const oldAt39 = simOld(39, 40);
|
|
73
|
+
const newAt39 = simWrap(39, 39);
|
|
74
|
+
assert(newAt39.wraps === 0 && newAt39.col === 39,
|
|
75
|
+
'new code: 39 chars stays on line 1 at col 39');
|
|
76
|
+
assert(oldAt39.wraps === 0 && oldAt39.col === 39,
|
|
77
|
+
'old code: 39 chars also on line 1 (no drift yet)');
|
|
78
|
+
|
|
79
|
+
const oldAt78 = simOld(78, 40);
|
|
80
|
+
const newAt78 = simWrap(78, 39);
|
|
81
|
+
assert(newAt78.wraps === 1 && newAt78.col === 39,
|
|
82
|
+
'new code: 78 chars on line 2, col 39');
|
|
83
|
+
assert(oldAt78.wraps === 1 && oldAt78.col === 38,
|
|
84
|
+
'old code: 78 chars on line 2, col 38 (drift of 1)');
|
|
85
|
+
|
|
86
|
+
// ---- Edge cases ----
|
|
87
|
+
// Empty text
|
|
88
|
+
const empty = simWrap(0, 39);
|
|
89
|
+
assert(empty.col === 0 && empty.wraps === 0, 'empty text: col 0, no wraps');
|
|
90
|
+
|
|
91
|
+
// Exactly 1 char
|
|
92
|
+
const one = simWrap(1, 39);
|
|
93
|
+
assert(one.col === 1 && one.wraps === 0, '1 char: col 1, no wraps');
|
|
94
|
+
|
|
95
|
+
// Width of 1 (minimum after guard)
|
|
96
|
+
const narrow = simWrap(5, 1);
|
|
97
|
+
assert(narrow.wraps === 4 && narrow.col === 1, 'width=1: 5 chars -> 4 wraps, col 1');
|
|
98
|
+
|
|
99
|
+
// Very long text
|
|
100
|
+
const longText = simWrap(1000, 39);
|
|
101
|
+
assert(longText.wraps === 25 && longText.col === 25, '1000 chars at width 39: 25 wraps, col 25');
|
|
102
|
+
|
|
103
|
+
// ---- wrapWidth guard: ensure wrapWidth < 1 is clamped to 1 ----
|
|
104
|
+
function simClampedWrapWidth(widgetW) {
|
|
105
|
+
let ww = widgetW - 1;
|
|
106
|
+
if (ww < 1) ww = 1;
|
|
107
|
+
return ww;
|
|
108
|
+
}
|
|
109
|
+
assert(simClampedWrapWidth(1) === 1, 'wrapWidth guard: widgetWidth=1 -> wrapWidth=1');
|
|
110
|
+
assert(simClampedWrapWidth(0) === 1, 'wrapWidth guard: widgetWidth=0 -> wrapWidth=1');
|
|
111
|
+
assert(simClampedWrapWidth(2) === 1, 'wrapWidth guard: widgetWidth=2 -> wrapWidth=1');
|
|
112
|
+
assert(simClampedWrapWidth(40) === 39, 'wrapWidth guard: widgetWidth=40 -> wrapWidth=39');
|
|
113
|
+
|
|
114
|
+
// ---- Cumulative drift test across many wrap boundaries ----
|
|
115
|
+
// With blessed's behavior, at exact multiples of wrapWidth the cursor
|
|
116
|
+
// stays at col=wrapWidth on the previous visual line, NOT at col=0
|
|
117
|
+
// on the next line. This is the regression test for issue #112.
|
|
118
|
+
for (let p = 1; p <= 20; p++) {
|
|
119
|
+
const chars = p * 39;
|
|
120
|
+
const result = simWrap(chars, 39);
|
|
121
|
+
assert(result.col === 39 && result.wraps === p - 1,
|
|
122
|
+
'no cumulative drift at boundary ' + p + ' (' + chars + ' chars): col=39, wraps=' + (p - 1));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- Wrap only happens when EXCEEDING wrapWidth ----
|
|
126
|
+
for (let p = 1; p <= 10; p++) {
|
|
127
|
+
const chars = p * 39 + 1; // one past boundary
|
|
128
|
+
const result = simWrap(chars, 39);
|
|
129
|
+
assert(result.col === 1 && result.wraps === p,
|
|
130
|
+
'wrap at boundary+1 ' + p + ' (' + chars + ' chars): col=1, wraps=' + p);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(passed + ' passed, ' + failed + ' failed');
|
|
135
|
+
if (failed > 0) throw new Error(failed + ' test(s) failed');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Debug script to test require paths
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
console.log('Current directory:', __dirname);
|
|
6
|
+
console.log('Test files:', fs.readdirSync(__dirname).filter((f) => f.endsWith('.test.js')));
|
|
7
|
+
|
|
8
|
+
const testFile = path.join(__dirname, 'integration.test.js');
|
|
9
|
+
console.log('\nTest file path:', testFile);
|
|
10
|
+
console.log('File exists:', fs.existsSync(testFile));
|
|
11
|
+
|
|
12
|
+
// Try to require the args-parser
|
|
13
|
+
const argsParserPath = path.join(__dirname, '../lib/args-parser.js');
|
|
14
|
+
console.log('\nArgs-parser path:', argsParserPath);
|
|
15
|
+
console.log('Args-parser exists:', fs.existsSync(argsParserPath));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const argsParser = require(argsParserPath);
|
|
19
|
+
console.log('Args-parser loaded successfully');
|
|
20
|
+
console.log('Exports:', Object.keys(argsParser));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Failed to load args-parser:', error.message);
|
|
23
|
+
}
|