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.
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # Tests for service/server.js - Standalone callback server as brew service
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 ports/sockets to avoid conflicts
99
+ // Use random port to avoid conflicts
127
100
  const config = {
128
101
  httpPort: 0,
129
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
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
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
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
- test_service_returns_401_for_invalid_nonce() {
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
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
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 + '/callback?nonce=invalid&response=once', {
209
- method: 'POST'
210
- });
181
+ const res = await fetch('http://localhost:' + port + '/nonexistent');
211
182
 
212
- if (res.status !== 401) {
213
- console.log('FAIL: Expected 401, got ' + res.status);
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
- socketPath: '/tmp/opencode-pilot-test-' + process.pid + '.sock'
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
- 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
- }
219
+ // Send OPTIONS preflight request
220
+ const res = await fetch('http://localhost:' + port + '/health', {
221
+ method: 'OPTIONS'
222
+ });
272
223
 
273
- const contentType = res.headers.get('content-type');
274
- if (!contentType || !contentType.includes('text/html')) {
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 html = await res.text();
280
- if (!html.includes('<!DOCTYPE html>')) {
281
- console.log('FAIL: Response is not valid HTML');
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
- 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
- }
248
+ # =============================================================================
249
+ # Run Tests
250
+ # =============================================================================
342
251
 
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
- }
252
+ echo "File Structure Tests:"
385
253
 
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
- }
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
- 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
- }
261
+ echo ""
262
+ echo "Export Tests:"
470
263
 
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
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
- test_service_has_callback_endpoint \
1284
- test_service_handles_session_registration \
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
- 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
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