opencode-pilot 0.1.0 → 0.2.0
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/.github/workflows/ci.yml +8 -1
- package/.releaserc.cjs +10 -1
- package/AGENTS.md +6 -12
- package/README.md +31 -25
- package/bin/opencode-pilot +47 -209
- package/examples/config.yaml +2 -9
- package/package.json +6 -6
- package/plugin/index.js +45 -245
- package/service/{io.opencode.ntfy.plist → io.opencode.pilot.plist} +5 -5
- package/service/server.js +44 -1381
- package/test/run_tests.bash +1 -1
- package/test/test_actions.bash +21 -36
- package/test/test_cli.bash +20 -24
- package/test/test_plist.bash +11 -12
- package/test/test_poller.bash +20 -20
- package/test/test_repo_config.bash +19 -233
- package/test/test_service.bash +48 -1095
- package/test/unit/paths.test.js +16 -43
- package/test/unit/plugin.test.js +46 -0
- package/dist/opencode-ntfy.tar.gz +0 -0
- package/plugin/config.js +0 -76
- package/plugin/logger.js +0 -125
- package/plugin/notifier.js +0 -110
- package/test/test_config.bash +0 -438
- package/test/test_logger.bash +0 -401
- package/test/test_notifier.bash +0 -310
- package/test/test_plugin.bash +0 -952
- package/test/unit/config.test.js +0 -86
package/test/test_service.bash
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# Tests for service/server.js - Standalone
|
|
4
|
-
# Issue #13: Separate callback server as brew service
|
|
3
|
+
# Tests for service/server.js - Standalone polling server
|
|
5
4
|
#
|
|
6
5
|
|
|
7
6
|
set -euo pipefail
|
|
@@ -11,9 +10,6 @@ source "$SCRIPT_DIR/test_helper.bash"
|
|
|
11
10
|
|
|
12
11
|
SERVICE_DIR="$(dirname "$SCRIPT_DIR")/service"
|
|
13
12
|
|
|
14
|
-
# Disable HTTPS redirect for tests (overrides any user config)
|
|
15
|
-
export NTFY_CALLBACK_HTTPS=false
|
|
16
|
-
|
|
17
13
|
echo "Testing service/server.js module..."
|
|
18
14
|
echo ""
|
|
19
15
|
|
|
@@ -65,13 +61,6 @@ test_service_has_http_server() {
|
|
|
65
61
|
}
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
test_service_has_unix_socket() {
|
|
69
|
-
grep -q "createServer\|net\|\.sock\|socket" "$SERVICE_DIR/server.js" || {
|
|
70
|
-
echo "Unix socket handling not found in server.js"
|
|
71
|
-
return 1
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
64
|
test_service_has_health_endpoint() {
|
|
76
65
|
grep -q "/health" "$SERVICE_DIR/server.js" || {
|
|
77
66
|
echo "/health endpoint not found in server.js"
|
|
@@ -79,27 +68,6 @@ test_service_has_health_endpoint() {
|
|
|
79
68
|
}
|
|
80
69
|
}
|
|
81
70
|
|
|
82
|
-
test_service_has_callback_endpoint() {
|
|
83
|
-
grep -q "/callback" "$SERVICE_DIR/server.js" || {
|
|
84
|
-
echo "/callback endpoint not found in server.js"
|
|
85
|
-
return 1
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
test_service_handles_session_registration() {
|
|
90
|
-
grep -q "register\|session" "$SERVICE_DIR/server.js" || {
|
|
91
|
-
echo "Session registration not found in server.js"
|
|
92
|
-
return 1
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
test_service_handles_nonce_creation() {
|
|
97
|
-
grep -q "createNonce\|nonce" "$SERVICE_DIR/server.js" || {
|
|
98
|
-
echo "Nonce creation not found in server.js"
|
|
99
|
-
return 1
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
71
|
test_service_logs_with_prefix() {
|
|
104
72
|
grep -q "\[opencode-pilot\]" "$SERVICE_DIR/server.js" || {
|
|
105
73
|
echo "Logging prefix [opencode-pilot] not found in server.js"
|
|
@@ -107,7 +75,12 @@ test_service_logs_with_prefix() {
|
|
|
107
75
|
}
|
|
108
76
|
}
|
|
109
77
|
|
|
110
|
-
|
|
78
|
+
test_service_imports_poll_service() {
|
|
79
|
+
grep -q "poll-service" "$SERVICE_DIR/server.js" || {
|
|
80
|
+
echo "poll-service import not found in server.js"
|
|
81
|
+
return 1
|
|
82
|
+
}
|
|
83
|
+
}
|
|
111
84
|
|
|
112
85
|
# =============================================================================
|
|
113
86
|
# Functional Tests (requires Node.js)
|
|
@@ -123,10 +96,10 @@ test_service_starts_and_stops() {
|
|
|
123
96
|
result=$(node --experimental-vm-modules -e "
|
|
124
97
|
import { startService, stopService } from './service/server.js';
|
|
125
98
|
|
|
126
|
-
// Use random
|
|
99
|
+
// Use random port to avoid conflicts
|
|
127
100
|
const config = {
|
|
128
101
|
httpPort: 0,
|
|
129
|
-
|
|
102
|
+
enablePolling: false
|
|
130
103
|
};
|
|
131
104
|
|
|
132
105
|
const service = await startService(config);
|
|
@@ -161,7 +134,7 @@ test_service_health_endpoint_returns_200() {
|
|
|
161
134
|
|
|
162
135
|
const config = {
|
|
163
136
|
httpPort: 0,
|
|
164
|
-
|
|
137
|
+
enablePolling: false
|
|
165
138
|
};
|
|
166
139
|
|
|
167
140
|
const service = await startService(config);
|
|
@@ -187,7 +160,7 @@ test_service_health_endpoint_returns_200() {
|
|
|
187
160
|
fi
|
|
188
161
|
}
|
|
189
162
|
|
|
190
|
-
|
|
163
|
+
test_service_returns_404_for_unknown_route() {
|
|
191
164
|
if ! command -v node &>/dev/null; then
|
|
192
165
|
echo "SKIP: node not available"
|
|
193
166
|
return 0
|
|
@@ -199,18 +172,16 @@ test_service_returns_401_for_invalid_nonce() {
|
|
|
199
172
|
|
|
200
173
|
const config = {
|
|
201
174
|
httpPort: 0,
|
|
202
|
-
|
|
175
|
+
enablePolling: false
|
|
203
176
|
};
|
|
204
177
|
|
|
205
178
|
const service = await startService(config);
|
|
206
179
|
const port = service.httpServer.address().port;
|
|
207
180
|
|
|
208
|
-
const res = await fetch('http://localhost:' + port + '/
|
|
209
|
-
method: 'POST'
|
|
210
|
-
});
|
|
181
|
+
const res = await fetch('http://localhost:' + port + '/nonexistent');
|
|
211
182
|
|
|
212
|
-
if (res.status !==
|
|
213
|
-
console.log('FAIL: Expected
|
|
183
|
+
if (res.status !== 404) {
|
|
184
|
+
console.log('FAIL: Expected 404, got ' + res.status);
|
|
214
185
|
process.exit(1);
|
|
215
186
|
}
|
|
216
187
|
|
|
@@ -227,25 +198,7 @@ test_service_returns_401_for_invalid_nonce() {
|
|
|
227
198
|
fi
|
|
228
199
|
}
|
|
229
200
|
|
|
230
|
-
|
|
231
|
-
# Mobile UI Tests
|
|
232
|
-
# =============================================================================
|
|
233
|
-
|
|
234
|
-
test_service_has_mobile_session_route() {
|
|
235
|
-
grep -q "/m/" "$SERVICE_DIR/server.js" || {
|
|
236
|
-
echo "Mobile session route /m/ not found in server.js"
|
|
237
|
-
return 1
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
test_service_has_api_session_route() {
|
|
242
|
-
grep -q "/api/" "$SERVICE_DIR/server.js" || {
|
|
243
|
-
echo "API proxy route /api/ not found in server.js"
|
|
244
|
-
return 1
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
test_service_mobile_page_returns_html() {
|
|
201
|
+
test_service_handles_cors_preflight() {
|
|
249
202
|
if ! command -v node &>/dev/null; then
|
|
250
203
|
echo "SKIP: node not available"
|
|
251
204
|
return 0
|
|
@@ -257,28 +210,25 @@ test_service_mobile_page_returns_html() {
|
|
|
257
210
|
|
|
258
211
|
const config = {
|
|
259
212
|
httpPort: 0,
|
|
260
|
-
|
|
213
|
+
enablePolling: false
|
|
261
214
|
};
|
|
262
215
|
|
|
263
216
|
const service = await startService(config);
|
|
264
217
|
const port = service.httpServer.address().port;
|
|
265
218
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
219
|
+
// Send OPTIONS preflight request
|
|
220
|
+
const res = await fetch('http://localhost:' + port + '/health', {
|
|
221
|
+
method: 'OPTIONS'
|
|
222
|
+
});
|
|
272
223
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log('FAIL: Expected text/html, got ' + contentType);
|
|
224
|
+
if (res.status !== 204) {
|
|
225
|
+
console.log('FAIL: Expected 204 for OPTIONS, got ' + res.status);
|
|
276
226
|
process.exit(1);
|
|
277
227
|
}
|
|
278
228
|
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
281
|
-
console.log('FAIL:
|
|
229
|
+
const allowOrigin = res.headers.get('access-control-allow-origin');
|
|
230
|
+
if (allowOrigin !== '*') {
|
|
231
|
+
console.log('FAIL: Expected Access-Control-Allow-Origin: *, got ' + allowOrigin);
|
|
282
232
|
process.exit(1);
|
|
283
233
|
}
|
|
284
234
|
|
|
@@ -295,995 +245,37 @@ test_service_mobile_page_returns_html() {
|
|
|
295
245
|
fi
|
|
296
246
|
}
|
|
297
247
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return 0
|
|
302
|
-
fi
|
|
303
|
-
|
|
304
|
-
local result
|
|
305
|
-
result=$(node --experimental-vm-modules -e "
|
|
306
|
-
import { startService, stopService } from './service/server.js';
|
|
307
|
-
|
|
308
|
-
const config = {
|
|
309
|
-
httpPort: 0,
|
|
310
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const service = await startService(config);
|
|
314
|
-
const port = service.httpServer.address().port;
|
|
315
|
-
|
|
316
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123');
|
|
317
|
-
const html = await res.text();
|
|
318
|
-
|
|
319
|
-
// Check for text input and send button
|
|
320
|
-
if (!html.includes('<textarea') && !html.includes('<input')) {
|
|
321
|
-
console.log('FAIL: No text input found in mobile page');
|
|
322
|
-
process.exit(1);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!html.includes('Send') && !html.includes('send')) {
|
|
326
|
-
console.log('FAIL: No send button found in mobile page');
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
await stopService(service);
|
|
331
|
-
console.log('PASS');
|
|
332
|
-
" 2>&1) || {
|
|
333
|
-
echo "Functional test failed: $result"
|
|
334
|
-
return 1
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
338
|
-
echo "$result"
|
|
339
|
-
return 1
|
|
340
|
-
fi
|
|
341
|
-
}
|
|
248
|
+
# =============================================================================
|
|
249
|
+
# Run Tests
|
|
250
|
+
# =============================================================================
|
|
342
251
|
|
|
343
|
-
|
|
344
|
-
if ! command -v node &>/dev/null; then
|
|
345
|
-
echo "SKIP: node not available"
|
|
346
|
-
return 0
|
|
347
|
-
fi
|
|
348
|
-
|
|
349
|
-
# This test verifies the route exists and attempts to proxy GET requests
|
|
350
|
-
# We use a random high port that's unlikely to have a server running
|
|
351
|
-
local result
|
|
352
|
-
result=$(node --experimental-vm-modules -e "
|
|
353
|
-
import { startService, stopService } from './service/server.js';
|
|
354
|
-
|
|
355
|
-
const config = {
|
|
356
|
-
httpPort: 0,
|
|
357
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const service = await startService(config);
|
|
361
|
-
const port = service.httpServer.address().port;
|
|
362
|
-
|
|
363
|
-
// Use a random high port (59999) that's very unlikely to have a server
|
|
364
|
-
const res = await fetch('http://localhost:' + port + '/api/59999/session/ses_123');
|
|
365
|
-
|
|
366
|
-
// We expect 502 Bad Gateway since there's no server on port 59999
|
|
367
|
-
// The important thing is the route exists and attempts to proxy (not 404)
|
|
368
|
-
if (res.status !== 502) {
|
|
369
|
-
console.log('FAIL: Expected 502 (proxy target unavailable), got ' + res.status);
|
|
370
|
-
process.exit(1);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
await stopService(service);
|
|
374
|
-
console.log('PASS');
|
|
375
|
-
" 2>&1) || {
|
|
376
|
-
echo "Functional test failed: $result"
|
|
377
|
-
return 1
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
381
|
-
echo "$result"
|
|
382
|
-
return 1
|
|
383
|
-
fi
|
|
384
|
-
}
|
|
252
|
+
echo "File Structure Tests:"
|
|
385
253
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
local result
|
|
393
|
-
result=$(node --experimental-vm-modules -e "
|
|
394
|
-
import { startService, stopService } from './service/server.js';
|
|
395
|
-
|
|
396
|
-
const config = {
|
|
397
|
-
httpPort: 0,
|
|
398
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const service = await startService(config);
|
|
402
|
-
const port = service.httpServer.address().port;
|
|
403
|
-
|
|
404
|
-
// Use a random high port (59999) that's very unlikely to have a server
|
|
405
|
-
const res = await fetch('http://localhost:' + port + '/api/59999/session/ses_123/chat', {
|
|
406
|
-
method: 'POST',
|
|
407
|
-
headers: { 'Content-Type': 'application/json' },
|
|
408
|
-
body: JSON.stringify({ content: 'test' })
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
// We expect 502 Bad Gateway since there's no server on port 59999
|
|
412
|
-
if (res.status !== 502) {
|
|
413
|
-
console.log('FAIL: Expected 502 (proxy target unavailable), got ' + res.status);
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await stopService(service);
|
|
418
|
-
console.log('PASS');
|
|
419
|
-
" 2>&1) || {
|
|
420
|
-
echo "Functional test failed: $result"
|
|
421
|
-
return 1
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
425
|
-
echo "$result"
|
|
426
|
-
return 1
|
|
427
|
-
fi
|
|
428
|
-
}
|
|
254
|
+
for test_func in \
|
|
255
|
+
test_service_file_exists \
|
|
256
|
+
test_service_js_syntax
|
|
257
|
+
do
|
|
258
|
+
run_test "${test_func#test_}" "$test_func"
|
|
259
|
+
done
|
|
429
260
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
echo "SKIP: node not available"
|
|
433
|
-
return 0
|
|
434
|
-
fi
|
|
435
|
-
|
|
436
|
-
local result
|
|
437
|
-
result=$(node --experimental-vm-modules -e "
|
|
438
|
-
import { startService, stopService } from './service/server.js';
|
|
439
|
-
|
|
440
|
-
const config = {
|
|
441
|
-
httpPort: 0,
|
|
442
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const service = await startService(config);
|
|
446
|
-
const port = service.httpServer.address().port;
|
|
447
|
-
|
|
448
|
-
// Use a random high port (59999) that's very unlikely to have a server
|
|
449
|
-
const res = await fetch('http://localhost:' + port + '/api/59999/agent');
|
|
450
|
-
|
|
451
|
-
// We expect 502 Bad Gateway since there's no server on port 59999
|
|
452
|
-
// The important thing is the route exists and attempts to proxy (not 404)
|
|
453
|
-
if (res.status !== 502) {
|
|
454
|
-
console.log('FAIL: Expected 502 (proxy target unavailable), got ' + res.status);
|
|
455
|
-
process.exit(1);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
await stopService(service);
|
|
459
|
-
console.log('PASS');
|
|
460
|
-
" 2>&1) || {
|
|
461
|
-
echo "Functional test failed: $result"
|
|
462
|
-
return 1
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
466
|
-
echo "$result"
|
|
467
|
-
return 1
|
|
468
|
-
fi
|
|
469
|
-
}
|
|
261
|
+
echo ""
|
|
262
|
+
echo "Export Tests:"
|
|
470
263
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
local result
|
|
478
|
-
result=$(node --experimental-vm-modules -e "
|
|
479
|
-
import { startService, stopService } from './service/server.js';
|
|
480
|
-
|
|
481
|
-
const config = {
|
|
482
|
-
httpPort: 0,
|
|
483
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
const service = await startService(config);
|
|
487
|
-
const port = service.httpServer.address().port;
|
|
488
|
-
|
|
489
|
-
// Use a random high port (59999) that's very unlikely to have a server
|
|
490
|
-
const res = await fetch('http://localhost:' + port + '/api/59999/provider');
|
|
491
|
-
|
|
492
|
-
// We expect 502 Bad Gateway since there's no server on port 59999
|
|
493
|
-
// The important thing is the route exists and attempts to proxy (not 404)
|
|
494
|
-
if (res.status !== 502) {
|
|
495
|
-
console.log('FAIL: Expected 502 (proxy target unavailable), got ' + res.status);
|
|
496
|
-
process.exit(1);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
await stopService(service);
|
|
500
|
-
console.log('PASS');
|
|
501
|
-
" 2>&1) || {
|
|
502
|
-
echo "Functional test failed: $result"
|
|
503
|
-
return 1
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
507
|
-
echo "$result"
|
|
508
|
-
return 1
|
|
509
|
-
fi
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
test_service_mobile_ui_fetches_messages_endpoint() {
|
|
513
|
-
if ! command -v node &>/dev/null; then
|
|
514
|
-
echo "SKIP: node not available"
|
|
515
|
-
return 0
|
|
516
|
-
fi
|
|
517
|
-
|
|
518
|
-
local result
|
|
519
|
-
result=$(node --experimental-vm-modules -e "
|
|
520
|
-
import { startService, stopService } from './service/server.js';
|
|
521
|
-
|
|
522
|
-
const config = {
|
|
523
|
-
httpPort: 0,
|
|
524
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const service = await startService(config);
|
|
528
|
-
const port = service.httpServer.address().port;
|
|
529
|
-
|
|
530
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123');
|
|
531
|
-
const html = await res.text();
|
|
532
|
-
|
|
533
|
-
// The mobile UI JavaScript should fetch from /message endpoint, not expect messages in session
|
|
534
|
-
// This is critical: messages are at /session/:id/message, not embedded in /session/:id
|
|
535
|
-
if (!html.includes('/message')) {
|
|
536
|
-
console.log('FAIL: Mobile UI should fetch from /message endpoint');
|
|
537
|
-
process.exit(1);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Should NOT rely on session.messages (which doesn't exist in OpenCode API)
|
|
541
|
-
// The loadSession function should fetch messages separately
|
|
542
|
-
if (html.includes('session.messages')) {
|
|
543
|
-
console.log('FAIL: Mobile UI should not use session.messages (it does not exist in API)');
|
|
544
|
-
process.exit(1);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
await stopService(service);
|
|
548
|
-
console.log('PASS');
|
|
549
|
-
" 2>&1) || {
|
|
550
|
-
echo "Functional test failed: $result"
|
|
551
|
-
return 1
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
555
|
-
echo "$result"
|
|
556
|
-
return 1
|
|
557
|
-
fi
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
test_service_mobile_ui_parses_message_info_role() {
|
|
561
|
-
if ! command -v node &>/dev/null; then
|
|
562
|
-
echo "SKIP: node not available"
|
|
563
|
-
return 0
|
|
564
|
-
fi
|
|
565
|
-
|
|
566
|
-
local result
|
|
567
|
-
result=$(node --experimental-vm-modules -e "
|
|
568
|
-
import { startService, stopService } from './service/server.js';
|
|
569
|
-
|
|
570
|
-
const config = {
|
|
571
|
-
httpPort: 0,
|
|
572
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
const service = await startService(config);
|
|
576
|
-
const port = service.httpServer.address().port;
|
|
577
|
-
|
|
578
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123');
|
|
579
|
-
const html = await res.text();
|
|
580
|
-
|
|
581
|
-
// The mobile UI should look for role in message.info.role (OpenCode structure)
|
|
582
|
-
// not message.role (which doesn't exist at top level)
|
|
583
|
-
if (!html.includes('.info.role') && !html.includes('info\"].role')) {
|
|
584
|
-
console.log('FAIL: Mobile UI should access role via message.info.role');
|
|
585
|
-
process.exit(1);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
await stopService(service);
|
|
589
|
-
console.log('PASS');
|
|
590
|
-
" 2>&1) || {
|
|
591
|
-
echo "Functional test failed: $result"
|
|
592
|
-
return 1
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
596
|
-
echo "$result"
|
|
597
|
-
return 1
|
|
598
|
-
fi
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
test_service_mobile_ui_fetches_session_title() {
|
|
602
|
-
if ! command -v node &>/dev/null; then
|
|
603
|
-
echo "SKIP: node not available"
|
|
604
|
-
return 0
|
|
605
|
-
fi
|
|
606
|
-
|
|
607
|
-
local result
|
|
608
|
-
result=$(node --experimental-vm-modules -e "
|
|
609
|
-
import { startService, stopService } from './service/server.js';
|
|
610
|
-
|
|
611
|
-
const config = {
|
|
612
|
-
httpPort: 0,
|
|
613
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
const service = await startService(config);
|
|
617
|
-
const port = service.httpServer.address().port;
|
|
618
|
-
|
|
619
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
620
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
621
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
622
|
-
});
|
|
623
|
-
const html = await res.text();
|
|
624
|
-
|
|
625
|
-
// The mobile UI should fetch session info to get the autogenerated title
|
|
626
|
-
// This requires fetching from /session/:id endpoint and displaying the title
|
|
627
|
-
if (!html.includes('loadSessionInfo') || !html.includes('sessionTitle')) {
|
|
628
|
-
console.log('FAIL: Mobile UI should fetch and display session title');
|
|
629
|
-
process.exit(1);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
await stopService(service);
|
|
633
|
-
console.log('PASS');
|
|
634
|
-
" 2>&1) || {
|
|
635
|
-
echo "Functional test failed: $result"
|
|
636
|
-
return 1
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
640
|
-
echo "$result"
|
|
641
|
-
return 1
|
|
642
|
-
fi
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
test_service_mobile_ui_shows_conversation_history() {
|
|
646
|
-
if ! command -v node &>/dev/null; then
|
|
647
|
-
echo "SKIP: node not available"
|
|
648
|
-
return 0
|
|
649
|
-
fi
|
|
650
|
-
|
|
651
|
-
local result
|
|
652
|
-
result=$(node --experimental-vm-modules -e "
|
|
653
|
-
import { startService, stopService } from './service/server.js';
|
|
654
|
-
|
|
655
|
-
const config = {
|
|
656
|
-
httpPort: 0,
|
|
657
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
const service = await startService(config);
|
|
661
|
-
const port = service.httpServer.address().port;
|
|
662
|
-
|
|
663
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
664
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
665
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
666
|
-
});
|
|
667
|
-
const html = await res.text();
|
|
668
|
-
|
|
669
|
-
// The mobile UI should display multiple messages from conversation history
|
|
670
|
-
// Look for a container that shows all messages and the renderMessages function
|
|
671
|
-
if (!html.includes('renderMessages') || !html.includes('messages-list')) {
|
|
672
|
-
console.log('FAIL: Mobile UI should have renderMessages function AND messages-list container for conversation history');
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
await stopService(service);
|
|
677
|
-
console.log('PASS');
|
|
678
|
-
" 2>&1) || {
|
|
679
|
-
echo "Functional test failed: $result"
|
|
680
|
-
return 1
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
684
|
-
echo "$result"
|
|
685
|
-
return 1
|
|
686
|
-
fi
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
test_service_mobile_page_has_agent_selector() {
|
|
690
|
-
if ! command -v node &>/dev/null; then
|
|
691
|
-
echo "SKIP: node not available"
|
|
692
|
-
return 0
|
|
693
|
-
fi
|
|
694
|
-
|
|
695
|
-
local result
|
|
696
|
-
result=$(node --experimental-vm-modules -e "
|
|
697
|
-
import { startService, stopService } from './service/server.js';
|
|
698
|
-
|
|
699
|
-
const config = {
|
|
700
|
-
httpPort: 0,
|
|
701
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
const service = await startService(config);
|
|
705
|
-
const port = service.httpServer.address().port;
|
|
706
|
-
|
|
707
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
708
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
709
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
710
|
-
});
|
|
711
|
-
const html = await res.text();
|
|
712
|
-
|
|
713
|
-
// Should have agent selector (select element with id='agent')
|
|
714
|
-
if (!html.includes('id=\"agent\"') && !html.includes(\"id='agent'\")) {
|
|
715
|
-
console.log('FAIL: No agent selector found in mobile session page');
|
|
716
|
-
process.exit(1);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Should load agents from API
|
|
720
|
-
if (!html.includes('/agent')) {
|
|
721
|
-
console.log('FAIL: Mobile page should fetch agents from API');
|
|
722
|
-
process.exit(1);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
await stopService(service);
|
|
726
|
-
console.log('PASS');
|
|
727
|
-
" 2>&1) || {
|
|
728
|
-
echo "Functional test failed: $result"
|
|
729
|
-
return 1
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
733
|
-
echo "$result"
|
|
734
|
-
return 1
|
|
735
|
-
fi
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
test_service_mobile_page_has_model_selector() {
|
|
739
|
-
if ! command -v node &>/dev/null; then
|
|
740
|
-
echo "SKIP: node not available"
|
|
741
|
-
return 0
|
|
742
|
-
fi
|
|
743
|
-
|
|
744
|
-
local result
|
|
745
|
-
result=$(node --experimental-vm-modules -e "
|
|
746
|
-
import { startService, stopService } from './service/server.js';
|
|
747
|
-
|
|
748
|
-
const config = {
|
|
749
|
-
httpPort: 0,
|
|
750
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
751
|
-
};
|
|
752
|
-
|
|
753
|
-
const service = await startService(config);
|
|
754
|
-
const port = service.httpServer.address().port;
|
|
755
|
-
|
|
756
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
757
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
758
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
759
|
-
});
|
|
760
|
-
const html = await res.text();
|
|
761
|
-
|
|
762
|
-
// Should have model selector (select element with id='model')
|
|
763
|
-
if (!html.includes('id=\"model\"') && !html.includes(\"id='model'\")) {
|
|
764
|
-
console.log('FAIL: No model selector found in mobile session page');
|
|
765
|
-
process.exit(1);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Should load models/providers from API or favorites
|
|
769
|
-
if (!html.includes('/provider') && !html.includes('/favorites')) {
|
|
770
|
-
console.log('FAIL: Mobile page should fetch models from API or favorites');
|
|
771
|
-
process.exit(1);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
await stopService(service);
|
|
775
|
-
console.log('PASS');
|
|
776
|
-
" 2>&1) || {
|
|
777
|
-
echo "Functional test failed: $result"
|
|
778
|
-
return 1
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
782
|
-
echo "$result"
|
|
783
|
-
return 1
|
|
784
|
-
fi
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
test_service_mobile_page_sends_agent_with_message() {
|
|
788
|
-
if ! command -v node &>/dev/null; then
|
|
789
|
-
echo "SKIP: node not available"
|
|
790
|
-
return 0
|
|
791
|
-
fi
|
|
792
|
-
|
|
793
|
-
local result
|
|
794
|
-
result=$(node --experimental-vm-modules -e "
|
|
795
|
-
import { startService, stopService } from './service/server.js';
|
|
796
|
-
|
|
797
|
-
const config = {
|
|
798
|
-
httpPort: 0,
|
|
799
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
const service = await startService(config);
|
|
803
|
-
const port = service.httpServer.address().port;
|
|
804
|
-
|
|
805
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
806
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
807
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
808
|
-
});
|
|
809
|
-
const html = await res.text();
|
|
810
|
-
|
|
811
|
-
// The sendMessage function should include agent in the request body
|
|
812
|
-
// Look for 'agent:' or 'agent :' in the message body construction
|
|
813
|
-
if (!html.includes('agent:') && !html.includes('agent :')) {
|
|
814
|
-
console.log('FAIL: sendMessage should include agent in request body');
|
|
815
|
-
process.exit(1);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
await stopService(service);
|
|
819
|
-
console.log('PASS');
|
|
820
|
-
" 2>&1) || {
|
|
821
|
-
echo "Functional test failed: $result"
|
|
822
|
-
return 1
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
826
|
-
echo "$result"
|
|
827
|
-
return 1
|
|
828
|
-
fi
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
test_service_mobile_page_sends_model_with_message() {
|
|
832
|
-
if ! command -v node &>/dev/null; then
|
|
833
|
-
echo "SKIP: node not available"
|
|
834
|
-
return 0
|
|
835
|
-
fi
|
|
836
|
-
|
|
837
|
-
local result
|
|
838
|
-
result=$(node --experimental-vm-modules -e "
|
|
839
|
-
import { startService, stopService } from './service/server.js';
|
|
840
|
-
|
|
841
|
-
const config = {
|
|
842
|
-
httpPort: 0,
|
|
843
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
const service = await startService(config);
|
|
847
|
-
const port = service.httpServer.address().port;
|
|
848
|
-
|
|
849
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
850
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
851
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
852
|
-
});
|
|
853
|
-
const html = await res.text();
|
|
854
|
-
|
|
855
|
-
// The sendMessage function should include model in the request body when selected
|
|
856
|
-
// Look for model config construction (providerID/modelID)
|
|
857
|
-
if (!html.includes('providerID') || !html.includes('modelID')) {
|
|
858
|
-
console.log('FAIL: sendMessage should support model selection with providerID/modelID');
|
|
859
|
-
process.exit(1);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
await stopService(service);
|
|
863
|
-
console.log('PASS');
|
|
864
|
-
" 2>&1) || {
|
|
865
|
-
echo "Functional test failed: $result"
|
|
866
|
-
return 1
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
870
|
-
echo "$result"
|
|
871
|
-
return 1
|
|
872
|
-
fi
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
# =============================================================================
|
|
876
|
-
# Security Tests
|
|
877
|
-
# =============================================================================
|
|
878
|
-
|
|
879
|
-
test_service_rejects_privileged_ports() {
|
|
880
|
-
if ! command -v node &>/dev/null; then
|
|
881
|
-
echo "SKIP: node not available"
|
|
882
|
-
return 0
|
|
883
|
-
fi
|
|
884
|
-
|
|
885
|
-
local result
|
|
886
|
-
result=$(node --experimental-vm-modules -e "
|
|
887
|
-
import { startService, stopService } from './service/server.js';
|
|
888
|
-
|
|
889
|
-
const config = {
|
|
890
|
-
httpPort: 0,
|
|
891
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
const service = await startService(config);
|
|
895
|
-
const port = service.httpServer.address().port;
|
|
896
|
-
|
|
897
|
-
// Try to proxy to port 22 (SSH) - should be rejected
|
|
898
|
-
const res = await fetch('http://localhost:' + port + '/api/22/session/ses_123');
|
|
899
|
-
|
|
900
|
-
// Expect 400 Bad Request for invalid port, not 502 (which would mean it tried to connect)
|
|
901
|
-
if (res.status !== 400) {
|
|
902
|
-
console.log('FAIL: Expected 400 for privileged port, got ' + res.status);
|
|
903
|
-
process.exit(1);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
await stopService(service);
|
|
907
|
-
console.log('PASS');
|
|
908
|
-
" 2>&1) || {
|
|
909
|
-
echo "Functional test failed: $result"
|
|
910
|
-
return 1
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
914
|
-
echo "$result"
|
|
915
|
-
return 1
|
|
916
|
-
fi
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
test_service_rejects_low_ports() {
|
|
920
|
-
if ! command -v node &>/dev/null; then
|
|
921
|
-
echo "SKIP: node not available"
|
|
922
|
-
return 0
|
|
923
|
-
fi
|
|
924
|
-
|
|
925
|
-
local result
|
|
926
|
-
result=$(node --experimental-vm-modules -e "
|
|
927
|
-
import { startService, stopService } from './service/server.js';
|
|
928
|
-
|
|
929
|
-
const config = {
|
|
930
|
-
httpPort: 0,
|
|
931
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const service = await startService(config);
|
|
935
|
-
const port = service.httpServer.address().port;
|
|
936
|
-
|
|
937
|
-
// Try to proxy to port 80 (HTTP) - should be rejected as it's below 1024
|
|
938
|
-
const res = await fetch('http://localhost:' + port + '/api/80/session/ses_123');
|
|
939
|
-
|
|
940
|
-
if (res.status !== 400) {
|
|
941
|
-
console.log('FAIL: Expected 400 for low port, got ' + res.status);
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
await stopService(service);
|
|
946
|
-
console.log('PASS');
|
|
947
|
-
" 2>&1) || {
|
|
948
|
-
echo "Functional test failed: $result"
|
|
949
|
-
return 1
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
953
|
-
echo "$result"
|
|
954
|
-
return 1
|
|
955
|
-
fi
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
test_service_escapes_html_in_reponame() {
|
|
959
|
-
if ! command -v node &>/dev/null; then
|
|
960
|
-
echo "SKIP: node not available"
|
|
961
|
-
return 0
|
|
962
|
-
fi
|
|
963
|
-
|
|
964
|
-
local result
|
|
965
|
-
result=$(node --experimental-vm-modules -e "
|
|
966
|
-
import { startService, stopService } from './service/server.js';
|
|
967
|
-
|
|
968
|
-
const config = {
|
|
969
|
-
httpPort: 0,
|
|
970
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
const service = await startService(config);
|
|
974
|
-
const port = service.httpServer.address().port;
|
|
975
|
-
|
|
976
|
-
// Try XSS via repo name
|
|
977
|
-
const xssPayload = encodeURIComponent('<script>alert(1)</script>');
|
|
978
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/' + xssPayload + '/session/ses_123');
|
|
979
|
-
const html = await res.text();
|
|
980
|
-
|
|
981
|
-
// The script tag should be escaped, not raw
|
|
982
|
-
if (html.includes('<script>alert(1)</script>')) {
|
|
983
|
-
console.log('FAIL: XSS payload was not escaped in HTML');
|
|
984
|
-
process.exit(1);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// Should contain escaped version
|
|
988
|
-
if (!html.includes('<script>') && !html.includes('\\\\u003c')) {
|
|
989
|
-
console.log('FAIL: Expected escaped script tag');
|
|
990
|
-
process.exit(1);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
await stopService(service);
|
|
994
|
-
console.log('PASS');
|
|
995
|
-
" 2>&1) || {
|
|
996
|
-
echo "Functional test failed: $result"
|
|
997
|
-
return 1
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
1001
|
-
echo "$result"
|
|
1002
|
-
return 1
|
|
1003
|
-
fi
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
test_service_markdown_renderer_escapes_xss() {
|
|
1007
|
-
if ! command -v node &>/dev/null; then
|
|
1008
|
-
echo "SKIP: node not available"
|
|
1009
|
-
return 0
|
|
1010
|
-
fi
|
|
1011
|
-
|
|
1012
|
-
local result
|
|
1013
|
-
result=$(node --experimental-vm-modules -e "
|
|
1014
|
-
import { startService, stopService } from './service/server.js';
|
|
1015
|
-
|
|
1016
|
-
const config = {
|
|
1017
|
-
httpPort: 0,
|
|
1018
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
1019
|
-
};
|
|
1020
|
-
|
|
1021
|
-
const service = await startService(config);
|
|
1022
|
-
const port = service.httpServer.address().port;
|
|
1023
|
-
|
|
1024
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123');
|
|
1025
|
-
const html = await res.text();
|
|
1026
|
-
|
|
1027
|
-
// Extract the renderMarkdown function and test it
|
|
1028
|
-
// The function should escape HTML before applying markdown
|
|
1029
|
-
// We verify by checking the escapeHtml is called before regex replacements
|
|
1030
|
-
|
|
1031
|
-
// Check that escapeHtml is defined and called first in renderMarkdown
|
|
1032
|
-
if (!html.includes('function escapeHtml')) {
|
|
1033
|
-
console.log('FAIL: escapeHtml function not found');
|
|
1034
|
-
process.exit(1);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (!html.includes('function renderMarkdown')) {
|
|
1038
|
-
console.log('FAIL: renderMarkdown function not found');
|
|
1039
|
-
process.exit(1);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Verify escapeHtml is called at the start of renderMarkdown
|
|
1043
|
-
// The pattern should be: escapeHtml(text) before any .replace() calls
|
|
1044
|
-
const renderMarkdownMatch = html.match(/function renderMarkdown[\\s\\S]*?escapeHtml\\(text\\)/);
|
|
1045
|
-
if (!renderMarkdownMatch) {
|
|
1046
|
-
console.log('FAIL: renderMarkdown should call escapeHtml(text) before processing');
|
|
1047
|
-
process.exit(1);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Verify the escapeHtml function uses safe DOM-based escaping
|
|
1051
|
-
if (!html.includes('textContent') || !html.includes('innerHTML')) {
|
|
1052
|
-
console.log('FAIL: escapeHtml should use DOM-based escaping (textContent -> innerHTML)');
|
|
1053
|
-
process.exit(1);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
await stopService(service);
|
|
1057
|
-
console.log('PASS');
|
|
1058
|
-
" 2>&1) || {
|
|
1059
|
-
echo "Functional test failed: $result"
|
|
1060
|
-
return 1
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
1064
|
-
echo "$result"
|
|
1065
|
-
return 1
|
|
1066
|
-
fi
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
test_service_handles_cors_preflight() {
|
|
1070
|
-
if ! command -v node &>/dev/null; then
|
|
1071
|
-
echo "SKIP: node not available"
|
|
1072
|
-
return 0
|
|
1073
|
-
fi
|
|
1074
|
-
|
|
1075
|
-
local result
|
|
1076
|
-
result=$(node --experimental-vm-modules -e "
|
|
1077
|
-
import { startService, stopService } from './service/server.js';
|
|
1078
|
-
|
|
1079
|
-
const config = {
|
|
1080
|
-
httpPort: 0,
|
|
1081
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
1082
|
-
};
|
|
1083
|
-
|
|
1084
|
-
const service = await startService(config);
|
|
1085
|
-
const port = service.httpServer.address().port;
|
|
1086
|
-
|
|
1087
|
-
// Send OPTIONS preflight request
|
|
1088
|
-
const res = await fetch('http://localhost:' + port + '/api/4096/session/ses_123', {
|
|
1089
|
-
method: 'OPTIONS'
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
if (res.status !== 204) {
|
|
1093
|
-
console.log('FAIL: Expected 204 for OPTIONS, got ' + res.status);
|
|
1094
|
-
process.exit(1);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const allowOrigin = res.headers.get('access-control-allow-origin');
|
|
1098
|
-
if (allowOrigin !== '*') {
|
|
1099
|
-
console.log('FAIL: Expected Access-Control-Allow-Origin: *, got ' + allowOrigin);
|
|
1100
|
-
process.exit(1);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
await stopService(service);
|
|
1104
|
-
console.log('PASS');
|
|
1105
|
-
" 2>&1) || {
|
|
1106
|
-
echo "Functional test failed: $result"
|
|
1107
|
-
return 1
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
1111
|
-
echo "$result"
|
|
1112
|
-
return 1
|
|
1113
|
-
fi
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
test_service_mobile_ui_uses_dynamic_viewport_units() {
|
|
1117
|
-
if ! command -v node &>/dev/null; then
|
|
1118
|
-
echo "SKIP: node not available"
|
|
1119
|
-
return 0
|
|
1120
|
-
fi
|
|
1121
|
-
|
|
1122
|
-
local result
|
|
1123
|
-
result=$(node --experimental-vm-modules -e "
|
|
1124
|
-
import { startService, stopService } from './service/server.js';
|
|
1125
|
-
|
|
1126
|
-
const config = {
|
|
1127
|
-
httpPort: 0,
|
|
1128
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
const service = await startService(config);
|
|
1132
|
-
const port = service.httpServer.address().port;
|
|
1133
|
-
|
|
1134
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
1135
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
1136
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
1137
|
-
});
|
|
1138
|
-
const html = await res.text();
|
|
1139
|
-
|
|
1140
|
-
// Issue #40: Mobile UI should use dvh (dynamic viewport height) units
|
|
1141
|
-
// instead of vh to handle mobile keyboard viewport changes
|
|
1142
|
-
if (!html.includes('dvh') && !html.includes('100svh')) {
|
|
1143
|
-
console.log('FAIL: Mobile UI should use dynamic viewport units (dvh/svh) for proper mobile keyboard handling');
|
|
1144
|
-
process.exit(1);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// Should not use 100vh for min-height as it doesn't account for keyboard
|
|
1148
|
-
if (html.includes('min-height: 100vh')) {
|
|
1149
|
-
console.log('FAIL: Mobile UI should not use 100vh for min-height (use dvh instead)');
|
|
1150
|
-
process.exit(1);
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
await stopService(service);
|
|
1154
|
-
console.log('PASS');
|
|
1155
|
-
" 2>&1) || {
|
|
1156
|
-
echo "Functional test failed: $result"
|
|
1157
|
-
return 1
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
1161
|
-
echo "$result"
|
|
1162
|
-
return 1
|
|
1163
|
-
fi
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
test_service_mobile_ui_has_viewport_resize_handler() {
|
|
1167
|
-
if ! command -v node &>/dev/null; then
|
|
1168
|
-
echo "SKIP: node not available"
|
|
1169
|
-
return 0
|
|
1170
|
-
fi
|
|
1171
|
-
|
|
1172
|
-
local result
|
|
1173
|
-
result=$(node --experimental-vm-modules -e "
|
|
1174
|
-
import { startService, stopService } from './service/server.js';
|
|
1175
|
-
|
|
1176
|
-
const config = {
|
|
1177
|
-
httpPort: 0,
|
|
1178
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
const service = await startService(config);
|
|
1182
|
-
const port = service.httpServer.address().port;
|
|
1183
|
-
|
|
1184
|
-
// Include X-Forwarded-Proto header to bypass HTTPS redirect in test
|
|
1185
|
-
const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123', {
|
|
1186
|
-
headers: { 'X-Forwarded-Proto': 'https' }
|
|
1187
|
-
});
|
|
1188
|
-
const html = await res.text();
|
|
1189
|
-
|
|
1190
|
-
// Issue #40: Mobile UI should use visualViewport API to handle keyboard resize
|
|
1191
|
-
if (!html.includes('visualViewport')) {
|
|
1192
|
-
console.log('FAIL: Mobile UI should use visualViewport API for keyboard handling');
|
|
1193
|
-
process.exit(1);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
await stopService(service);
|
|
1197
|
-
console.log('PASS');
|
|
1198
|
-
" 2>&1) || {
|
|
1199
|
-
echo "Functional test failed: $result"
|
|
1200
|
-
return 1
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
1204
|
-
echo "$result"
|
|
1205
|
-
return 1
|
|
1206
|
-
fi
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
test_service_rejects_large_request_body() {
|
|
1210
|
-
if ! command -v node &>/dev/null; then
|
|
1211
|
-
echo "SKIP: node not available"
|
|
1212
|
-
return 0
|
|
1213
|
-
fi
|
|
1214
|
-
|
|
1215
|
-
local result
|
|
1216
|
-
result=$(node --experimental-vm-modules -e "
|
|
1217
|
-
import { startService, stopService } from './service/server.js';
|
|
1218
|
-
|
|
1219
|
-
const config = {
|
|
1220
|
-
httpPort: 0,
|
|
1221
|
-
socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
|
|
1222
|
-
};
|
|
1223
|
-
|
|
1224
|
-
const service = await startService(config);
|
|
1225
|
-
const port = service.httpServer.address().port;
|
|
1226
|
-
|
|
1227
|
-
// Send a 2MB body (should be rejected if limit is 1MB)
|
|
1228
|
-
const largeBody = 'x'.repeat(2 * 1024 * 1024);
|
|
1229
|
-
const res = await fetch('http://localhost:' + port + '/api/4096/session/ses_123/chat', {
|
|
1230
|
-
method: 'POST',
|
|
1231
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1232
|
-
body: JSON.stringify({ content: largeBody })
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
if (res.status !== 413) {
|
|
1236
|
-
console.log('FAIL: Expected 413 for large body, got ' + res.status);
|
|
1237
|
-
process.exit(1);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
await stopService(service);
|
|
1241
|
-
console.log('PASS');
|
|
1242
|
-
" 2>&1) || {
|
|
1243
|
-
echo "Functional test failed: $result"
|
|
1244
|
-
return 1
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
if ! echo "$result" | grep -q "PASS"; then
|
|
1248
|
-
echo "$result"
|
|
1249
|
-
return 1
|
|
1250
|
-
fi
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
# =============================================================================
|
|
1254
|
-
# Run Tests
|
|
1255
|
-
# =============================================================================
|
|
1256
|
-
|
|
1257
|
-
echo "File Structure Tests:"
|
|
1258
|
-
|
|
1259
|
-
for test_func in \
|
|
1260
|
-
test_service_file_exists \
|
|
1261
|
-
test_service_js_syntax
|
|
1262
|
-
do
|
|
1263
|
-
run_test "${test_func#test_}" "$test_func"
|
|
1264
|
-
done
|
|
1265
|
-
|
|
1266
|
-
echo ""
|
|
1267
|
-
echo "Export Tests:"
|
|
1268
|
-
|
|
1269
|
-
for test_func in \
|
|
1270
|
-
test_service_exports_start_service \
|
|
1271
|
-
test_service_exports_stop_service
|
|
1272
|
-
do
|
|
1273
|
-
run_test "${test_func#test_}" "$test_func"
|
|
1274
|
-
done
|
|
264
|
+
for test_func in \
|
|
265
|
+
test_service_exports_start_service \
|
|
266
|
+
test_service_exports_stop_service
|
|
267
|
+
do
|
|
268
|
+
run_test "${test_func#test_}" "$test_func"
|
|
269
|
+
done
|
|
1275
270
|
|
|
1276
271
|
echo ""
|
|
1277
272
|
echo "Implementation Tests:"
|
|
1278
273
|
|
|
1279
274
|
for test_func in \
|
|
1280
275
|
test_service_has_http_server \
|
|
1281
|
-
test_service_has_unix_socket \
|
|
1282
276
|
test_service_has_health_endpoint \
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
test_service_handles_nonce_creation \
|
|
1286
|
-
test_service_logs_with_prefix
|
|
277
|
+
test_service_logs_with_prefix \
|
|
278
|
+
test_service_imports_poll_service
|
|
1287
279
|
do
|
|
1288
280
|
run_test "${test_func#test_}" "$test_func"
|
|
1289
281
|
done
|
|
@@ -1294,47 +286,8 @@ echo "Functional Tests:"
|
|
|
1294
286
|
for test_func in \
|
|
1295
287
|
test_service_starts_and_stops \
|
|
1296
288
|
test_service_health_endpoint_returns_200 \
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
run_test "${test_func#test_}" "$test_func"
|
|
1300
|
-
done
|
|
1301
|
-
|
|
1302
|
-
echo ""
|
|
1303
|
-
echo "Mobile UI Tests:"
|
|
1304
|
-
|
|
1305
|
-
for test_func in \
|
|
1306
|
-
test_service_has_mobile_session_route \
|
|
1307
|
-
test_service_has_api_session_route \
|
|
1308
|
-
test_service_mobile_page_returns_html \
|
|
1309
|
-
test_service_mobile_page_has_text_input \
|
|
1310
|
-
test_service_api_session_proxies_get \
|
|
1311
|
-
test_service_api_chat_proxies_post \
|
|
1312
|
-
test_service_api_agent_proxies_get \
|
|
1313
|
-
test_service_api_provider_proxies_get \
|
|
1314
|
-
test_service_mobile_ui_fetches_messages_endpoint \
|
|
1315
|
-
test_service_mobile_ui_parses_message_info_role \
|
|
1316
|
-
test_service_mobile_ui_fetches_session_title \
|
|
1317
|
-
test_service_mobile_ui_shows_conversation_history \
|
|
1318
|
-
test_service_mobile_ui_uses_dynamic_viewport_units \
|
|
1319
|
-
test_service_mobile_ui_has_viewport_resize_handler \
|
|
1320
|
-
test_service_mobile_page_has_agent_selector \
|
|
1321
|
-
test_service_mobile_page_has_model_selector \
|
|
1322
|
-
test_service_mobile_page_sends_agent_with_message \
|
|
1323
|
-
test_service_mobile_page_sends_model_with_message
|
|
1324
|
-
do
|
|
1325
|
-
run_test "${test_func#test_}" "$test_func"
|
|
1326
|
-
done
|
|
1327
|
-
|
|
1328
|
-
echo ""
|
|
1329
|
-
echo "Security Tests:"
|
|
1330
|
-
|
|
1331
|
-
for test_func in \
|
|
1332
|
-
test_service_rejects_privileged_ports \
|
|
1333
|
-
test_service_rejects_low_ports \
|
|
1334
|
-
test_service_escapes_html_in_reponame \
|
|
1335
|
-
test_service_markdown_renderer_escapes_xss \
|
|
1336
|
-
test_service_handles_cors_preflight \
|
|
1337
|
-
test_service_rejects_large_request_body
|
|
289
|
+
test_service_returns_404_for_unknown_route \
|
|
290
|
+
test_service_handles_cors_preflight
|
|
1338
291
|
do
|
|
1339
292
|
run_test "${test_func#test_}" "$test_func"
|
|
1340
293
|
done
|