@p10i/create-nestbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +34 -0
- package/.env.example +35 -0
- package/.gitattributes +10 -0
- package/INSTALL.md +258 -0
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/bin/create-nestbox.js +184 -0
- package/docker/nginx/Dockerfile +31 -0
- package/docker/opencode/Dockerfile +36 -0
- package/docker/opencode/entrypoint.sh +9 -0
- package/docker/php-fpm/Dockerfile +43 -0
- package/docker/rollup/Dockerfile +16 -0
- package/docker-compose.yaml +139 -0
- package/home/code/.user.ini +1 -0
- package/home/code/404.php +104 -0
- package/home/code/__data/.gitignore +2 -0
- package/home/code/__includes/footer.php +3 -0
- package/home/code/__includes/functions.php +325 -0
- package/home/code/__includes/header.php +20 -0
- package/home/code/__templates/super-document.php +47 -0
- package/home/code/scripts-bundle.css +13295 -0
- package/home/code/scripts-bundle.js +6277 -0
- package/home/code/scripts.js +3 -0
- package/home/code/styles.css +36 -0
- package/home/configs/nginx/nginx.conf +199 -0
- package/home/configs/opencode/instance.example.md +16 -0
- package/home/configs/opencode/instructions.md +308 -0
- package/home/configs/opencode/opencode.json +15 -0
- package/home/configs/php/cli.php +33 -0
- package/home/configs/php/composer.json +10 -0
- package/home/configs/php/composer.lock +805 -0
- package/home/configs/php/index.php +105 -0
- package/home/configs/php/init.php +3 -0
- package/home/configs/php/php.ini +5 -0
- package/home/configs/php/preload.php +5 -0
- package/home/configs/rollup/package-lock.json +2089 -0
- package/home/configs/rollup/package.json +16 -0
- package/home/configs/rollup/rollup.config.js +28 -0
- package/package.json +42 -0
- package/scripts/check-package.js +31 -0
- package/template.gitignore +20 -0
- package/tests/nestbox.ps1 +322 -0
- package/tests/nestbox.sh +438 -0
package/tests/nestbox.sh
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
START=0
|
|
6
|
+
TIMEOUT=30
|
|
7
|
+
PAGE_ROOT_ARG=""
|
|
8
|
+
PASSES=0
|
|
9
|
+
FAILURES=0
|
|
10
|
+
SKIPS=0
|
|
11
|
+
|
|
12
|
+
usage() {
|
|
13
|
+
printf 'Usage: %s [--start] [--timeout seconds] [--page-root path]\n' "$0"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
while (($#)); do
|
|
17
|
+
case "$1" in
|
|
18
|
+
--start)
|
|
19
|
+
START=1
|
|
20
|
+
;;
|
|
21
|
+
--timeout)
|
|
22
|
+
shift
|
|
23
|
+
TIMEOUT="${1:-}"
|
|
24
|
+
[[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { usage; exit 2; }
|
|
25
|
+
;;
|
|
26
|
+
--page-root)
|
|
27
|
+
shift
|
|
28
|
+
PAGE_ROOT_ARG="${1:-}"
|
|
29
|
+
[[ -n "$PAGE_ROOT_ARG" ]] || { usage; exit 2; }
|
|
30
|
+
;;
|
|
31
|
+
--help|-h)
|
|
32
|
+
usage
|
|
33
|
+
exit 0
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
usage
|
|
37
|
+
exit 2
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
shift
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
44
|
+
PAGE_ROOT="${PAGE_ROOT_ARG:-${NESTBOX_PAGE_PATH:-$ROOT/home/code}}"
|
|
45
|
+
if [[ "$PAGE_ROOT" != /* ]]; then
|
|
46
|
+
PAGE_ROOT="$ROOT/$PAGE_ROOT"
|
|
47
|
+
fi
|
|
48
|
+
if [[ -n "$PAGE_ROOT_ARG" ]]; then
|
|
49
|
+
export NESTBOX_PAGE_PATH="$PAGE_ROOT"
|
|
50
|
+
fi
|
|
51
|
+
COMPOSE=(docker compose --project-directory "$ROOT")
|
|
52
|
+
TEMP_DIR=""
|
|
53
|
+
SSE_PID=""
|
|
54
|
+
|
|
55
|
+
cleanup() {
|
|
56
|
+
if [[ -n "$SSE_PID" ]] && kill -0 "$SSE_PID" 2>/dev/null; then
|
|
57
|
+
kill "$SSE_PID" 2>/dev/null || true
|
|
58
|
+
wait "$SSE_PID" 2>/dev/null || true
|
|
59
|
+
fi
|
|
60
|
+
[[ -z "$TEMP_DIR" ]] || rm -rf "$TEMP_DIR"
|
|
61
|
+
}
|
|
62
|
+
trap cleanup EXIT
|
|
63
|
+
|
|
64
|
+
pass() {
|
|
65
|
+
PASSES=$((PASSES + 1))
|
|
66
|
+
printf 'PASS %s\n' "$1"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fail() {
|
|
70
|
+
FAILURES=$((FAILURES + 1))
|
|
71
|
+
printf 'FAIL %s\n' "$1" >&2
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
skip() {
|
|
75
|
+
SKIPS=$((SKIPS + 1))
|
|
76
|
+
printf 'SKIP %s\n' "$1"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
require_command() {
|
|
80
|
+
if command -v "$1" >/dev/null 2>&1; then
|
|
81
|
+
pass "$1 is available"
|
|
82
|
+
else
|
|
83
|
+
fail "$1 is required"
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
compose() {
|
|
89
|
+
"${COMPOSE[@]}" "$@"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
container_id() {
|
|
93
|
+
compose ps -q "$1" 2>/dev/null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
container_running() {
|
|
97
|
+
local id
|
|
98
|
+
id="$(container_id "$1")"
|
|
99
|
+
[[ -n "$id" ]] && [[ "$(docker inspect --format '{{.State.Running}}' "$id" 2>/dev/null)" == "true" ]]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
restart_count() {
|
|
103
|
+
local id
|
|
104
|
+
id="$(container_id "$1")"
|
|
105
|
+
[[ -n "$id" ]] || return 1
|
|
106
|
+
docker inspect --format '{{.RestartCount}}' "$id" 2>/dev/null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
http_request() {
|
|
110
|
+
local path="$1"
|
|
111
|
+
local host_header="${2:-}"
|
|
112
|
+
local method="${3:-GET}"
|
|
113
|
+
local body_file="$TEMP_DIR/body"
|
|
114
|
+
local header_file="$TEMP_DIR/headers"
|
|
115
|
+
local args=(--silent --show-error --noproxy '*' --max-time "$TIMEOUT" --request "$method" --output "$body_file" --dump-header "$header_file" --write-out '%{http_code}')
|
|
116
|
+
[[ -z "$host_header" ]] || args+=(--header "Host: $host_header")
|
|
117
|
+
curl "${args[@]}" "$BASE_URL$path"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
header_value() {
|
|
121
|
+
local name="$1"
|
|
122
|
+
local wanted
|
|
123
|
+
wanted="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]'):"
|
|
124
|
+
awk -v wanted="$wanted" '
|
|
125
|
+
{ line=$0; sub(/\r$/, "", line); lower=tolower(line) }
|
|
126
|
+
index(lower, wanted) == 1 { sub(/^[^:]*:[[:space:]]*/, "", line); value=line }
|
|
127
|
+
END { print value }
|
|
128
|
+
' "$TEMP_DIR/headers"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if ! require_command docker || ! require_command curl; then
|
|
132
|
+
printf '\n%d passed, %d failed, %d skipped\n' "$PASSES" "$FAILURES" "$SKIPS"
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if docker compose version >/dev/null 2>&1; then
|
|
137
|
+
pass 'Docker Compose v2 is available'
|
|
138
|
+
else
|
|
139
|
+
fail 'Docker Compose v2 is required'
|
|
140
|
+
exit 1
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/nestbox-test.XXXXXX")"
|
|
144
|
+
|
|
145
|
+
[[ -f "$ROOT/.env" ]] && pass '.env exists' || fail '.env is missing'
|
|
146
|
+
[[ ! -d "$ROOT/home/.git" ]] && pass 'Page and configuration tree has no embedded Git repository' || fail 'home/.git must not be included in an installation'
|
|
147
|
+
if [[ -f "$PAGE_ROOT/index.php" ]]; then
|
|
148
|
+
INDEX_PRESENT=1
|
|
149
|
+
pass 'Page tree has an implemented index.php'
|
|
150
|
+
else
|
|
151
|
+
INDEX_PRESENT=0
|
|
152
|
+
pass 'Fresh page tree intentionally has no index.php'
|
|
153
|
+
fi
|
|
154
|
+
grep -Fq 'free of empty lines between its nested or adjacent element tags' "$ROOT/home/configs/opencode/instructions.md" && pass 'OpenCode instructions enforce contiguous raw HTML blocks' || fail 'OpenCode instructions omit the raw HTML blank-line invariant'
|
|
155
|
+
grep -Fq '## PHP To OpenCode API' "$ROOT/home/configs/opencode/instructions.md" && pass 'OpenCode instructions document the PHP API' || fail 'OpenCode instructions omit the PHP API contract'
|
|
156
|
+
grep -Fq '## Extending The Environment' "$ROOT/home/configs/opencode/instructions.md" && pass 'OpenCode instructions document environment extension' || fail 'OpenCode instructions omit environment extension guidance'
|
|
157
|
+
grep -Fq 'deliberately has no Docker socket' "$ROOT/home/configs/opencode/instructions.md" && pass 'OpenCode instructions preserve the host container boundary' || fail 'OpenCode instructions omit the host container boundary'
|
|
158
|
+
[[ -f "$ROOT/home/configs/opencode/instance.example.md" ]] && pass 'OpenCode instance policy template exists' || fail 'OpenCode instance policy template is missing'
|
|
159
|
+
[[ -f "$ROOT/home/configs/opencode/instance.md" ]] && pass 'Installed OpenCode instance policy exists' || fail 'Copy instance.example.md to instance.md before startup'
|
|
160
|
+
grep -Fq 'Commit policy:' "$ROOT/home/configs/opencode/instance.md" && pass 'OpenCode instance policy records commit behavior' || fail 'OpenCode instance policy omits commit behavior'
|
|
161
|
+
grep -Fq 'Communication language:' "$ROOT/home/configs/opencode/instance.md" && pass 'OpenCode instance policy records communication language' || fail 'OpenCode instance policy omits communication language'
|
|
162
|
+
grep -Fq '"instance.md"' "$ROOT/home/configs/opencode/opencode.json" && pass 'OpenCode loads instance policy directly' || fail 'OpenCode does not load instance policy'
|
|
163
|
+
|
|
164
|
+
if compose config --quiet; then
|
|
165
|
+
pass 'Compose configuration is valid'
|
|
166
|
+
else
|
|
167
|
+
fail 'Compose configuration is invalid'
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
services="$(compose config --services 2>/dev/null || true)"
|
|
171
|
+
for service in nginx php-fpm rollup opencode; do
|
|
172
|
+
if grep -qx "$service" <<<"$services"; then
|
|
173
|
+
pass "Compose defines $service"
|
|
174
|
+
else
|
|
175
|
+
fail "Compose does not define $service"
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
running_services="$(compose ps --services 2>/dev/null || true)"
|
|
179
|
+
while IFS= read -r service; do
|
|
180
|
+
[[ -z "$service" ]] && continue
|
|
181
|
+
if ! grep -qx "$service" <<<"$services"; then
|
|
182
|
+
fail "Compose project contains unexpected service $service; choose a unique COMPOSE_PROJECT_NAME"
|
|
183
|
+
fi
|
|
184
|
+
done <<<"$running_services"
|
|
185
|
+
|
|
186
|
+
if ((START)); then
|
|
187
|
+
if compose build nginx php-fpm rollup opencode; then
|
|
188
|
+
pass 'All service images build'
|
|
189
|
+
else
|
|
190
|
+
fail 'One or more service images failed to build'
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# The Rollup watcher intentionally writes generated bundles to the bind
|
|
194
|
+
# mount, so lifecycle mode leaves it untouched and validates its image via
|
|
195
|
+
# the build plus the bundles served by Nginx.
|
|
196
|
+
if compose up -d nginx php-fpm opencode; then
|
|
197
|
+
pass 'Application services started'
|
|
198
|
+
else
|
|
199
|
+
fail 'Application services failed to start'
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
for service in nginx php-fpm opencode; do
|
|
204
|
+
if container_running "$service"; then
|
|
205
|
+
pass "$service is running"
|
|
206
|
+
else
|
|
207
|
+
fail "$service is not running"
|
|
208
|
+
fi
|
|
209
|
+
done
|
|
210
|
+
|
|
211
|
+
if [[ -n "$(container_id rollup)" ]]; then
|
|
212
|
+
container_running rollup && pass 'rollup is running' || fail 'rollup exists but is not running'
|
|
213
|
+
else
|
|
214
|
+
skip 'rollup watcher is not running; generated bundles are tested from the image'
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
NGINX_RESTARTS="$(restart_count nginx 2>/dev/null || printf '')"
|
|
218
|
+
PHP_RESTARTS="$(restart_count php-fpm 2>/dev/null || printf '')"
|
|
219
|
+
OPENCODE_RESTARTS="$(restart_count opencode 2>/dev/null || printf '')"
|
|
220
|
+
|
|
221
|
+
published="$(compose port nginx 80 2>/dev/null || true)"
|
|
222
|
+
if [[ "$published" =~ ^\[(.*)\]:([0-9]+)$ ]]; then
|
|
223
|
+
WEB_HOST="${BASH_REMATCH[1]}"
|
|
224
|
+
WEB_PORT="${BASH_REMATCH[2]}"
|
|
225
|
+
elif [[ "$published" =~ ^(.*):([0-9]+)$ ]]; then
|
|
226
|
+
WEB_HOST="${BASH_REMATCH[1]}"
|
|
227
|
+
WEB_PORT="${BASH_REMATCH[2]}"
|
|
228
|
+
else
|
|
229
|
+
fail 'Could not resolve the published Nginx port'
|
|
230
|
+
printf '\n%d passed, %d failed, %d skipped\n' "$PASSES" "$FAILURES" "$SKIPS"
|
|
231
|
+
exit 1
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
case "$WEB_HOST" in
|
|
235
|
+
0.0.0.0|::) WEB_HOST=127.0.0.1 ;;
|
|
236
|
+
esac
|
|
237
|
+
if [[ "$WEB_HOST" == *:* ]]; then
|
|
238
|
+
BASE_URL="http://[$WEB_HOST]:$WEB_PORT"
|
|
239
|
+
else
|
|
240
|
+
BASE_URL="http://$WEB_HOST:$WEB_PORT"
|
|
241
|
+
fi
|
|
242
|
+
pass "Nginx is published at $BASE_URL"
|
|
243
|
+
|
|
244
|
+
deadline=$((SECONDS + TIMEOUT))
|
|
245
|
+
health_status=000
|
|
246
|
+
while ((SECONDS < deadline)); do
|
|
247
|
+
health_status="$(http_request '/_nestbox/health' 2>/dev/null || printf '000')"
|
|
248
|
+
[[ "$health_status" == "200" ]] && break
|
|
249
|
+
sleep 1
|
|
250
|
+
done
|
|
251
|
+
[[ "$health_status" == "200" ]] && pass 'Nginx and PHP health endpoint responds' || fail 'Health endpoint did not become ready'
|
|
252
|
+
|
|
253
|
+
compose exec -T nginx nginx -t >/dev/null 2>&1 && pass 'Nginx configuration is valid' || fail 'Nginx configuration is invalid'
|
|
254
|
+
compose exec -T nginx apk info -e nginx >/dev/null 2>&1 && pass 'Alpine Nginx package is installed' || fail 'Alpine Nginx package is missing'
|
|
255
|
+
compose exec -T nginx apk info -e nginx-mod-http-nchan >/dev/null 2>&1 && pass 'Alpine Nchan package is installed' || fail 'Alpine Nchan package is missing'
|
|
256
|
+
nginx_config="$(compose exec -T nginx nginx -T 2>&1 || true)"
|
|
257
|
+
if grep -q 'ngx_nchan_module.so' <<<"$nginx_config"; then
|
|
258
|
+
pass 'Nchan dynamic module is loaded'
|
|
259
|
+
else
|
|
260
|
+
fail 'Nchan dynamic module is not loaded'
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
if ((INDEX_PRESENT)); then
|
|
264
|
+
status="$(http_request '/' 2>/dev/null || printf '000')"
|
|
265
|
+
[[ "$status" == "200" ]] && pass 'Main super-document returns HTTP 200' || fail "Main super-document returned HTTP $status"
|
|
266
|
+
content_type="$(header_value 'Content-Type')"
|
|
267
|
+
content_type="$(printf '%s' "$content_type" | tr '[:upper:]' '[:lower:]')"
|
|
268
|
+
[[ "$content_type" == text/html* ]] && pass 'Main super-document is HTML' || fail 'Main super-document has the wrong content type'
|
|
269
|
+
else
|
|
270
|
+
status="$(http_request '/' 2>/dev/null || printf '000')"
|
|
271
|
+
if [[ "$status" == "404" ]] && grep -Fq 'Create and open session' "$TEMP_DIR/body"; then
|
|
272
|
+
pass 'Fresh root renders the OpenCode implementation form with HTTP 404'
|
|
273
|
+
else
|
|
274
|
+
fail "Fresh root did not render the OpenCode implementation form with HTTP 404"
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
for header in Content-Security-Policy X-Content-Type-Options Referrer-Policy Cache-Control; do
|
|
278
|
+
[[ -n "$(header_value "$header")" ]] && pass "$header is present" || fail "$header is missing"
|
|
279
|
+
done
|
|
280
|
+
[[ "$(header_value 'Cache-Control')" == *no-store* ]] && pass 'Nestbox responses disable browser caching' || fail 'Nestbox responses permit browser caching'
|
|
281
|
+
|
|
282
|
+
if ((INDEX_PRESENT)); then
|
|
283
|
+
status="$(http_request '/index.md' 2>/dev/null || printf '000')"
|
|
284
|
+
[[ "$status" == "200" ]] && pass 'Markdown direction returns HTTP 200' || fail "Markdown direction returned HTTP $status"
|
|
285
|
+
content_type="$(header_value 'Content-Type')"
|
|
286
|
+
content_type="$(printf '%s' "$content_type" | tr '[:upper:]' '[:lower:]')"
|
|
287
|
+
[[ "$content_type" == text/markdown* ]] && pass 'Markdown direction has text/markdown content type' || fail 'Markdown direction has the wrong content type'
|
|
288
|
+
else
|
|
289
|
+
status="$(http_request '/index.md' 2>/dev/null || printf '000')"
|
|
290
|
+
[[ "$status" == "404" ]] && pass 'Fresh index Markdown direction remains unresolved' || fail "Fresh index Markdown direction returned HTTP $status"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
for asset in scripts-bundle.js scripts-bundle.css styles.css; do
|
|
294
|
+
status="$(http_request "/$asset" 2>/dev/null || printf '000')"
|
|
295
|
+
if [[ "$status" == "200" && -s "$TEMP_DIR/body" ]]; then
|
|
296
|
+
pass "$asset is served and nonempty"
|
|
297
|
+
else
|
|
298
|
+
fail "$asset is unavailable or empty"
|
|
299
|
+
fi
|
|
300
|
+
[[ "$(header_value 'Cache-Control')" == *no-store* ]] && pass "$asset disables browser caching" || fail "$asset permits browser caching"
|
|
301
|
+
done
|
|
302
|
+
|
|
303
|
+
for path in /index.php /__templates/super-document /__includes/functions.php /__data/example.json /docs/__data/install.json; do
|
|
304
|
+
status="$(http_request "$path" 2>/dev/null || printf '000')"
|
|
305
|
+
[[ "$status" == "404" ]] && pass "$path is protected" || fail "$path returned HTTP $status instead of 404"
|
|
306
|
+
done
|
|
307
|
+
status="$(http_request '/.user.ini' 2>/dev/null || printf '000')"
|
|
308
|
+
[[ "$status" == "403" || "$status" == "404" ]] && pass 'Dotfiles are protected' || fail "Dotfile returned HTTP $status"
|
|
309
|
+
|
|
310
|
+
missing_path="/missing-$RANDOM"
|
|
311
|
+
missing_uri="$missing_path?view=full"
|
|
312
|
+
status="$(http_request "$missing_uri" 2>/dev/null || printf '000')"
|
|
313
|
+
if [[ "$status" == "404" ]] && grep -Fq 'Create and open session' "$TEMP_DIR/body"; then
|
|
314
|
+
pass 'Missing routes render the interactive PHP 404 document'
|
|
315
|
+
else
|
|
316
|
+
fail 'Missing route did not render the interactive PHP 404 document'
|
|
317
|
+
fi
|
|
318
|
+
grep -Fq "name=\"requested_uri\" value=\"$missing_uri\"" "$TEMP_DIR/body" && pass 'Interactive 404 preserves the requested page URI' || fail 'Interactive 404 lost the requested page URI'
|
|
319
|
+
if grep -Fq 'name="prompt" type="text"' "$TEMP_DIR/body" && grep -Fq 'target="nestbox-opencode"' "$TEMP_DIR/body"; then
|
|
320
|
+
pass 'Interactive 404 uses a one-line prompt and the shared OpenCode window'
|
|
321
|
+
else
|
|
322
|
+
fail 'Interactive 404 prompt form has the wrong behavior'
|
|
323
|
+
fi
|
|
324
|
+
if grep -Fq 'nestbox-toolbar' "$TEMP_DIR/body" && grep -Fq 'Open Chat' "$TEMP_DIR/body" && ! grep -Fq 'Continue in OpenCode' "$TEMP_DIR/body"; then
|
|
325
|
+
pass 'Shared header opens the common OpenCode chat'
|
|
326
|
+
else
|
|
327
|
+
fail 'Shared header contains page-specific session controls'
|
|
328
|
+
fi
|
|
329
|
+
status="$(http_request '/404' '' 'POST' 2>/dev/null || printf '000')"
|
|
330
|
+
[[ "$status" == "422" ]] && pass 'Interactive 404 rejects invalid requests' || fail "Interactive 404 accepted an invalid request with HTTP $status"
|
|
331
|
+
|
|
332
|
+
nginx_id="$(container_id nginx)"
|
|
333
|
+
publisher_binding="$(docker inspect --format '{{json (index .NetworkSettings.Ports "8080/tcp")}}' "$nginx_id" 2>/dev/null || true)"
|
|
334
|
+
if [[ -z "$publisher_binding" || "$publisher_binding" == "null" ]]; then
|
|
335
|
+
pass 'Internal publisher port is not published on the host'
|
|
336
|
+
else
|
|
337
|
+
fail 'Internal publisher port is published on the host'
|
|
338
|
+
fi
|
|
339
|
+
status="$(http_request '/_nestbox/publish?topics=nestbox-test:public' '' 'POST' 2>/dev/null || printf '000')"
|
|
340
|
+
[[ "$status" == "404" ]] && pass 'Publisher endpoint is unavailable on the public port' || fail "Public publisher endpoint returned HTTP $status"
|
|
341
|
+
|
|
342
|
+
if ((INDEX_PRESENT)); then
|
|
343
|
+
if compose exec -T php-fpm php -l /home/code/index.php >/dev/null 2>&1; then
|
|
344
|
+
pass 'Installed super-document passes PHP lint'
|
|
345
|
+
else
|
|
346
|
+
fail 'Installed super-document failed PHP lint'
|
|
347
|
+
fi
|
|
348
|
+
else
|
|
349
|
+
pass 'Fresh page tree has no index.php to lint'
|
|
350
|
+
fi
|
|
351
|
+
if compose exec -T php-fpm php -l /home/code/404.php >/dev/null 2>&1; then
|
|
352
|
+
pass 'Interactive 404 super-document passes PHP lint'
|
|
353
|
+
else
|
|
354
|
+
fail 'Interactive 404 super-document failed PHP lint'
|
|
355
|
+
fi
|
|
356
|
+
context_code='require "/home/code/__includes/functions.php"; $context = nestbox_page_context("/docs/install?tab=all"); exit($context["statePath"] === "/home/code/docs/__data/install.json" && $context["agentDirectory"] === "/nestbox/home/code/docs" ? 0 : 1);'
|
|
357
|
+
if compose exec -T php-fpm php -r "$context_code" >/dev/null 2>&1; then
|
|
358
|
+
pass 'Nested pages map to directory-local data and agent scope'
|
|
359
|
+
else
|
|
360
|
+
fail 'Nested page data or agent context is incorrect'
|
|
361
|
+
fi
|
|
362
|
+
if compose exec -T php-fpm php /sources/php/cli.php __templates/super-document >/dev/null 2>&1; then
|
|
363
|
+
fail 'Protected super-document executed through the CLI router'
|
|
364
|
+
else
|
|
365
|
+
pass 'CLI router rejects protected super-documents'
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
opencode_health_code='require "/home/code/__includes/functions.php"; $health = nestbox_opencode_request("GET", "/global/health", [], "/workspace"); exit(($health["healthy"] ?? false) ? 0 : 1);'
|
|
369
|
+
if compose exec -T php-fpm php -r "$opencode_health_code" >/dev/null 2>&1; then
|
|
370
|
+
pass 'PHP reaches the authenticated internal OpenCode API'
|
|
371
|
+
else
|
|
372
|
+
fail 'PHP cannot reach the authenticated internal OpenCode API'
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
status="$(http_request '/Agent' "localhost:$WEB_PORT" 2>/dev/null || printf '000')"
|
|
376
|
+
location="$(header_value 'Location')"
|
|
377
|
+
if [[ "$status" == "302" && "$location" == "http://agent.localhost:$WEB_PORT/" ]]; then
|
|
378
|
+
pass '/Agent redirects to the same-port OpenCode origin'
|
|
379
|
+
else
|
|
380
|
+
fail "/Agent redirect was HTTP $status with Location $location"
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
status="$(http_request '/doc' "agent.localhost:$WEB_PORT" 2>/dev/null || printf '000')"
|
|
384
|
+
if [[ "$status" == "401" || "$status" =~ ^[23][0-9][0-9]$ ]]; then
|
|
385
|
+
pass 'OpenCode host routing responds'
|
|
386
|
+
else
|
|
387
|
+
fail "OpenCode host routing returned HTTP $status"
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
channel="nestbox-test:$(date +%s)-$$"
|
|
391
|
+
token="event-$(date +%s)-$$-$RANDOM"
|
|
392
|
+
sse_file="$TEMP_DIR/events"
|
|
393
|
+
curl --silent --show-error --noproxy '*' --no-buffer --max-time "$TIMEOUT" \
|
|
394
|
+
--header 'Accept: text/event-stream' \
|
|
395
|
+
"$BASE_URL/_nestbox/events?topics=nestbox-test:*" >"$sse_file" 2>"$TEMP_DIR/events-error" &
|
|
396
|
+
SSE_PID=$!
|
|
397
|
+
deadline=$((SECONDS + TIMEOUT))
|
|
398
|
+
while ((SECONDS < deadline)); do
|
|
399
|
+
grep -Fq ': hi' "$sse_file" 2>/dev/null && break
|
|
400
|
+
sleep 1
|
|
401
|
+
done
|
|
402
|
+
if grep -Fq ': hi' "$sse_file" 2>/dev/null; then
|
|
403
|
+
pass 'EventSource subscriber is connected'
|
|
404
|
+
else
|
|
405
|
+
fail 'EventSource subscriber did not connect'
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
php_code='require "/home/code/__includes/functions.php"; exit(nestbox_bus_publish(getenv("TEST_TOPIC"), "changed", ["token" => getenv("TEST_TOKEN")]) ? 0 : 1);'
|
|
409
|
+
if compose exec -T -e "TEST_TOPIC=$channel" -e "TEST_TOKEN=$token" php-fpm php -r "$php_code" >/dev/null 2>&1; then
|
|
410
|
+
pass 'PHP publishes through the internal Nchan endpoint'
|
|
411
|
+
else
|
|
412
|
+
fail 'PHP failed to publish through the internal Nchan endpoint'
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
deadline=$((SECONDS + TIMEOUT))
|
|
416
|
+
while ((SECONDS < deadline)); do
|
|
417
|
+
grep -Fq "$token" "$sse_file" 2>/dev/null && break
|
|
418
|
+
sleep 1
|
|
419
|
+
done
|
|
420
|
+
if grep -Fq 'event: changed' "$sse_file" && grep -Fq "$token" "$sse_file" && grep -Fq "$channel" "$sse_file"; then
|
|
421
|
+
pass 'EventSource receives the wildcard Nchan event with its payload'
|
|
422
|
+
else
|
|
423
|
+
fail 'EventSource did not receive the expected Nchan event'
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
for entry in "nginx:$NGINX_RESTARTS" "php-fpm:$PHP_RESTARTS" "opencode:$OPENCODE_RESTARTS"; do
|
|
427
|
+
service="${entry%%:*}"
|
|
428
|
+
before="${entry#*:}"
|
|
429
|
+
after="$(restart_count "$service" 2>/dev/null || printf '')"
|
|
430
|
+
if [[ -n "$before" && "$before" == "$after" ]] && container_running "$service"; then
|
|
431
|
+
pass "$service remained stable during the test"
|
|
432
|
+
else
|
|
433
|
+
fail "$service restarted or stopped during the test"
|
|
434
|
+
fi
|
|
435
|
+
done
|
|
436
|
+
|
|
437
|
+
printf '\n%d passed, %d failed, %d skipped\n' "$PASSES" "$FAILURES" "$SKIPS"
|
|
438
|
+
((FAILURES == 0))
|