opencode-pilot 0.1.0 → 0.2.1

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.
@@ -1,1342 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Tests for service/server.js - Standalone callback server as brew service
4
- # Issue #13: Separate callback server as brew service
5
- #
6
-
7
- set -euo pipefail
8
-
9
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
- source "$SCRIPT_DIR/test_helper.bash"
11
-
12
- SERVICE_DIR="$(dirname "$SCRIPT_DIR")/service"
13
-
14
- # Disable HTTPS redirect for tests (overrides any user config)
15
- export NTFY_CALLBACK_HTTPS=false
16
-
17
- echo "Testing service/server.js module..."
18
- echo ""
19
-
20
- # =============================================================================
21
- # File Structure Tests
22
- # =============================================================================
23
-
24
- test_service_file_exists() {
25
- assert_file_exists "$SERVICE_DIR/server.js"
26
- }
27
-
28
- test_service_js_syntax() {
29
- if ! command -v node &>/dev/null; then
30
- echo "SKIP: node not available"
31
- return 0
32
- fi
33
- node --check "$SERVICE_DIR/server.js" 2>&1 || {
34
- echo "server.js has syntax errors"
35
- return 1
36
- }
37
- }
38
-
39
- # =============================================================================
40
- # Export Tests
41
- # =============================================================================
42
-
43
- test_service_exports_start_service() {
44
- grep -q "export.*function startService\|export.*startService" "$SERVICE_DIR/server.js" || {
45
- echo "startService export not found in server.js"
46
- return 1
47
- }
48
- }
49
-
50
- test_service_exports_stop_service() {
51
- grep -q "export.*function stopService\|export.*stopService" "$SERVICE_DIR/server.js" || {
52
- echo "stopService export not found in server.js"
53
- return 1
54
- }
55
- }
56
-
57
- # =============================================================================
58
- # Implementation Tests
59
- # =============================================================================
60
-
61
- test_service_has_http_server() {
62
- grep -q "createServer\|http" "$SERVICE_DIR/server.js" || {
63
- echo "HTTP server not found in server.js"
64
- return 1
65
- }
66
- }
67
-
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
- test_service_has_health_endpoint() {
76
- grep -q "/health" "$SERVICE_DIR/server.js" || {
77
- echo "/health endpoint not found in server.js"
78
- return 1
79
- }
80
- }
81
-
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
- test_service_logs_with_prefix() {
104
- grep -q "\[opencode-pilot\]" "$SERVICE_DIR/server.js" || {
105
- echo "Logging prefix [opencode-pilot] not found in server.js"
106
- return 1
107
- }
108
- }
109
-
110
-
111
-
112
- # =============================================================================
113
- # Functional Tests (requires Node.js)
114
- # =============================================================================
115
-
116
- test_service_starts_and_stops() {
117
- if ! command -v node &>/dev/null; then
118
- echo "SKIP: node not available"
119
- return 0
120
- fi
121
-
122
- local result
123
- result=$(node --experimental-vm-modules -e "
124
- import { startService, stopService } from './service/server.js';
125
-
126
- // Use random ports/sockets to avoid conflicts
127
- const config = {
128
- httpPort: 0,
129
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
130
- };
131
-
132
- const service = await startService(config);
133
-
134
- if (!service.httpServer) {
135
- console.log('FAIL: HTTP server not started');
136
- process.exit(1);
137
- }
138
-
139
- await stopService(service);
140
- console.log('PASS');
141
- " 2>&1) || {
142
- echo "Functional test failed: $result"
143
- return 1
144
- }
145
-
146
- if ! echo "$result" | grep -q "PASS"; then
147
- echo "$result"
148
- return 1
149
- fi
150
- }
151
-
152
- test_service_health_endpoint_returns_200() {
153
- if ! command -v node &>/dev/null; then
154
- echo "SKIP: node not available"
155
- return 0
156
- fi
157
-
158
- local result
159
- result=$(node --experimental-vm-modules -e "
160
- import { startService, stopService } from './service/server.js';
161
-
162
- const config = {
163
- httpPort: 0,
164
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
165
- };
166
-
167
- const service = await startService(config);
168
- const port = service.httpServer.address().port;
169
-
170
- const res = await fetch('http://localhost:' + port + '/health');
171
-
172
- if (res.status !== 200) {
173
- console.log('FAIL: Health check returned ' + res.status);
174
- process.exit(1);
175
- }
176
-
177
- await stopService(service);
178
- console.log('PASS');
179
- " 2>&1) || {
180
- echo "Functional test failed: $result"
181
- return 1
182
- }
183
-
184
- if ! echo "$result" | grep -q "PASS"; then
185
- echo "$result"
186
- return 1
187
- fi
188
- }
189
-
190
- test_service_returns_401_for_invalid_nonce() {
191
- if ! command -v node &>/dev/null; then
192
- echo "SKIP: node not available"
193
- return 0
194
- fi
195
-
196
- local result
197
- result=$(node --experimental-vm-modules -e "
198
- import { startService, stopService } from './service/server.js';
199
-
200
- const config = {
201
- httpPort: 0,
202
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
203
- };
204
-
205
- const service = await startService(config);
206
- const port = service.httpServer.address().port;
207
-
208
- const res = await fetch('http://localhost:' + port + '/callback?nonce=invalid&response=once', {
209
- method: 'POST'
210
- });
211
-
212
- if (res.status !== 401) {
213
- console.log('FAIL: Expected 401, got ' + res.status);
214
- process.exit(1);
215
- }
216
-
217
- await stopService(service);
218
- console.log('PASS');
219
- " 2>&1) || {
220
- echo "Functional test failed: $result"
221
- return 1
222
- }
223
-
224
- if ! echo "$result" | grep -q "PASS"; then
225
- echo "$result"
226
- return 1
227
- fi
228
- }
229
-
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() {
249
- if ! command -v node &>/dev/null; then
250
- echo "SKIP: node not available"
251
- return 0
252
- fi
253
-
254
- local result
255
- result=$(node --experimental-vm-modules -e "
256
- import { startService, stopService } from './service/server.js';
257
-
258
- const config = {
259
- httpPort: 0,
260
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
261
- };
262
-
263
- const service = await startService(config);
264
- const port = service.httpServer.address().port;
265
-
266
- const res = await fetch('http://localhost:' + port + '/m/4096/myrepo/session/ses_123');
267
-
268
- if (res.status !== 200) {
269
- console.log('FAIL: Mobile page returned ' + res.status);
270
- process.exit(1);
271
- }
272
-
273
- const contentType = res.headers.get('content-type');
274
- if (!contentType || !contentType.includes('text/html')) {
275
- console.log('FAIL: Expected text/html, got ' + contentType);
276
- process.exit(1);
277
- }
278
-
279
- const html = await res.text();
280
- if (!html.includes('<!DOCTYPE html>')) {
281
- console.log('FAIL: Response is not valid HTML');
282
- process.exit(1);
283
- }
284
-
285
- await stopService(service);
286
- console.log('PASS');
287
- " 2>&1) || {
288
- echo "Functional test failed: $result"
289
- return 1
290
- }
291
-
292
- if ! echo "$result" | grep -q "PASS"; then
293
- echo "$result"
294
- return 1
295
- fi
296
- }
297
-
298
- test_service_mobile_page_has_text_input() {
299
- if ! command -v node &>/dev/null; then
300
- echo "SKIP: node not available"
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
- }
342
-
343
- test_service_api_session_proxies_get() {
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
- }
385
-
386
- test_service_api_chat_proxies_post() {
387
- if ! command -v node &>/dev/null; then
388
- echo "SKIP: node not available"
389
- return 0
390
- fi
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
- }
429
-
430
- test_service_api_agent_proxies_get() {
431
- if ! command -v node &>/dev/null; then
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
- }
470
-
471
- test_service_api_provider_proxies_get() {
472
- if ! command -v node &>/dev/null; then
473
- echo "SKIP: node not available"
474
- return 0
475
- fi
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('&lt;script&gt;') && !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
1275
-
1276
- echo ""
1277
- echo "Implementation Tests:"
1278
-
1279
- for test_func in \
1280
- test_service_has_http_server \
1281
- test_service_has_unix_socket \
1282
- test_service_has_health_endpoint \
1283
- test_service_has_callback_endpoint \
1284
- test_service_handles_session_registration \
1285
- test_service_handles_nonce_creation \
1286
- test_service_logs_with_prefix
1287
- do
1288
- run_test "${test_func#test_}" "$test_func"
1289
- done
1290
-
1291
- echo ""
1292
- echo "Functional Tests:"
1293
-
1294
- for test_func in \
1295
- test_service_starts_and_stops \
1296
- test_service_health_endpoint_returns_200 \
1297
- test_service_returns_401_for_invalid_nonce
1298
- do
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
1338
- do
1339
- run_test "${test_func#test_}" "$test_func"
1340
- done
1341
-
1342
- print_summary