@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,1255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for TUI attach mode (Issue #202)
|
|
4
|
+
* Phase 5: pave -a <agent> attached TUI
|
|
5
|
+
*
|
|
6
|
+
* Tests cover:
|
|
7
|
+
* - Agent registry getAgentServerUrl helper
|
|
8
|
+
* - Entry point routing (source inspection)
|
|
9
|
+
* - attached.js module structure (source inspection)
|
|
10
|
+
* - Args parser routing for -a without command
|
|
11
|
+
*
|
|
12
|
+
* Designed to run standalone: node test/tui-attach.test.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const registryPath = path.join(__dirname, '..', 'lib', 'agent-registry.js');
|
|
20
|
+
const registry = require(registryPath);
|
|
21
|
+
const { parseArgs } = require(path.join(__dirname, '..', 'lib', 'args-parser.js'));
|
|
22
|
+
|
|
23
|
+
const testDir = path.join(os.tmpdir(), 'pave-test-attach-' + process.pid);
|
|
24
|
+
|
|
25
|
+
let passed = 0;
|
|
26
|
+
let failed = 0;
|
|
27
|
+
|
|
28
|
+
function assert(condition, msg) {
|
|
29
|
+
if (!condition) throw new Error('Assertion failed: ' + msg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runTest(name, fn) {
|
|
33
|
+
try {
|
|
34
|
+
fn();
|
|
35
|
+
console.log('\u2705 ' + name);
|
|
36
|
+
passed++;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.log('\u274C ' + name + ': ' + e.message);
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetTestDir() {
|
|
44
|
+
try { fs.rmSync(testDir, { recursive: true }); } catch (e) {}
|
|
45
|
+
registry.setAgentsDir(testDir);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cleanup() {
|
|
49
|
+
try { fs.rmSync(testDir, { recursive: true }); } catch (e) {}
|
|
50
|
+
registry.resetAgentsDir();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// getAgentServerUrl
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
resetTestDir();
|
|
58
|
+
|
|
59
|
+
runTest('getAgentServerUrl: returns null for nonexistent agent', () => {
|
|
60
|
+
resetTestDir();
|
|
61
|
+
const url = registry.getAgentServerUrl('nonexistent');
|
|
62
|
+
assert(url === null, 'should return null');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
runTest('getAgentServerUrl: returns null when agent has no serverUrl', () => {
|
|
66
|
+
resetTestDir();
|
|
67
|
+
registry.writeStatus('test-agent', {
|
|
68
|
+
state: 'working',
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
sessionId: 'sess-1',
|
|
71
|
+
iteration: 1,
|
|
72
|
+
startedAt: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
const url = registry.getAgentServerUrl('test-agent');
|
|
75
|
+
assert(url === null, 'should return null when no serverUrl');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
runTest('getAgentServerUrl: returns serverUrl when agent is alive', () => {
|
|
79
|
+
resetTestDir();
|
|
80
|
+
registry.writeStatus('test-agent', {
|
|
81
|
+
state: 'working',
|
|
82
|
+
pid: process.pid, // Current process is alive
|
|
83
|
+
serverUrl: 'http://localhost:4096',
|
|
84
|
+
sessionId: 'sess-1',
|
|
85
|
+
iteration: 1,
|
|
86
|
+
startedAt: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
const url = registry.getAgentServerUrl('test-agent');
|
|
89
|
+
assert(url === 'http://localhost:4096', 'should return serverUrl: ' + url);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
runTest('getAgentServerUrl: returns null when agent is stopped', () => {
|
|
93
|
+
resetTestDir();
|
|
94
|
+
registry.writeStatus('test-agent', {
|
|
95
|
+
state: 'stopped',
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
serverUrl: 'http://localhost:4096',
|
|
98
|
+
sessionId: 'sess-1',
|
|
99
|
+
iteration: 5,
|
|
100
|
+
startedAt: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
const url = registry.getAgentServerUrl('test-agent');
|
|
103
|
+
assert(url === null, 'should return null when agent is stopped');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
runTest('getAgentServerUrl: returns null when agent PID is dead', () => {
|
|
107
|
+
resetTestDir();
|
|
108
|
+
registry.writeStatus('test-agent', {
|
|
109
|
+
state: 'working',
|
|
110
|
+
pid: 999999999, // Almost certainly not a real PID
|
|
111
|
+
serverUrl: 'http://localhost:4096',
|
|
112
|
+
sessionId: 'sess-1',
|
|
113
|
+
iteration: 1,
|
|
114
|
+
startedAt: Date.now(),
|
|
115
|
+
});
|
|
116
|
+
const url = registry.getAgentServerUrl('test-agent');
|
|
117
|
+
assert(url === null, 'should return null when PID is dead');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// Status.json includes serverUrl (source inspection)
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
runTest('agent loop writes serverUrl to status.json', () => {
|
|
125
|
+
const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
126
|
+
// Count occurrences of serverUrl in writeStatus calls (shorthand or explicit)
|
|
127
|
+
const matches = source.match(/\bserverUrl[,\s]/g) || [];
|
|
128
|
+
assert(matches.length >= 3, 'should write serverUrl in at least 3 writeStatus calls, found ' + matches.length);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
runTest('serverUrl declared in outer scope for cleanup access', () => {
|
|
132
|
+
const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
133
|
+
// serverUrl should be declared as let in the outer handleAgentCommand scope
|
|
134
|
+
assert(source.indexOf('let serverUrl = null;') !== -1, 'serverUrl should be declared as let in outer scope');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// Entry point routing (source inspection)
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
runTest('pave/index.js routes -a to runTUIAttached', () => {
|
|
142
|
+
const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
143
|
+
assert(source.indexOf('runTUIAttached') !== -1, 'should reference runTUIAttached');
|
|
144
|
+
assert(source.indexOf("require('../tui/attached.js')") !== -1 ||
|
|
145
|
+
source.indexOf("require('../tui/attached')") !== -1,
|
|
146
|
+
'should require tui/attached.js');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
runTest('entry point checks args.agent before routing', () => {
|
|
150
|
+
const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
151
|
+
// The routing code should check args.agent
|
|
152
|
+
const routeIdx = source.indexOf('args.agent');
|
|
153
|
+
const tuiIdx = source.indexOf('runTUIAttached');
|
|
154
|
+
assert(routeIdx < tuiIdx, 'args.agent check should come before runTUIAttached call');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ============================================================
|
|
158
|
+
// attached.js module structure
|
|
159
|
+
// ============================================================
|
|
160
|
+
|
|
161
|
+
runTest('tui/attached.js exists', () => {
|
|
162
|
+
const attachedPath = path.join(__dirname, '..', '..', 'tui', 'attached.js');
|
|
163
|
+
assert(fs.existsSync(attachedPath), 'attached.js should exist at: ' + attachedPath);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
runTest('tui/attached.js exports runTUIAttached', () => {
|
|
167
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
168
|
+
assert(source.indexOf('runTUIAttached') !== -1, 'should define runTUIAttached');
|
|
169
|
+
assert(source.indexOf('module.exports') !== -1, 'should have module.exports');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
runTest('attached.js uses blessed for TUI', () => {
|
|
173
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
174
|
+
assert(source.indexOf("require('blessed')") !== -1, 'should require blessed');
|
|
175
|
+
assert(source.indexOf('blessed.screen') !== -1, 'should create blessed screen');
|
|
176
|
+
assert(source.indexOf('blessed.box') !== -1, 'should create blessed boxes');
|
|
177
|
+
assert(source.indexOf('blessed.textarea') !== -1, 'should create blessed textarea for multi-line input');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
runTest('attached.js connects to SSE for real-time streaming', () => {
|
|
181
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
182
|
+
assert(source.indexOf('/global/event') !== -1 || source.indexOf('event?sessionID') !== -1,
|
|
183
|
+
'should connect to SSE endpoint');
|
|
184
|
+
assert(source.indexOf('text/event-stream') !== -1, 'should request text/event-stream');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
runTest('attached.js handles opencode-lite SSE events', () => {
|
|
188
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
189
|
+
assert(source.indexOf('message.part.delta') !== -1, 'should handle message.part.delta');
|
|
190
|
+
assert(source.indexOf('message.part.updated') !== -1, 'should handle message.part.updated');
|
|
191
|
+
assert(source.indexOf('message.updated') !== -1, 'should handle message.updated');
|
|
192
|
+
assert(source.indexOf('session.status') !== -1, 'should handle session.status');
|
|
193
|
+
assert(source.indexOf('session.error') !== -1, 'should handle session.error');
|
|
194
|
+
// Also keeps legacy events for backward compat
|
|
195
|
+
assert(source.indexOf('text.delta') !== -1, 'should handle legacy text.delta');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
runTest('attached.js uses correct server endpoints', () => {
|
|
199
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
200
|
+
// History: GET /session/<id>/message
|
|
201
|
+
assert(source.indexOf("/session/' + currentSessionId + '/message'") !== -1 ||
|
|
202
|
+
source.indexOf("/session/' + currentSessionId + '/message") !== -1,
|
|
203
|
+
'should use /session/<id>/message for history');
|
|
204
|
+
// Abort: POST /session/<id>/abort
|
|
205
|
+
assert(source.indexOf("/session/' + currentSessionId + '/abort'") !== -1,
|
|
206
|
+
'should use /session/<id>/abort for interrupt');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
runTest('attached.js routes Enter to inbox', () => {
|
|
210
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
211
|
+
assert(source.indexOf('writeInboxMessage') !== -1, 'should call writeInboxMessage for normal messages');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
runTest('attached.js routes Ctrl+U to interrupt', () => {
|
|
215
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
216
|
+
assert(source.indexOf('writeInterrupt') !== -1, 'should call writeInterrupt for Ctrl+U');
|
|
217
|
+
assert(source.indexOf('C-u') !== -1, 'should bind Ctrl+U key');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
runTest('attached.js Ctrl+C detaches without killing agent', () => {
|
|
221
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
222
|
+
assert(source.indexOf('C-c') !== -1, 'should bind Ctrl+C');
|
|
223
|
+
assert(source.indexOf('detach') !== -1, 'should call detach function');
|
|
224
|
+
// Should NOT contain process.kill or SIGTERM for the agent
|
|
225
|
+
assert(source.indexOf('process.kill') === -1, 'should NOT kill agent process');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
runTest('attached.js polls status.json for updates', () => {
|
|
229
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
230
|
+
assert(source.indexOf('STATUS_POLL_INTERVAL') !== -1, 'should define STATUS_POLL_INTERVAL');
|
|
231
|
+
assert(source.indexOf('setInterval') !== -1, 'should use setInterval for polling');
|
|
232
|
+
assert(source.indexOf('readStatus') !== -1, 'should read status.json');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
runTest('attached.js has SSE reconnection with exponential backoff', () => {
|
|
236
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
237
|
+
assert(source.indexOf('SSE_INITIAL_BACKOFF') !== -1, 'should define SSE_INITIAL_BACKOFF');
|
|
238
|
+
assert(source.indexOf('SSE_MAX_BACKOFF') !== -1, 'should define SSE_MAX_BACKOFF');
|
|
239
|
+
assert(source.indexOf('sseBackoff * 2') !== -1 || source.indexOf('sseBackoff*2') !== -1,
|
|
240
|
+
'should double backoff');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
runTest('attached.js cleans up sockets on SSE non-200', () => {
|
|
244
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
245
|
+
// Non-200 handler should call res.resume(), res.destroy(), req.destroy()
|
|
246
|
+
assert(source.indexOf('res.resume()') !== -1, 'should call res.resume() on non-200');
|
|
247
|
+
assert(source.indexOf('res.destroy()') !== -1, 'should call res.destroy() on non-200');
|
|
248
|
+
assert(source.indexOf('req.destroy()') !== -1, 'should call req.destroy() on non-200');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
runTest('attached.js detects session compaction', () => {
|
|
252
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
253
|
+
assert(source.indexOf('compacted') !== -1 || source.indexOf('Session compacted') !== -1,
|
|
254
|
+
'should handle session compaction');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
runTest('attached.js detects agent stop', () => {
|
|
258
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
259
|
+
assert(source.indexOf('Agent has stopped') !== -1 || source.indexOf('has stopped') !== -1,
|
|
260
|
+
'should detect and display when agent stops');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
runTest('attached.js never POSTs to /session/:id/message', () => {
|
|
264
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
265
|
+
// The attached TUI should NEVER post messages to the session directly
|
|
266
|
+
// It should only use inbox/interrupt (and GET /session/:id/message for history)
|
|
267
|
+
// Specifically, there must be no httpPost call constructing /session/:id/message
|
|
268
|
+
const postSessionMessagePattern = /httpPost\s*\([^)]*['"`]\/session\/['"`]\s*\+\s*[^)]*['"`]\/message['"`]/;
|
|
269
|
+
assert(!postSessionMessagePattern.test(source),
|
|
270
|
+
'should not POST to /session/:id/message');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ============================================================
|
|
274
|
+
// Args parser: -a without command
|
|
275
|
+
// ============================================================
|
|
276
|
+
|
|
277
|
+
runTest('parseArgs: -a without command sets agent', () => {
|
|
278
|
+
const result = parseArgs(['-a', 'my-agent']);
|
|
279
|
+
assert(result.agent === 'my-agent', 'agent should be my-agent: ' + result.agent);
|
|
280
|
+
assert(!result.command || result.command === null, 'command should be null');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
runTest('parseArgs: --agent without command sets agent', () => {
|
|
284
|
+
const result = parseArgs(['--agent', 'test-bot']);
|
|
285
|
+
assert(result.agent === 'test-bot', 'agent should be test-bot');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ============================================================
|
|
289
|
+
// Fix: escapeBlessed uses {open}/{close} tokens (Issue #239 Fix 1)
|
|
290
|
+
// ============================================================
|
|
291
|
+
|
|
292
|
+
runTest('escapeBlessed: uses {open}/{close} tokens, not backslash escaping', () => {
|
|
293
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
294
|
+
|
|
295
|
+
// Should NOT use backslash escaping (broken in blessed 0.1.81)
|
|
296
|
+
assert(source.indexOf("'\\\\{'") === -1 && source.indexOf("'\\\\}'") === -1,
|
|
297
|
+
'escapeBlessed should not use backslash escaping');
|
|
298
|
+
|
|
299
|
+
// Should use {open}/{close} approach
|
|
300
|
+
assert(source.indexOf('{open}') !== -1 && source.indexOf('{close}') !== -1,
|
|
301
|
+
'escapeBlessed should use {open}/{close} tokens');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ============================================================
|
|
305
|
+
// Fix: formatUptime called correctly (Issue #239 Fix 2)
|
|
306
|
+
// ============================================================
|
|
307
|
+
|
|
308
|
+
runTest('formatUptime: receives startedAt timestamp, not pre-computed elapsed', () => {
|
|
309
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
310
|
+
|
|
311
|
+
// Should NOT have the double-subtraction pattern
|
|
312
|
+
assert(source.indexOf('Date.now() - st.startedAt') === -1,
|
|
313
|
+
'formatUptime should not receive Date.now() - st.startedAt (double subtraction)');
|
|
314
|
+
|
|
315
|
+
// Should pass st.startedAt directly
|
|
316
|
+
assert(source.indexOf('registry.formatUptime(st.startedAt)') !== -1,
|
|
317
|
+
'formatUptime should receive st.startedAt directly');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
runTest('formatUptime: returns reasonable value for recent timestamp', () => {
|
|
321
|
+
// formatUptime expects a raw startedAt timestamp
|
|
322
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
|
323
|
+
const result = registry.formatUptime(fiveMinAgo);
|
|
324
|
+
assert(result === '5m', 'Expected 5m for 5 minutes ago, got: ' + result);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
runTest('formatUptime: double-subtraction would produce ~55000 years', () => {
|
|
328
|
+
// Demonstrate the bug: if you pass (Date.now() - startedAt) instead of startedAt,
|
|
329
|
+
// formatUptime computes Date.now() - (Date.now() - startedAt) = startedAt ≈ 1.75 trillion ms
|
|
330
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
|
331
|
+
const buggyElapsed = Date.now() - fiveMinAgo; // ~300000 ms
|
|
332
|
+
const buggyResult = registry.formatUptime(buggyElapsed);
|
|
333
|
+
// This would give Date.now() - 300000 ≈ current time minus 5 min ≈ huge uptime
|
|
334
|
+
// The correct result for 5 min ago should be "5m", not something with 'd'
|
|
335
|
+
assert(buggyResult.indexOf('d') !== -1 || buggyResult.indexOf('h') !== -1,
|
|
336
|
+
'Passing elapsed ms to formatUptime should give wrong large result (demonstrates bug): ' + buggyResult);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ============================================================
|
|
340
|
+
// Fix: No unused fs import (Issue #239 Fix 3)
|
|
341
|
+
// ============================================================
|
|
342
|
+
|
|
343
|
+
runTest('attached.js: no unused fs import', () => {
|
|
344
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
345
|
+
|
|
346
|
+
// Should not import fs
|
|
347
|
+
const fsImport = /const\s+fs\s*=\s*require\s*\(\s*['"]fs['"]\s*\)/.test(source);
|
|
348
|
+
assert(!fsImport, 'attached.js should not have unused fs import');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ============================================================
|
|
352
|
+
// Fix: resolveTerminal extracted to shared utility (Issue #239 Fix 4)
|
|
353
|
+
// ============================================================
|
|
354
|
+
|
|
355
|
+
runTest('attached.js: imports resolveTerminal from lib/terminal-utils, not index', () => {
|
|
356
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
357
|
+
|
|
358
|
+
// Should NOT require('./index')
|
|
359
|
+
assert(source.indexOf("require('./index')") === -1,
|
|
360
|
+
'attached.js should not require the full TUI index.js');
|
|
361
|
+
|
|
362
|
+
// Should require from lib/terminal-utils
|
|
363
|
+
assert(source.indexOf("require('./lib/terminal-utils')") !== -1,
|
|
364
|
+
'attached.js should import from lib/terminal-utils');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
runTest('lib/terminal-utils.js: exports resolveTerminal function', () => {
|
|
368
|
+
const termUtils = require(path.join(__dirname, '..', '..', 'tui', 'lib', 'terminal-utils.js'));
|
|
369
|
+
|
|
370
|
+
assert(typeof termUtils.resolveTerminal === 'function',
|
|
371
|
+
'terminal-utils should export resolveTerminal');
|
|
372
|
+
assert(Array.isArray(termUtils.KNOWN_TERMINALS),
|
|
373
|
+
'terminal-utils should export KNOWN_TERMINALS');
|
|
374
|
+
assert(typeof termUtils.REMAP_TERMINALS === 'object',
|
|
375
|
+
'terminal-utils should export REMAP_TERMINALS');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
runTest('lib/terminal-utils.js: resolveTerminal works correctly', () => {
|
|
379
|
+
const termUtils = require(path.join(__dirname, '..', '..', 'tui', 'lib', 'terminal-utils.js'));
|
|
380
|
+
|
|
381
|
+
// Known terminal passes through
|
|
382
|
+
let result = termUtils.resolveTerminal('xterm-256color');
|
|
383
|
+
assert(result.effectiveTerm === 'xterm-256color', 'xterm-256color should pass through');
|
|
384
|
+
assert(!result.wasRemapped, 'should not be remapped');
|
|
385
|
+
assert(!result.wasUnknown, 'should not be unknown');
|
|
386
|
+
|
|
387
|
+
// Ghostty gets remapped
|
|
388
|
+
result = termUtils.resolveTerminal('xterm-ghostty');
|
|
389
|
+
assert(result.effectiveTerm === 'xterm-256color', 'ghostty should remap to xterm-256color');
|
|
390
|
+
assert(result.wasRemapped, 'should be remapped');
|
|
391
|
+
|
|
392
|
+
// Unknown falls back
|
|
393
|
+
result = termUtils.resolveTerminal('totally-unknown-term');
|
|
394
|
+
assert(result.effectiveTerm === 'xterm-256color', 'unknown should fallback');
|
|
395
|
+
assert(result.wasUnknown, 'should be flagged as unknown');
|
|
396
|
+
|
|
397
|
+
// Null input
|
|
398
|
+
result = termUtils.resolveTerminal(null);
|
|
399
|
+
assert(result.effectiveTerm === 'unknown', 'null should give unknown');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
runTest('index.js: imports resolveTerminal from lib/terminal-utils', () => {
|
|
403
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'index.js'), 'utf8');
|
|
404
|
+
|
|
405
|
+
assert(source.indexOf("require('./lib/terminal-utils')") !== -1,
|
|
406
|
+
'index.js should import from lib/terminal-utils');
|
|
407
|
+
|
|
408
|
+
// Should NOT have the inline function definition anymore
|
|
409
|
+
assert(source.indexOf('function resolveTerminal(term)') === -1,
|
|
410
|
+
'index.js should not define resolveTerminal inline anymore');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ============================================================
|
|
414
|
+
// Issue #245 Fix 1: Chat box padding — bottom:3 instead of height
|
|
415
|
+
// ============================================================
|
|
416
|
+
|
|
417
|
+
runTest('245-1: chatBox uses bottom:3 to avoid overlap with input', () => {
|
|
418
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
419
|
+
// Should use bottom:3 for chatBox (robust positioning)
|
|
420
|
+
assert(source.indexOf('bottom: 3') !== -1, 'chatBox should use bottom: 3');
|
|
421
|
+
// Should NOT use the old height: "100%-3" pattern for chatBox
|
|
422
|
+
// (The string '100%-3' in the initial chatBox creation was the cause)
|
|
423
|
+
const chatBoxSection = source.substring(
|
|
424
|
+
source.indexOf('// Chat display area'),
|
|
425
|
+
source.indexOf('// Input box'),
|
|
426
|
+
);
|
|
427
|
+
assert(chatBoxSection.indexOf("'100%-3'") === -1 && chatBoxSection.indexOf('"100%-3"') === -1,
|
|
428
|
+
'chatBox should not use height: 100%-3');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ============================================================
|
|
432
|
+
// Issue #245 Fix 2: Ctrl+U works when input is focused
|
|
433
|
+
// ============================================================
|
|
434
|
+
|
|
435
|
+
runTest('245-2: Ctrl+U bound on inputBox (not just screen)', () => {
|
|
436
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
437
|
+
// Should have inputBox.key(['C-u']) binding
|
|
438
|
+
assert(source.indexOf("inputBox.key(['C-u']") !== -1 || source.indexOf('inputBox.key(["C-u"]') !== -1,
|
|
439
|
+
'Ctrl+U should be bound on inputBox');
|
|
440
|
+
// Should also have screen.key(['C-u']) binding
|
|
441
|
+
assert(source.indexOf("screen.key(['C-u']") !== -1,
|
|
442
|
+
'Ctrl+U should also be bound on screen');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
runTest('245-2: Ctrl+U handler calls writeInterrupt', () => {
|
|
446
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
447
|
+
// Both bindings should route to the same handler that calls writeInterrupt
|
|
448
|
+
assert(source.indexOf('handleCtrlU') !== -1, 'Should use shared handleCtrlU handler');
|
|
449
|
+
assert(source.indexOf('writeInterrupt') !== -1, 'handleCtrlU should call writeInterrupt');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// ============================================================
|
|
453
|
+
// Issue #245 Fix 3: Ctrl+C works when input is focused
|
|
454
|
+
// ============================================================
|
|
455
|
+
|
|
456
|
+
runTest('245-3: Ctrl+C bound on inputBox (not just screen)', () => {
|
|
457
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
458
|
+
// Should have inputBox.key(['C-c']) binding
|
|
459
|
+
assert(source.indexOf("inputBox.key(['C-c']") !== -1 || source.indexOf('inputBox.key(["C-c"]') !== -1,
|
|
460
|
+
'Ctrl+C should be bound on inputBox');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
runTest('245-3: Ctrl+C calls detach (not process.kill)', () => {
|
|
464
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
465
|
+
// The Ctrl+C inputBox handler should call detach()
|
|
466
|
+
let ctrlCIndex = source.indexOf("inputBox.key(['C-c']");
|
|
467
|
+
if (ctrlCIndex === -1) {
|
|
468
|
+
ctrlCIndex = source.indexOf("inputBox.key('C-c'");
|
|
469
|
+
}
|
|
470
|
+
assert(ctrlCIndex !== -1, 'Should have Ctrl+C binding on inputBox');
|
|
471
|
+
const ctrlCSection = source.substring(ctrlCIndex, ctrlCIndex + 200);
|
|
472
|
+
assert(ctrlCSection.indexOf('detach()') !== -1, 'Ctrl+C on inputBox should call detach()');
|
|
473
|
+
// Should NOT kill agent
|
|
474
|
+
assert(source.indexOf('process.kill') === -1, 'Should not kill agent process');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ============================================================
|
|
478
|
+
// Issue #245 Fix 4: Markdown rendering
|
|
479
|
+
// ============================================================
|
|
480
|
+
|
|
481
|
+
runTest('245-4: attached.js imports MarkdownRenderer', () => {
|
|
482
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
483
|
+
assert(source.indexOf("require('./lib/markdown-renderer')") !== -1,
|
|
484
|
+
'Should import MarkdownRenderer');
|
|
485
|
+
assert(source.indexOf('new MarkdownRenderer') !== -1,
|
|
486
|
+
'Should instantiate MarkdownRenderer');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
runTest('245-4: assistant history messages rendered with markdown', () => {
|
|
490
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
491
|
+
// History rendering should use mdRenderer.render() for assistant messages
|
|
492
|
+
assert(source.indexOf('mdRenderer.render(') !== -1,
|
|
493
|
+
'Should call mdRenderer.render() for assistant messages');
|
|
494
|
+
// Should have role check for assistant
|
|
495
|
+
assert(source.indexOf("role === 'assistant'") !== -1,
|
|
496
|
+
'Should check role === assistant for markdown rendering');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
runTest('245-4: streaming text accumulated for markdown finalization', () => {
|
|
500
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
501
|
+
// Should accumulate streaming text
|
|
502
|
+
assert(source.indexOf('streamingRawText') !== -1,
|
|
503
|
+
'Should track streamingRawText for markdown rendering');
|
|
504
|
+
assert(source.indexOf('finalizeStreaming') !== -1,
|
|
505
|
+
'Should have finalizeStreaming function to render markdown');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
runTest('245-4: MarkdownRenderer renders basic markdown correctly', () => {
|
|
509
|
+
const MR = require(path.join(__dirname, '..', '..', 'tui', 'lib', 'markdown-renderer.js'));
|
|
510
|
+
const md = new MR({ width: 80 });
|
|
511
|
+
|
|
512
|
+
// Bold renders
|
|
513
|
+
let result = md.render('**bold text**');
|
|
514
|
+
assert(result.indexOf('{bold}') !== -1, 'Should render bold: ' + result);
|
|
515
|
+
|
|
516
|
+
// Code block renders (var and 1 get syntax-highlighted, check for structure)
|
|
517
|
+
result = md.render('```js\nvar x = 1;\n```');
|
|
518
|
+
assert(result.indexOf('x =') !== -1, 'Should render code block with assignment: ' + result.substring(0, 100));
|
|
519
|
+
assert(result.indexOf('{gray-fg}') !== -1, 'Should have gray border in code block');
|
|
520
|
+
|
|
521
|
+
// Headers render
|
|
522
|
+
result = md.render('# Hello');
|
|
523
|
+
assert(result.indexOf('{bold}') !== -1, 'Should render header as bold');
|
|
524
|
+
|
|
525
|
+
// Inline code renders
|
|
526
|
+
result = md.render('Use `foo` here');
|
|
527
|
+
assert(result.indexOf('{yellow-fg}') !== -1 || result.indexOf('{inverse}') !== -1,
|
|
528
|
+
'Should render inline code');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ============================================================
|
|
532
|
+
// Issue #245 Fix 5: Text selection via Ctrl+S mouse toggle
|
|
533
|
+
// ============================================================
|
|
534
|
+
|
|
535
|
+
runTest('245-5: Ctrl+S bound on both screen and inputBox', () => {
|
|
536
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
537
|
+
// screen.key(['C-s'])
|
|
538
|
+
assert(source.indexOf("screen.key(['C-s']") !== -1,
|
|
539
|
+
'Ctrl+S should be bound on screen');
|
|
540
|
+
// inputBox.key(['C-s'])
|
|
541
|
+
assert(source.indexOf("inputBox.key(['C-s']") !== -1,
|
|
542
|
+
'Ctrl+S should be bound on inputBox');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
runTest('245-5: mouse toggle uses disableMouse/enableMouse', () => {
|
|
546
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
547
|
+
assert(source.indexOf('screen.program.disableMouse()') !== -1,
|
|
548
|
+
'Should call disableMouse for text selection mode');
|
|
549
|
+
assert(source.indexOf('screen.program.enableMouse()') !== -1,
|
|
550
|
+
'Should call enableMouse for scroll mode');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
runTest('245-5: default mouse mode is selection (disabled)', () => {
|
|
554
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
555
|
+
// At the end of initialization, mouse should be disabled by default
|
|
556
|
+
// Check that the default state sets mouseEnabled = false
|
|
557
|
+
const defaultSection = source.substring(source.indexOf('// Default to text selection'));
|
|
558
|
+
assert(defaultSection.indexOf('disableMouse') !== -1,
|
|
559
|
+
'Should disable mouse capture by default');
|
|
560
|
+
assert(defaultSection.indexOf('mouseEnabled = false') !== -1,
|
|
561
|
+
'Should set mouseEnabled = false by default');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// ============================================================
|
|
565
|
+
// Issue #245 Fix 6: Multi-line input (textarea + Alt+Enter)
|
|
566
|
+
// ============================================================
|
|
567
|
+
|
|
568
|
+
runTest('245-6: inputBox is blessed.textarea (not textbox)', () => {
|
|
569
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
570
|
+
assert(source.indexOf('blessed.textarea(') !== -1,
|
|
571
|
+
'Should use blessed.textarea for multi-line input');
|
|
572
|
+
// Should NOT use blessed.textbox
|
|
573
|
+
assert(source.indexOf('blessed.textbox(') === -1,
|
|
574
|
+
'Should NOT use blessed.textbox');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
runTest('245-6: textarea has wrap and wordWrap options', () => {
|
|
578
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
579
|
+
// Find the textarea options section
|
|
580
|
+
const textareaSection = source.substring(
|
|
581
|
+
source.indexOf('blessed.textarea('),
|
|
582
|
+
source.indexOf('blessed.textarea(') + 500,
|
|
583
|
+
);
|
|
584
|
+
assert(textareaSection.indexOf('wrap: true') !== -1,
|
|
585
|
+
'textarea should have wrap: true');
|
|
586
|
+
assert(textareaSection.indexOf('wordWrap: true') !== -1,
|
|
587
|
+
'textarea should have wordWrap: true');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
runTest('245-6: Alt+Enter intercepted at stdin level for newline insertion', () => {
|
|
591
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
592
|
+
// Should intercept at raw stdin level (same as main TUI)
|
|
593
|
+
assert(source.indexOf('origStdinEmit') !== -1,
|
|
594
|
+
'Should intercept stdin emit for Alt+Enter');
|
|
595
|
+
assert(source.indexOf('\\x1b\\r') !== -1 || source.indexOf('\\x1b\\n') !== -1,
|
|
596
|
+
'Should detect Alt+Enter escape sequences');
|
|
597
|
+
assert(source.indexOf('appendNewline') !== -1,
|
|
598
|
+
'Should have appendNewline function');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
runTest('245-6: adjustInputHeight grows textarea based on content', () => {
|
|
602
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
603
|
+
assert(source.indexOf('adjustInputHeight') !== -1,
|
|
604
|
+
'Should have adjustInputHeight function');
|
|
605
|
+
// Should have a max height limit
|
|
606
|
+
assert(source.indexOf('maxHeight') !== -1,
|
|
607
|
+
'Should limit max height for input');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ============================================================
|
|
611
|
+
// Issue #245 Fix 7: Escape behavior (unfocus first, detach second)
|
|
612
|
+
// ============================================================
|
|
613
|
+
|
|
614
|
+
runTest('245-7: Escape unfocuses input first, then detaches', () => {
|
|
615
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
616
|
+
// Should track inputFocused state
|
|
617
|
+
assert(source.indexOf('inputFocused') !== -1,
|
|
618
|
+
'Should track inputFocused state');
|
|
619
|
+
// Escape handler should check inputFocused
|
|
620
|
+
const escIdx = source.indexOf("screen.key(['escape']");
|
|
621
|
+
assert(escIdx !== -1, 'Escape handler should bind on screen.key');
|
|
622
|
+
const escSection = source.substring(escIdx);
|
|
623
|
+
assert(escSection.indexOf('inputFocused') !== -1,
|
|
624
|
+
'Escape handler should check inputFocused');
|
|
625
|
+
// Should call inputBox.cancel() or similar to unfocus
|
|
626
|
+
assert(source.indexOf('inputBox.cancel()') !== -1 || source.indexOf('chatBox.focus()') !== -1,
|
|
627
|
+
'Should unfocus input on first Escape');
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
runTest('245-7: Enter re-focuses input when chatBox is focused', () => {
|
|
631
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
632
|
+
// chatBox.key(['enter']) should refocus input
|
|
633
|
+
assert(source.indexOf("chatBox.key(['enter']") !== -1,
|
|
634
|
+
'Enter on chatBox should re-focus input');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
runTest('245-7: inputBox focus event updates inputFocused state', () => {
|
|
638
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
639
|
+
assert(source.indexOf("inputBox.on('focus'") !== -1,
|
|
640
|
+
'Should listen for inputBox focus event');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// ============================================================
|
|
644
|
+
// General: stdin interceptor cleanup on detach
|
|
645
|
+
// ============================================================
|
|
646
|
+
|
|
647
|
+
runTest('245-general: detach restores original stdin emit', () => {
|
|
648
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
649
|
+
// The detach function should restore original stdin emit
|
|
650
|
+
const detachSection = source.substring(source.indexOf('function detach()'));
|
|
651
|
+
assert(detachSection.indexOf('origStdinEmit') !== -1,
|
|
652
|
+
'detach should restore original stdin emit');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
runTest('245-general: help text updated with new shortcuts', () => {
|
|
656
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
657
|
+
// Help text should mention Ctrl+S and Esc
|
|
658
|
+
assert(source.indexOf('Ctrl+S') !== -1,
|
|
659
|
+
'Help text should mention Ctrl+S for mouse toggle');
|
|
660
|
+
assert(source.indexOf('Esc') !== -1,
|
|
661
|
+
'Help text should mention Esc');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
runTest('245-general: status bar shows mouse mode', () => {
|
|
665
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
666
|
+
assert(source.indexOf('mouseMode') !== -1 || source.indexOf('mouse:') !== -1,
|
|
667
|
+
'Status bar should show mouse mode');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
runTest('245-general: resize handler updates markdown renderer width', () => {
|
|
671
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
672
|
+
assert(source.indexOf('mdRenderer.width') !== -1,
|
|
673
|
+
'Resize handler should update mdRenderer.width');
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// ============================================================
|
|
677
|
+
// Copilot review follow-up tests
|
|
678
|
+
// ============================================================
|
|
679
|
+
|
|
680
|
+
runTest('review-1: finalizeStreaming preserves non-stream content (tool/status lines)', () => {
|
|
681
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
682
|
+
// finalizeStreaming should locate the escaped raw text and replace only that segment
|
|
683
|
+
const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
|
|
684
|
+
assert(finalizeSection.indexOf('escapedRawText') !== -1,
|
|
685
|
+
'Should search for escaped raw text to replace only the streamed segment');
|
|
686
|
+
assert(finalizeSection.indexOf('after') !== -1,
|
|
687
|
+
'Should preserve content after the streamed segment');
|
|
688
|
+
// The new version should include 'after' in the setContent call
|
|
689
|
+
assert(finalizeSection.indexOf('rendered + after') !== -1,
|
|
690
|
+
'Should append preserved tail content after rendered markdown');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
runTest('review-2: Enter-to-send calls adjustInputHeight after clearValue', () => {
|
|
694
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
695
|
+
// Find the Enter key handler (use larger window to capture the full handler)
|
|
696
|
+
const enterIdx = source.indexOf("inputBox.key('enter'");
|
|
697
|
+
const enterSection = source.substring(enterIdx, enterIdx + 600);
|
|
698
|
+
// The send branch (after sendToInbox) should have clearValue followed by adjustInputHeight
|
|
699
|
+
const sendIdx = enterSection.indexOf('sendToInbox(');
|
|
700
|
+
const clearAfterSend = enterSection.indexOf('clearValue()', sendIdx);
|
|
701
|
+
const adjustAfterSend = enterSection.indexOf('adjustInputHeight()', sendIdx);
|
|
702
|
+
assert(clearAfterSend !== -1, 'Enter send branch should call clearValue');
|
|
703
|
+
assert(adjustAfterSend !== -1, 'Enter send branch should call adjustInputHeight');
|
|
704
|
+
assert(adjustAfterSend > clearAfterSend, 'adjustInputHeight should come after clearValue in send branch');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
runTest('review-3: adjustInputHeight uses wrap-aware visual line counting', () => {
|
|
708
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
709
|
+
const adjustSection = source.substring(source.indexOf('function adjustInputHeight'));
|
|
710
|
+
// Should calculate visual lines based on innerWidth wrapping
|
|
711
|
+
assert(adjustSection.indexOf('innerWidth') !== -1,
|
|
712
|
+
'Should calculate innerWidth for wrap-aware counting');
|
|
713
|
+
assert(adjustSection.indexOf('Math.ceil') !== -1,
|
|
714
|
+
'Should use Math.ceil for visual line calculation');
|
|
715
|
+
assert(adjustSection.indexOf('visualLines') !== -1,
|
|
716
|
+
'Should count visual lines, not just logical lines');
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
runTest('review-4: streamingContentStart includes trailing space', () => {
|
|
720
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
721
|
+
// Both message.start and message.updated should set header with trailing space
|
|
722
|
+
const matches = source.match(/streamingContentStart\s*=\s*'[^']*Agent[^']*'/g) || [];
|
|
723
|
+
for (let i = 0; i < matches.length; i++) {
|
|
724
|
+
const m = matches[i];
|
|
725
|
+
// Should end with a space before the closing quote
|
|
726
|
+
assert(m.indexOf("Agent:{/green-fg} '") !== -1,
|
|
727
|
+
'streamingContentStart should have trailing space: ' + m);
|
|
728
|
+
}
|
|
729
|
+
assert(matches.length >= 2, 'Should have at least 2 streamingContentStart Agent assignments (message.start + message.updated), got: ' + matches.length);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
runTest('review-5: inputBox blur handler keeps inputFocused in sync', () => {
|
|
733
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
734
|
+
assert(source.indexOf("inputBox.on('blur'") !== -1,
|
|
735
|
+
'Should have inputBox blur handler');
|
|
736
|
+
// Blur handler should set inputFocused = false
|
|
737
|
+
const blurIdx = source.indexOf("inputBox.on('blur'");
|
|
738
|
+
const blurSection = source.substring(blurIdx, blurIdx + 100);
|
|
739
|
+
assert(blurSection.indexOf('inputFocused = false') !== -1,
|
|
740
|
+
'Blur handler should set inputFocused = false');
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
runTest('review-5: chatBox focus handler keeps inputFocused in sync', () => {
|
|
744
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
745
|
+
assert(source.indexOf("chatBox.on('focus'") !== -1,
|
|
746
|
+
'Should have chatBox focus handler');
|
|
747
|
+
const focusIdx = source.indexOf("chatBox.on('focus'");
|
|
748
|
+
const focusSection = source.substring(focusIdx, focusIdx + 100);
|
|
749
|
+
assert(focusSection.indexOf('inputFocused = false') !== -1,
|
|
750
|
+
'chatBox focus handler should set inputFocused = false');
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Copilot review round 2 tests
|
|
754
|
+
|
|
755
|
+
runTest('review2-1: mouseEnabled declared before formatStatusBar is called', () => {
|
|
756
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
757
|
+
// mouseEnabled must be declared before the first formatStatusBar call
|
|
758
|
+
const mouseDecl = source.indexOf('let mouseEnabled');
|
|
759
|
+
const firstFormatCall = source.indexOf('formatStatusBar(agentName');
|
|
760
|
+
assert(mouseDecl !== -1, 'mouseEnabled should be declared');
|
|
761
|
+
assert(firstFormatCall !== -1, 'formatStatusBar should be called');
|
|
762
|
+
assert(mouseDecl < firstFormatCall,
|
|
763
|
+
'mouseEnabled (' + mouseDecl + ') must be declared before first formatStatusBar call (' + firstFormatCall + ')');
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
runTest('review2-2: finalizeStreaming guards on rawIdx to avoid corrupting interleaved output', () => {
|
|
767
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
768
|
+
const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
|
|
769
|
+
// Should check if rawIdx was found
|
|
770
|
+
assert(finalizeSection.indexOf('rawIdx !== -1') !== -1,
|
|
771
|
+
'Should check if rawIdx was found');
|
|
772
|
+
// When rawIdx === -1, should skip finalization (not append duplicate)
|
|
773
|
+
assert(finalizeSection.indexOf('Skip') !== -1 || finalizeSection.indexOf('skip') !== -1,
|
|
774
|
+
'Should skip finalization when raw text not found');
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
runTest('review2-3: Enter key handler comment is accurate (no submit reference)', () => {
|
|
778
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
779
|
+
// Find the line before the enter key binding
|
|
780
|
+
const enterIdx = source.indexOf("inputBox.key('enter'");
|
|
781
|
+
const contextBefore = source.substring(Math.max(0, enterIdx - 100), enterIdx);
|
|
782
|
+
assert(contextBefore.indexOf('submit') === -1,
|
|
783
|
+
'Comment before Enter handler should not misleadingly reference submit event');
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
runTest('review2-4: test section comment does not reference wrong PR number', () => {
|
|
787
|
+
const testSource = fs.readFileSync(__filename, 'utf8');
|
|
788
|
+
// Test file should not reference "PR #247" in section headers
|
|
789
|
+
const sectionHeaders = testSource.match(/^\/\/ .+$/gm) || [];
|
|
790
|
+
for (let i = 0; i < sectionHeaders.length; i++) {
|
|
791
|
+
assert(sectionHeaders[i].indexOf('PR #247') === -1,
|
|
792
|
+
'Test section header should not reference PR #247: ' + sectionHeaders[i]);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Copilot review round 3 tests
|
|
797
|
+
|
|
798
|
+
runTest('review3-1: finalizeStreaming skips on rawIdx=-1 (no dead append code)', () => {
|
|
799
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
800
|
+
const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
|
|
801
|
+
// Should NOT have the old pattern: rawIdx = headerEndIdx (which made rawIdx !== -1 always true)
|
|
802
|
+
assert(finalizeSection.indexOf('rawIdx = headerEndIdx') === -1,
|
|
803
|
+
'Should not reassign rawIdx to headerEndIdx (makes append fallback dead code)');
|
|
804
|
+
// The rawIdx !== -1 check should be reachable
|
|
805
|
+
assert(finalizeSection.indexOf('rawIdx !== -1') !== -1,
|
|
806
|
+
'Should still check rawIdx !== -1');
|
|
807
|
+
// Should NOT have append fallback (skip instead to avoid duplicates)
|
|
808
|
+
assert(finalizeSection.indexOf('needsSpaceAppend') === -1,
|
|
809
|
+
'Should not have append fallback (skip finalization when raw text not found)');
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
runTest('review3-2: appendNewline renamed from insertNewlineAtCursor', () => {
|
|
813
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
814
|
+
// Should use appendNewline (accurate name)
|
|
815
|
+
assert(source.indexOf('function appendNewline') !== -1,
|
|
816
|
+
'Should have appendNewline function');
|
|
817
|
+
// Should NOT have the old misleading name
|
|
818
|
+
assert(source.indexOf('insertNewlineAtCursor') === -1,
|
|
819
|
+
'Should not use old insertNewlineAtCursor name');
|
|
820
|
+
// Function comment should mention appending (not cursor insertion)
|
|
821
|
+
const funcIdx = source.indexOf('function appendNewline');
|
|
822
|
+
const commentBefore = source.substring(Math.max(0, funcIdx - 200), funcIdx);
|
|
823
|
+
assert(commentBefore.indexOf('Append') !== -1 || commentBefore.indexOf('append') !== -1,
|
|
824
|
+
'Comment should describe append semantics');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
runTest('review3-3: no unused variables in test file', () => {
|
|
828
|
+
const testSource = fs.readFileSync(__filename, 'utf8');
|
|
829
|
+
// The oldPattern variable was flagged as unused — search for declaration pattern
|
|
830
|
+
const unusedVar = 'var old' + 'Pattern';
|
|
831
|
+
assert(testSource.indexOf(unusedVar) === -1,
|
|
832
|
+
'Should not have unused oldPattern variable');
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
runTest('review3-4: status bar refreshed after default mouse mode set', () => {
|
|
836
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
837
|
+
// After mouseEnabled = false (default), should call updateStatusBar
|
|
838
|
+
const defaultSection = source.substring(source.indexOf('// Default to text selection'));
|
|
839
|
+
const mouseSetIdx = defaultSection.indexOf('mouseEnabled = false');
|
|
840
|
+
const updateIdx = defaultSection.indexOf('updateStatusBar()');
|
|
841
|
+
assert(mouseSetIdx !== -1, 'Should set mouseEnabled = false');
|
|
842
|
+
assert(updateIdx !== -1, 'Should call updateStatusBar after default mouse mode');
|
|
843
|
+
assert(updateIdx > mouseSetIdx, 'updateStatusBar should come after mouseEnabled = false');
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// Copilot review round 4 tests
|
|
847
|
+
|
|
848
|
+
runTest('review4-1: Enter on empty input clears accumulated newlines', () => {
|
|
849
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
850
|
+
const enterIdx = source.indexOf("inputBox.key('enter'");
|
|
851
|
+
const enterSection = source.substring(enterIdx, enterIdx + 600);
|
|
852
|
+
// Should handle empty/whitespace-only input
|
|
853
|
+
assert(enterSection.indexOf('!value.trim()') !== -1,
|
|
854
|
+
'Enter handler should check for empty/whitespace input');
|
|
855
|
+
// Should clear and return early for empty input
|
|
856
|
+
const emptyBranchIdx = enterSection.indexOf('!value.trim()');
|
|
857
|
+
const clearInBranch = enterSection.indexOf('clearValue()', emptyBranchIdx);
|
|
858
|
+
assert(clearInBranch !== -1, 'Should call clearValue in empty input branch');
|
|
859
|
+
const returnInBranch = enterSection.indexOf('return', emptyBranchIdx);
|
|
860
|
+
assert(returnInBranch !== -1 && returnInBranch < enterSection.indexOf('sendToInbox'),
|
|
861
|
+
'Should return early for empty input before sendToInbox');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
runTest('review4-2: finalizeStreaming uses conditional space (no double-spacing)', () => {
|
|
865
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
866
|
+
const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
|
|
867
|
+
// Should check for trailing whitespace before adding space
|
|
868
|
+
assert(finalizeSection.indexOf('needsSpaceBefore') !== -1,
|
|
869
|
+
'Should conditionally add space before rendered markdown');
|
|
870
|
+
assert(finalizeSection.indexOf('/\\s$/') !== -1,
|
|
871
|
+
'Should use regex to check for trailing whitespace');
|
|
872
|
+
// Should NOT have unconditional hard-coded space
|
|
873
|
+
assert(finalizeSection.indexOf("before + ' ' + rendered") === -1,
|
|
874
|
+
'Should not have unconditional space before rendered content');
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Copilot review round 5 tests
|
|
878
|
+
|
|
879
|
+
runTest('review5-1: help text includes Ctrl+C detach shortcut', () => {
|
|
880
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
881
|
+
assert(source.indexOf('Ctrl+C: detach') !== -1,
|
|
882
|
+
'Help text should mention Ctrl+C: detach');
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Copilot review round 6 tests
|
|
886
|
+
|
|
887
|
+
runTest('review6-1: assistant history messages are capped for responsiveness', () => {
|
|
888
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
889
|
+
const histSection = source.substring(source.indexOf('function fetchHistory'));
|
|
890
|
+
// Should cap assistant content length before rendering
|
|
891
|
+
assert(histSection.indexOf('cappedContent') !== -1,
|
|
892
|
+
'Should cap assistant content before markdown rendering');
|
|
893
|
+
assert(histSection.indexOf('truncated') !== -1,
|
|
894
|
+
'Should add truncation marker for long assistant messages');
|
|
895
|
+
// Cap should be higher than user messages (500) but still bounded
|
|
896
|
+
assert(histSection.indexOf('5000') !== -1,
|
|
897
|
+
'Assistant cap should be 5000 chars (higher than user 500 but still bounded)');
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Copilot review round 7 tests
|
|
901
|
+
|
|
902
|
+
runTest('review7-1: message.updated gates streaming start on finish===null', () => {
|
|
903
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
904
|
+
const updatedSection = source.substring(source.indexOf("eventName === 'message.updated'"));
|
|
905
|
+
// Should check finish to avoid double Agent: header
|
|
906
|
+
assert(updatedSection.indexOf('!finish') !== -1,
|
|
907
|
+
'message.updated handler should gate on finish being null/falsy');
|
|
908
|
+
assert(updatedSection.indexOf('!streamingActive') !== -1,
|
|
909
|
+
'message.updated handler should check streamingActive to prevent re-start');
|
|
910
|
+
// Should finalize on completion (finish set)
|
|
911
|
+
assert(updatedSection.indexOf('finish && streamingActive') !== -1,
|
|
912
|
+
'message.updated should finalize when finish is set');
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
runTest('review7-2: streamingRawText is capped during streaming', () => {
|
|
916
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
917
|
+
const deltaSection = source.substring(source.indexOf("eventName === 'message.part.delta'"));
|
|
918
|
+
// Should cap streaming text accumulation
|
|
919
|
+
assert(deltaSection.indexOf('STREAM_CAP') !== -1,
|
|
920
|
+
'Should define STREAM_CAP for streaming text limit');
|
|
921
|
+
assert(deltaSection.indexOf('streamingRawText.length >= STREAM_CAP') !== -1,
|
|
922
|
+
'Should check streamingRawText length against cap for early return');
|
|
923
|
+
// Legacy text.delta should also be capped
|
|
924
|
+
const legacySection = source.substring(source.indexOf("eventName === 'text.delta'"));
|
|
925
|
+
assert(legacySection.indexOf('LEGACY_STREAM_CAP') !== -1,
|
|
926
|
+
'Legacy text.delta should also cap streaming text');
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
runTest('review7-3: unused source var removed from review2-4 test', () => {
|
|
930
|
+
const testSource = fs.readFileSync(__filename, 'utf8');
|
|
931
|
+
// The review2-4 test should not read attached.js (only reads __filename)
|
|
932
|
+
const review24Idx = testSource.indexOf("review2-4:");
|
|
933
|
+
const nextRunTestIdx = testSource.indexOf('runTest(', review24Idx + 10);
|
|
934
|
+
const review24Section = testSource.substring(review24Idx, nextRunTestIdx);
|
|
935
|
+
assert(review24Section.indexOf("attached.js") === -1,
|
|
936
|
+
'review2-4 test should not read attached.js (unused var was removed)');
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
runTest('review7-4: streaming uses part.delta for text extraction', () => {
|
|
940
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
941
|
+
// The extractText function should handle part.delta shape
|
|
942
|
+
assert(source.indexOf('part.delta') !== -1 || source.indexOf("'message.part.delta'") !== -1,
|
|
943
|
+
'Should handle message.part.delta event for streaming text');
|
|
944
|
+
// extractText should look at parsed data for text content
|
|
945
|
+
const extractSection = source.substring(source.indexOf('function extractText'));
|
|
946
|
+
assert(extractSection.indexOf('parsed') !== -1,
|
|
947
|
+
'extractText should examine parsed event data');
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
runTest('review7-5: adjustInputHeight derives width from element or screen', () => {
|
|
951
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
952
|
+
const adjustSection = source.substring(source.indexOf('function adjustInputHeight'));
|
|
953
|
+
// Should prefer inputBox.iwidth (computed inner width from blessed)
|
|
954
|
+
assert(adjustSection.indexOf('inputBox.iwidth') !== -1,
|
|
955
|
+
'Should check inputBox.iwidth for computed inner width');
|
|
956
|
+
// Should still fall back to screen.width when iwidth is not available
|
|
957
|
+
assert(adjustSection.indexOf('screen.width') !== -1,
|
|
958
|
+
'Should fall back to screen.width when inputBox.iwidth is not available');
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Copilot review round 8 tests
|
|
962
|
+
|
|
963
|
+
runTest('review8-1: finalizeStreaming resets state even when no text accumulated', () => {
|
|
964
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
965
|
+
const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'),
|
|
966
|
+
source.indexOf('function finalizeStreaming') + 800);
|
|
967
|
+
// Should reset streamingActive before the early return for no text
|
|
968
|
+
const activeResetIdx = finalizeSection.indexOf('streamingActive = false');
|
|
969
|
+
const hadTextIdx = finalizeSection.indexOf('if (!hadText) return');
|
|
970
|
+
assert(activeResetIdx !== -1, 'Should reset streamingActive');
|
|
971
|
+
assert(hadTextIdx !== -1, 'Should have early return for no text');
|
|
972
|
+
assert(activeResetIdx < hadTextIdx,
|
|
973
|
+
'streamingActive reset should come before the no-text early return');
|
|
974
|
+
// Should save rawText/contentStart before clearing
|
|
975
|
+
assert(finalizeSection.indexOf('const rawText = streamingRawText') !== -1
|
|
976
|
+
|| finalizeSection.indexOf('let rawText = streamingRawText') !== -1
|
|
977
|
+
|| finalizeSection.indexOf('var rawText = streamingRawText') !== -1,
|
|
978
|
+
'Should save streamingRawText to local var before clearing');
|
|
979
|
+
assert(finalizeSection.indexOf('const contentStart = streamingContentStart') !== -1
|
|
980
|
+
|| finalizeSection.indexOf('let contentStart = streamingContentStart') !== -1
|
|
981
|
+
|| finalizeSection.indexOf('var contentStart = streamingContentStart') !== -1,
|
|
982
|
+
'Should save streamingContentStart to local var before clearing');
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
runTest('review8-2: help line width is dynamic (not fixed)', () => {
|
|
986
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
987
|
+
// helpLine should not have a fixed numeric width
|
|
988
|
+
const helpSection = source.substring(source.indexOf('var helpLine'));
|
|
989
|
+
const widthMatch = helpSection.match(/width:\s*(\S+)/);
|
|
990
|
+
assert(widthMatch, 'helpLine should have a width property');
|
|
991
|
+
// Should be dynamic (shrink, percentage, or computed) not a fixed number
|
|
992
|
+
const widthVal = widthMatch[1].replace(/[,}]/, '');
|
|
993
|
+
assert(isNaN(Number(widthVal)), 'helpLine width should not be a fixed number, got: ' + widthVal);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Copilot review round 9 tests
|
|
997
|
+
|
|
998
|
+
runTest('review9-1: streaming cap also limits chatBox display (not just rawText)', () => {
|
|
999
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1000
|
+
const deltaSection = source.substring(source.indexOf("eventName === 'message.part.delta'"),
|
|
1001
|
+
source.indexOf("eventName === 'message.part.delta'") + 1500);
|
|
1002
|
+
// Should return early when at cap (skip both accumulation AND display)
|
|
1003
|
+
assert(deltaSection.indexOf('streamingRawText.length >= STREAM_CAP') !== -1,
|
|
1004
|
+
'Should check >= cap for early return');
|
|
1005
|
+
// Should cap what gets displayed too
|
|
1006
|
+
assert(deltaSection.indexOf('cappedText') !== -1,
|
|
1007
|
+
'Should use cappedText variable for both accumulation and display');
|
|
1008
|
+
// Should show truncation marker when cap is hit
|
|
1009
|
+
assert(deltaSection.indexOf('streaming truncated') !== -1,
|
|
1010
|
+
'Should show truncation marker when cap is reached');
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
runTest('review9-2: legacy streaming cap also limits chatBox display', () => {
|
|
1014
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1015
|
+
const legacySection = source.substring(source.indexOf("eventName === 'text.delta'"),
|
|
1016
|
+
source.indexOf("eventName === 'text.delta'") + 600);
|
|
1017
|
+
// Should return early when at cap
|
|
1018
|
+
assert(legacySection.indexOf('streamingRawText.length >= LEGACY_STREAM_CAP') !== -1,
|
|
1019
|
+
'Legacy path should check >= cap for early return');
|
|
1020
|
+
// Should use capped text for display
|
|
1021
|
+
assert(legacySection.indexOf('legacyCapped') !== -1,
|
|
1022
|
+
'Legacy path should use capped text for display');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
runTest('review9-3: bracketed paste handling enabled', () => {
|
|
1026
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1027
|
+
// Should enable bracketed paste mode
|
|
1028
|
+
assert(source.indexOf('\\x1b[?2004h') !== -1,
|
|
1029
|
+
'Should enable bracketed paste mode with escape sequence');
|
|
1030
|
+
// Should detect paste start/end markers
|
|
1031
|
+
assert(source.indexOf('\\x1b[200~') !== -1,
|
|
1032
|
+
'Should detect paste start marker');
|
|
1033
|
+
assert(source.indexOf('\\x1b[201~') !== -1,
|
|
1034
|
+
'Should detect paste end marker');
|
|
1035
|
+
// Should buffer pasted text
|
|
1036
|
+
assert(source.indexOf('pasteBuffer') !== -1,
|
|
1037
|
+
'Should buffer pasted text');
|
|
1038
|
+
assert(source.indexOf('isPasting') !== -1,
|
|
1039
|
+
'Should track paste state');
|
|
1040
|
+
// Should disable bracketed paste on detach
|
|
1041
|
+
assert(source.indexOf('\\x1b[?2004l') !== -1,
|
|
1042
|
+
'Should disable bracketed paste mode on detach');
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Copilot review round 10 tests
|
|
1046
|
+
|
|
1047
|
+
runTest('review10-1: rapid-input fallback uses burst count (not single timing)', () => {
|
|
1048
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1049
|
+
// Should track burst count, not just single timestamp
|
|
1050
|
+
assert(source.indexOf('rapidBurstCount') !== -1,
|
|
1051
|
+
'Should track rapidBurstCount for multi-char burst detection');
|
|
1052
|
+
assert(source.indexOf('RAPID_BURST_MIN') !== -1,
|
|
1053
|
+
'Should define RAPID_BURST_MIN constant');
|
|
1054
|
+
// Should require multiple rapid events before suppressing Enter
|
|
1055
|
+
assert(source.indexOf('rapidBurstCount >= RAPID_BURST_MIN') !== -1,
|
|
1056
|
+
'Should compare burst count against minimum threshold');
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
runTest('review10-2: finalizeStreaming skips when raw segment not found (no duplicate)', () => {
|
|
1060
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1061
|
+
const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
|
|
1062
|
+
// Should NOT append rendered content when rawIdx === -1
|
|
1063
|
+
assert(finalizeSection.indexOf('needsSpaceAppend') === -1,
|
|
1064
|
+
'Should not have append-with-space fallback (would duplicate content)');
|
|
1065
|
+
// The else branch should skip finalization
|
|
1066
|
+
assert(finalizeSection.indexOf('Skip') !== -1 || finalizeSection.indexOf('skip') !== -1,
|
|
1067
|
+
'Should skip finalization when raw text not found');
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
runTest('review10-3: adjustInputHeight called on keypress (throttled)', () => {
|
|
1071
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1072
|
+
// Should listen for keypress events on inputBox
|
|
1073
|
+
assert(source.indexOf("inputBox.on('keypress'") !== -1,
|
|
1074
|
+
'Should bind keypress handler on inputBox');
|
|
1075
|
+
// Should throttle the calls
|
|
1076
|
+
assert(source.indexOf('adjustThrottleTimer') !== -1,
|
|
1077
|
+
'Should use a throttle timer for keypress-triggered adjustInputHeight');
|
|
1078
|
+
// Should call adjustInputHeight within the throttled handler
|
|
1079
|
+
const keypressSection = source.substring(source.indexOf("inputBox.on('keypress'"),
|
|
1080
|
+
source.indexOf("inputBox.on('keypress'") + 300);
|
|
1081
|
+
assert(keypressSection.indexOf('adjustInputHeight()') !== -1,
|
|
1082
|
+
'Should call adjustInputHeight in throttled keypress handler');
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// Copilot review round 11 tests
|
|
1086
|
+
|
|
1087
|
+
runTest('review11-1: message.part.updated handled separately from part.delta', () => {
|
|
1088
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1089
|
+
// Should NOT handle both in the same branch
|
|
1090
|
+
assert(source.indexOf("eventName === 'message.part.delta' || eventName === 'message.part.updated'") === -1,
|
|
1091
|
+
'part.delta and part.updated should NOT be in the same if-branch');
|
|
1092
|
+
// Should have separate handler for part.updated
|
|
1093
|
+
assert(source.indexOf("eventName === 'message.part.updated'") !== -1,
|
|
1094
|
+
'Should have separate handler for message.part.updated');
|
|
1095
|
+
// part.updated should skip when streaming already has content (to avoid duplicate)
|
|
1096
|
+
const updatedSection = source.substring(source.indexOf("eventName === 'message.part.updated'"),
|
|
1097
|
+
source.indexOf("eventName === 'message.part.updated'") + 500);
|
|
1098
|
+
assert(updatedSection.indexOf('streamingRawText.length > 0') !== -1,
|
|
1099
|
+
'part.updated should check if deltas were already received');
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
runTest('review11-2: extractText handles part.delta field', () => {
|
|
1103
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1104
|
+
const extractSection = source.substring(source.indexOf('function extractText'),
|
|
1105
|
+
source.indexOf('function extractText') + 300);
|
|
1106
|
+
assert(extractSection.indexOf('part.delta') !== -1,
|
|
1107
|
+
'extractText should check part.delta for delta events');
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// Copilot review round 12 tests
|
|
1111
|
+
|
|
1112
|
+
runTest('review12-1: sendToInbox preserves original text (trimEnd only)', () => {
|
|
1113
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1114
|
+
const sendSection = source.substring(source.indexOf('function sendToInbox'),
|
|
1115
|
+
source.indexOf('function sendToInbox') + 400);
|
|
1116
|
+
// Should NOT use trim() to modify the text before sending
|
|
1117
|
+
assert(sendSection.indexOf("text = text.trim()") === -1,
|
|
1118
|
+
'sendToInbox should NOT use full trim() which strips leading indentation');
|
|
1119
|
+
// Should use trimEnd (or trimRight) to preserve leading whitespace
|
|
1120
|
+
assert(sendSection.indexOf('trimEnd()') !== -1,
|
|
1121
|
+
'sendToInbox should use trimEnd to preserve leading whitespace in multi-line input');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
runTest('review12-2: rapid-input uses burst count not single-event timing', () => {
|
|
1125
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1126
|
+
const stdinSection = source.substring(source.indexOf('process.stdin.emit = function'),
|
|
1127
|
+
source.indexOf('process.stdin.emit = function') + 1500);
|
|
1128
|
+
assert(stdinSection.indexOf('rapidBurstCount >= RAPID_BURST_MIN') !== -1,
|
|
1129
|
+
'Should use burst count threshold to suppress Enter');
|
|
1130
|
+
// Should count chars, not just events
|
|
1131
|
+
assert(stdinSection.indexOf('altStr.length') !== -1,
|
|
1132
|
+
'Should count characters in multi-char chunks');
|
|
1133
|
+
// Should reset counter when timing gap exceeds threshold
|
|
1134
|
+
assert(stdinSection.indexOf('rapidBurstCount = 0') !== -1,
|
|
1135
|
+
'Should reset burst counter on slow events');
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// Copilot review round 13 tests
|
|
1139
|
+
|
|
1140
|
+
runTest('review13-1: process-level cleanup for bracketed paste', () => {
|
|
1141
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1142
|
+
assert(source.indexOf('emergencyCleanup') !== -1,
|
|
1143
|
+
'Should define emergencyCleanup function');
|
|
1144
|
+
assert(source.indexOf("process.on('exit'") !== -1,
|
|
1145
|
+
'Should register exit handler');
|
|
1146
|
+
assert(source.indexOf("process.on('SIGINT'") !== -1,
|
|
1147
|
+
'Should register SIGINT handler');
|
|
1148
|
+
assert(source.indexOf("process.on('SIGTERM'") !== -1,
|
|
1149
|
+
'Should register SIGTERM handler');
|
|
1150
|
+
assert(source.indexOf("process.on('uncaughtException'") !== -1,
|
|
1151
|
+
'Should register uncaughtException handler');
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
runTest('review13-2: detach() guards against double-call', () => {
|
|
1155
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1156
|
+
const detachSection = source.substring(source.indexOf('function detach()'),
|
|
1157
|
+
source.indexOf('function detach()') + 300);
|
|
1158
|
+
// Should check detaching flag and return early
|
|
1159
|
+
assert(detachSection.indexOf('if (detaching) return') !== -1,
|
|
1160
|
+
'detach() should return early if already detaching');
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
runTest('review13-3: multi-char stdin chunk with newlines handled as paste', () => {
|
|
1164
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1165
|
+
const stdinSection = source.substring(source.indexOf('process.stdin.emit = function'),
|
|
1166
|
+
source.indexOf('process.stdin.emit = function') + 2000);
|
|
1167
|
+
// Should detect multi-char chunks with embedded newlines
|
|
1168
|
+
assert(stdinSection.indexOf('altStr.length > 1') !== -1,
|
|
1169
|
+
'Should detect multi-char data chunks');
|
|
1170
|
+
// Should replace CR/LF with newlines
|
|
1171
|
+
assert(stdinSection.indexOf("replace(/\\r\\n/g, '\\n')") !== -1,
|
|
1172
|
+
'Should normalize CRLF to LF in pasted text');
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// Copilot review round 14 tests
|
|
1176
|
+
|
|
1177
|
+
runTest('review14-1: Ctrl+C test guards against indexOf=-1 false positive', () => {
|
|
1178
|
+
const testSource = fs.readFileSync(__filename, 'utf8');
|
|
1179
|
+
const ctrlCTestSection = testSource.substring(testSource.indexOf('245-3: Ctrl+C calls detach'));
|
|
1180
|
+
// Should check ctrlCIndex !== -1 before using it
|
|
1181
|
+
assert(ctrlCTestSection.indexOf('ctrlCIndex !== -1') !== -1,
|
|
1182
|
+
'Test should assert index is not -1 before substring');
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
runTest('review14-2: rapidBurstCount reset after bracketed paste', () => {
|
|
1186
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1187
|
+
// After paste start marker
|
|
1188
|
+
const pasteStartIdx = source.indexOf("\\x1b[200~') !== -1");
|
|
1189
|
+
assert(pasteStartIdx !== -1, 'Should find paste start marker detection code');
|
|
1190
|
+
const pasteStartSection = source.substring(pasteStartIdx, pasteStartIdx + 400);
|
|
1191
|
+
assert(pasteStartSection.indexOf('rapidBurstCount = 0') !== -1,
|
|
1192
|
+
'Should reset rapidBurstCount when paste starts');
|
|
1193
|
+
// After paste end — search from isPasting = false with bigger window
|
|
1194
|
+
const pasteEndIdx = source.indexOf("isPasting = false");
|
|
1195
|
+
assert(pasteEndIdx !== -1, 'Should find end-of-paste handler marker "isPasting = false"');
|
|
1196
|
+
const pasteEndSection = source.substring(pasteEndIdx, pasteEndIdx + 500);
|
|
1197
|
+
assert(pasteEndSection.indexOf('rapidBurstCount = 0') !== -1,
|
|
1198
|
+
'Should reset rapidBurstCount when paste ends');
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
runTest('review14-3: uncaughtException handler destroys screen', () => {
|
|
1202
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1203
|
+
const uncaughtSection = source.substring(source.indexOf('uncaughtException'),
|
|
1204
|
+
source.indexOf('uncaughtException') + 300);
|
|
1205
|
+
assert(uncaughtSection.indexOf('screen.destroy()') !== -1,
|
|
1206
|
+
'uncaughtException handler should call screen.destroy()');
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
runTest('review14-4: adjustInputHeight uses inputBox.iwidth when available', () => {
|
|
1210
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1211
|
+
const adjustSection = source.substring(source.indexOf('function adjustInputHeight'),
|
|
1212
|
+
source.indexOf('function adjustInputHeight') + 500);
|
|
1213
|
+
assert(adjustSection.indexOf('inputBox.iwidth') !== -1,
|
|
1214
|
+
'Should use inputBox.iwidth for inner width calculation');
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Copilot review round 15 tests
|
|
1218
|
+
|
|
1219
|
+
runTest('review15-1: Escape bound on both screen and inputBox', () => {
|
|
1220
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1221
|
+
// Should have a shared handleEscape function
|
|
1222
|
+
assert(source.indexOf('function handleEscape') !== -1,
|
|
1223
|
+
'Should define handleEscape function');
|
|
1224
|
+
// Should bind on screen
|
|
1225
|
+
assert(source.indexOf("screen.key(['escape'], handleEscape)") !== -1,
|
|
1226
|
+
'Escape should be bound on screen');
|
|
1227
|
+
// Should bind on inputBox
|
|
1228
|
+
assert(source.indexOf("inputBox.key(['escape'], handleEscape)") !== -1,
|
|
1229
|
+
'Escape should also be bound on inputBox');
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// Copilot review round 17 tests
|
|
1233
|
+
|
|
1234
|
+
runTest('review17-1: rapidBurstCount reset after multi-char paste chunk', () => {
|
|
1235
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
|
|
1236
|
+
// Find the multi-char chunk handling section (non-bracketed paste)
|
|
1237
|
+
const multiCharIdx = source.indexOf('altStr.length > 1');
|
|
1238
|
+
assert(multiCharIdx !== -1, 'Should find multi-char chunk detection code');
|
|
1239
|
+
const multiCharSection = source.substring(multiCharIdx, multiCharIdx + 600);
|
|
1240
|
+
// Should reset rapidBurstCount after handling the paste chunk
|
|
1241
|
+
assert(multiCharSection.indexOf('rapidBurstCount = 0') !== -1,
|
|
1242
|
+
'Should reset rapidBurstCount after multi-char paste chunk');
|
|
1243
|
+
// Should also reset lastStdinDataTime
|
|
1244
|
+
assert(multiCharSection.indexOf('lastStdinDataTime = 0') !== -1,
|
|
1245
|
+
'Should reset lastStdinDataTime after multi-char paste chunk');
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// ============================================================
|
|
1249
|
+
// Cleanup & Summary
|
|
1250
|
+
// ============================================================
|
|
1251
|
+
|
|
1252
|
+
cleanup();
|
|
1253
|
+
|
|
1254
|
+
console.log('\nTotal: ' + (passed + failed) + ', Passed: ' + passed + ', Failed: ' + failed);
|
|
1255
|
+
if (failed > 0) process.exitCode = 1;
|