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.
- 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/server.js +44 -1381
- package/test/unit/paths.test.js +4 -46
- 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/service/io.opencode.ntfy.plist +0 -29
- package/test/run_tests.bash +0 -34
- package/test/test_actions.bash +0 -263
- package/test/test_cli.bash +0 -161
- package/test/test_config.bash +0 -438
- package/test/test_helper.bash +0 -140
- package/test/test_logger.bash +0 -401
- package/test/test_notifier.bash +0 -310
- package/test/test_plist.bash +0 -125
- package/test/test_plugin.bash +0 -952
- package/test/test_poll_service.bash +0 -179
- package/test/test_poller.bash +0 -120
- package/test/test_readiness.bash +0 -313
- package/test/test_repo_config.bash +0 -406
- package/test/test_service.bash +0 -1342
- package/test/unit/config.test.js +0 -86
package/test/test_service.bash
DELETED
|
@@ -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('<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
|
|
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
|