@nanobpm/nano-workforce 0.151.1 → 0.153.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +23 -0
- package/CHANGELOG.md +12 -0
- package/README.md +56 -0
- package/install.sh +1221 -0
- package/package.json +1 -1
- package/test/install-smoke.sh +337 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.153.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Hermetic smoke test for install.sh — asserts the command sequence it EMITS
|
|
3
|
+
# under --dry-run for a few selections, plus the guard-rail exit codes. No nano
|
|
4
|
+
# CLI or coding harness/adapter needs to be installed: --dry-run means the
|
|
5
|
+
# mutating commands are printed (never run), harness detection is stubbed via
|
|
6
|
+
# NANO_INSTALL_HARNESSES_OVERRIDE, and adapter presence via
|
|
7
|
+
# NANO_INSTALL_ADAPTERS_PRESENT. Only `node` and `npm` are required (the
|
|
8
|
+
# preflight probe still runs), so run it where both are available.
|
|
9
|
+
#
|
|
10
|
+
# The phase-2 (app install, nano-workforce#583) scenarios also drive the real
|
|
11
|
+
# curl-backed console flow against an in-process node stub console via the
|
|
12
|
+
# NANO_INSTALL_TEST_PHASE2_ONLY hook — those need `node` + `curl`, and are
|
|
13
|
+
# skipped (still PASS) if either is missing.
|
|
14
|
+
#
|
|
15
|
+
# Run from the repo root: sh test/install-smoke.sh
|
|
16
|
+
set -eu
|
|
17
|
+
|
|
18
|
+
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
|
19
|
+
SCRIPT="$ROOT/install.sh"
|
|
20
|
+
FAILED=0
|
|
21
|
+
|
|
22
|
+
pass() { printf 'PASS: %s\n' "$1"; }
|
|
23
|
+
fail() { printf 'FAIL: %s\n' "$1" >&2; FAILED=1; }
|
|
24
|
+
|
|
25
|
+
# assert_contains "<label>" "<needle>" "<haystack>"
|
|
26
|
+
assert_contains() {
|
|
27
|
+
case "$3" in
|
|
28
|
+
*"$2"*) pass "$1" ;;
|
|
29
|
+
*) fail "$1 — expected to find: $2"; printf '%s\n' "$3" | sed 's/^/ | /' >&2 ;;
|
|
30
|
+
esac
|
|
31
|
+
}
|
|
32
|
+
assert_not_contains() {
|
|
33
|
+
case "$3" in
|
|
34
|
+
*"$2"*) fail "$1 — did NOT expect: $2" ;;
|
|
35
|
+
*) pass "$1" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# --- Scenario 1: copilot + qwen, models + instances, dry-run ----------------
|
|
40
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="copilot qwen" \
|
|
41
|
+
sh "$SCRIPT" --harness copilot:gpt-5.4:5 --harness qwen:qwen3-coder-plus:2 --yes --dry-run 2>&1)
|
|
42
|
+
assert_contains "s1: copilot model baked into --command" \
|
|
43
|
+
"--command 'copilot --acp --model gpt-5.4'" "$OUT"
|
|
44
|
+
assert_contains "s1: copilot --model bookkeeping" "--model 'gpt-5.4'" "$OUT"
|
|
45
|
+
assert_contains "s1: copilot hired senior/acp/yolo" \
|
|
46
|
+
"--rank senior --command 'copilot --acp --model gpt-5.4' --model 'gpt-5.4' --capabilities '' --protocol acp --permission yolo" "$OUT"
|
|
47
|
+
assert_contains "s1: qwen model baked (mandatory)" \
|
|
48
|
+
"--command 'qwen --experimental-acp --model qwen3-coder-plus'" "$OUT"
|
|
49
|
+
assert_contains "s1: workforce add copilot x5 auto" \
|
|
50
|
+
"nano workforce add copilot --instances 5 --auto" "$OUT"
|
|
51
|
+
assert_contains "s1: workforce add qwen x2 auto" \
|
|
52
|
+
"nano workforce add qwen --instances 2 --auto" "$OUT"
|
|
53
|
+
assert_contains "s1: engine start then workforce start" "nano start" "$OUT"
|
|
54
|
+
assert_contains "s1: workforce start" "nano workforce start" "$OUT"
|
|
55
|
+
assert_contains "s1: yolo warning shown" "UNATTENDED with --permission yolo" "$OUT"
|
|
56
|
+
|
|
57
|
+
# --- Scenario 2: qwen with NO model must fail (mandatory-model rule) ---------
|
|
58
|
+
if NANO_INSTALL_HARNESSES_OVERRIDE="qwen" \
|
|
59
|
+
sh "$SCRIPT" --harness qwen --yes --dry-run >/dev/null 2>&1; then
|
|
60
|
+
fail "s2: qwen without a model should exit non-zero"
|
|
61
|
+
else
|
|
62
|
+
pass "s2: qwen without a model exits non-zero"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# --- Scenario 3: missing ACP adapter, non-interactive decline -> skip + rc1 --
|
|
66
|
+
set +e
|
|
67
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="claude copilot" NANO_INSTALL_ADAPTERS_PRESENT="" \
|
|
68
|
+
sh "$SCRIPT" --harness claude:opus:1 --harness copilot:gpt-5.4:2 --yes --dry-run 2>&1)
|
|
69
|
+
RC=$?
|
|
70
|
+
set -e
|
|
71
|
+
assert_contains "s3: claude skipped (missing adapter)" \
|
|
72
|
+
"skipping claude — ACP adapter 'claude-code-acp' not installed" "$OUT"
|
|
73
|
+
assert_not_contains "s3: claude NOT hired" "nano hire --name claude" "$OUT"
|
|
74
|
+
assert_contains "s3: copilot still hired" "nano hire --name copilot" "$OUT"
|
|
75
|
+
if [ "$RC" -ne 0 ]; then pass "s3: partial success exits non-zero"; else fail "s3: expected non-zero exit after a skip"; fi
|
|
76
|
+
|
|
77
|
+
# --- Scenario 4: no TTY + no --harness must not hang; exits non-zero --------
|
|
78
|
+
# install.sh reads prompts from /dev/tty, and `</dev/null` does not detach the
|
|
79
|
+
# controlling terminal — so from an interactive shell this scenario would open
|
|
80
|
+
# the real tty and hang. Only exercise it when there is no controlling TTY (CI),
|
|
81
|
+
# and skip it (still PASS) when run interactively.
|
|
82
|
+
if ( exec </dev/tty ) >/dev/null 2>&1; then
|
|
83
|
+
pass "s4: skipped (controlling TTY present; would hang interactively)"
|
|
84
|
+
else
|
|
85
|
+
set +e
|
|
86
|
+
sh "$SCRIPT" --dry-run </dev/null >/dev/null 2>&1
|
|
87
|
+
RC=$?
|
|
88
|
+
set -e
|
|
89
|
+
if [ "$RC" -ne 0 ]; then pass "s4: no TTY and no --harness exits non-zero"; else fail "s4: expected non-zero exit with no TTY and no flags"; fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# --- Scenario 5: default instances (5 then 1) and default model ------------
|
|
93
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="copilot claude" NANO_INSTALL_ADAPTERS_PRESENT="claude-code-acp" \
|
|
94
|
+
sh "$SCRIPT" --harness copilot --harness claude --yes --dry-run 2>&1)
|
|
95
|
+
assert_contains "s5: first selection defaults to 5 instances" \
|
|
96
|
+
"nano workforce add copilot --instances 5 --auto" "$OUT"
|
|
97
|
+
assert_contains "s5: later selection defaults to 1 instance" \
|
|
98
|
+
"nano workforce add claude --instances 1 --auto" "$OUT"
|
|
99
|
+
assert_contains "s5: default model => bare adapter command, empty --model" \
|
|
100
|
+
"--command 'claude-code-acp' --model ''" "$OUT"
|
|
101
|
+
|
|
102
|
+
# ===========================================================================
|
|
103
|
+
# Phase 2 — install & run the Nano Workforce app (nano-workforce#583)
|
|
104
|
+
# ===========================================================================
|
|
105
|
+
|
|
106
|
+
# --- Scenario 6: phase-2 dry-run emits the full console call sequence -------
|
|
107
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="copilot" \
|
|
108
|
+
sh "$SCRIPT" --harness copilot:gpt-5.4:1 --yes --dry-run 2>&1)
|
|
109
|
+
assert_contains "s6: urban toolkit install" \
|
|
110
|
+
"POST http://localhost:8080/console/api/urban/install" "$OUT"
|
|
111
|
+
assert_contains "s6: extension install with pkg" \
|
|
112
|
+
'POST http://localhost:8080/console/api/extensions/install {"pkg":"@nanobpm/nano-workforce"}' "$OUT"
|
|
113
|
+
assert_contains "s6: GET projects to confirm the template" \
|
|
114
|
+
"GET http://localhost:8080/console/api/projects" "$OUT"
|
|
115
|
+
assert_contains "s6: create project from template nano-workforce" \
|
|
116
|
+
'{"name":"Workforce","template":"nano-workforce"}' "$OUT"
|
|
117
|
+
assert_contains "s6: 409-exists convergence branch documented" \
|
|
118
|
+
"on 409 (project exists) we skip scaffolding" "$OUT"
|
|
119
|
+
assert_contains "s6: NANO_WORKFORCE_BASE_URL is console origin + app-view prefix" \
|
|
120
|
+
'"NANO_WORKFORCE_BASE_URL":"http://localhost:8080/console/app-view/Workforce"' "$OUT"
|
|
121
|
+
assert_contains "s6: NANOBPMN_BASE_URL written" \
|
|
122
|
+
'"NANOBPMN_BASE_URL":"http://localhost:8080"' "$OUT"
|
|
123
|
+
assert_contains "s6: PR_REVIEW_PORT written" '"PR_REVIEW_PORT":"3000"' "$OUT"
|
|
124
|
+
assert_contains "s6: run the project" \
|
|
125
|
+
"POST http://localhost:8080/console/api/projects/Workforce/run" "$OUT"
|
|
126
|
+
assert_contains "s6: readiness poll of the app's own /app/api/version" \
|
|
127
|
+
"poll GET http://localhost:8080/console/app-view/Workforce/app/api/version" "$OUT"
|
|
128
|
+
assert_contains "s6: surface app-view URL" \
|
|
129
|
+
"http://localhost:8080/console/app-view/Workforce/" "$OUT"
|
|
130
|
+
assert_contains "s6: surface Tasks inbox" \
|
|
131
|
+
"http://localhost:8080/console/app-view/Workforce/tasks" "$OUT"
|
|
132
|
+
assert_contains "s6: surface Delivery Graphs" \
|
|
133
|
+
"http://localhost:8080/console/app-view/Workforce/delivery-graphs" "$OUT"
|
|
134
|
+
assert_contains "s6: surface agent guide" \
|
|
135
|
+
"http://localhost:8080/console/app-view/Workforce/app/api/agent" "$OUT"
|
|
136
|
+
assert_contains "s6: surface MCP endpoint" \
|
|
137
|
+
"http://localhost:8080/console/app-view/Workforce/app/mcp" "$OUT"
|
|
138
|
+
# Dry-run must not assert readiness: it changed nothing, so it must not claim the
|
|
139
|
+
# app "is up" / "is running" — it states the surfaces are what *would* be available.
|
|
140
|
+
assert_not_contains "s6: dry-run does not falsely claim the app is up" \
|
|
141
|
+
"Nano Workforce is up" "$OUT"
|
|
142
|
+
assert_not_contains "s6: dry-run does not falsely claim the app is running" \
|
|
143
|
+
"the Nano Workforce app is running" "$OUT"
|
|
144
|
+
assert_contains "s6: dry-run frames surfaces as would-be-available" \
|
|
145
|
+
"would be available" "$OUT"
|
|
146
|
+
assert_contains "s6: dry-run final report states no changes were made" \
|
|
147
|
+
"Dry-run complete — no changes were made" "$OUT"
|
|
148
|
+
|
|
149
|
+
# --- Scenario 6b: --project-name flows through every URL and body ----------
|
|
150
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="copilot" \
|
|
151
|
+
sh "$SCRIPT" --harness copilot:gpt-5.4:1 --yes --dry-run --project-name Fleet 2>&1)
|
|
152
|
+
assert_contains "s6b: project name in the create body" \
|
|
153
|
+
'{"name":"Fleet","template":"nano-workforce"}' "$OUT"
|
|
154
|
+
assert_contains "s6b: project name in the app-view base" \
|
|
155
|
+
'"NANO_WORKFORCE_BASE_URL":"http://localhost:8080/console/app-view/Fleet"' "$OUT"
|
|
156
|
+
assert_contains "s6b: project name in the run URL" \
|
|
157
|
+
"POST http://localhost:8080/console/api/projects/Fleet/run" "$OUT"
|
|
158
|
+
|
|
159
|
+
# --- Scenario 6c: GITHUB_TOKEN is redacted in dry-run, never printed --------
|
|
160
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="copilot" GITHUB_TOKEN="ghp_SHOULD_NOT_APPEAR" \
|
|
161
|
+
sh "$SCRIPT" --harness copilot:gpt-5.4:1 --yes --dry-run 2>&1)
|
|
162
|
+
assert_contains "s6c: token key present, masked" '"GITHUB_TOKEN":"***"' "$OUT"
|
|
163
|
+
assert_not_contains "s6c: token value never printed" "ghp_SHOULD_NOT_APPEAR" "$OUT"
|
|
164
|
+
|
|
165
|
+
# --- Scenario 7: --skip-app runs phase 1 only, emits no console calls -------
|
|
166
|
+
OUT=$(NANO_INSTALL_HARNESSES_OVERRIDE="copilot" \
|
|
167
|
+
sh "$SCRIPT" --harness copilot:gpt-5.4:1 --yes --dry-run --skip-app 2>&1)
|
|
168
|
+
assert_not_contains "s7: no console API calls under --skip-app" "/console/api/" "$OUT"
|
|
169
|
+
assert_contains "s7: skip note shown" "Phase 2 skipped (--skip-app)" "$OUT"
|
|
170
|
+
assert_contains "s7: dry-run + skip-app report states no changes were made" \
|
|
171
|
+
"Dry-run complete — no changes were made" "$OUT"
|
|
172
|
+
|
|
173
|
+
# --- Scenarios 8-15: live phase-2 flow against a stubbed node console -------
|
|
174
|
+
if command -v node >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
|
|
175
|
+
STUBDIR=$(mktemp -d "${TMPDIR:-/tmp}/nwf-smoke.XXXXXX")
|
|
176
|
+
STUB="$STUBDIR/stub-console.cjs"
|
|
177
|
+
# The repo's package.json sets "type":"module", so the CommonJS stub must be
|
|
178
|
+
# a .cjs file. It answers just enough of the console API to exercise the flow.
|
|
179
|
+
cat > "$STUB" <<'CJS'
|
|
180
|
+
const http = require('http');
|
|
181
|
+
const mode = process.env.STUB_MODE || 'happy'; // happy | exists | timeout
|
|
182
|
+
let ran = false;
|
|
183
|
+
const srv = http.createServer((req, res) => {
|
|
184
|
+
const url = req.url, m = req.method;
|
|
185
|
+
const json = (c, o) => { res.writeHead(c, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
|
|
186
|
+
req.on('data', () => {}); req.on('end', () => {
|
|
187
|
+
if (m === 'GET' && url === '/console/api/projects') {
|
|
188
|
+
const projects = mode === 'exists' ? [{ name: 'Workforce', template: 'nano-workforce' }] : [];
|
|
189
|
+
return json(200, { templates: [{ id: 'nano-workforce', pack: 'nano-workforce' }], projects });
|
|
190
|
+
}
|
|
191
|
+
if (m === 'POST' && url === '/console/api/urban/install') return json(200, { urbanAvailable: false });
|
|
192
|
+
if (m === 'POST' && url === '/console/api/extensions/install') return json(201, { id: 'nano-workforce' });
|
|
193
|
+
if (m === 'POST' && url === '/console/api/projects') return mode === 'exists' ? json(409, { error: 'exists' }) : json(201, { name: 'Workforce' });
|
|
194
|
+
if (m === 'PUT' && url === '/console/api/projects/Workforce/config') return json(200, { ok: true });
|
|
195
|
+
if (m === 'POST' && url === '/console/api/projects/Workforce/run') { ran = true; return json(200, { state: 'running' }); }
|
|
196
|
+
if (m === 'GET' && url === '/console/app-view/Workforce/app/api/version') {
|
|
197
|
+
if (mode === 'timeout') return json(404, {});
|
|
198
|
+
return ran ? json(200, { version: '9.9.9' }) : json(404, {});
|
|
199
|
+
}
|
|
200
|
+
return json(404, { error: 'nomatch', url });
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
srv.listen(0, '127.0.0.1', () => console.log('PORT ' + srv.address().port));
|
|
204
|
+
setTimeout(() => process.exit(0), 30000); // self-terminate: never leak the stub
|
|
205
|
+
CJS
|
|
206
|
+
|
|
207
|
+
# run_phase2 <mode> [extra install.sh args...] -> sets PHASE2_RC, PHASE2_OUT
|
|
208
|
+
run_phase2() {
|
|
209
|
+
_mode=$1; shift
|
|
210
|
+
STUB_MODE="$_mode" node "$STUB" > "$STUBDIR/$_mode.log" 2>&1 &
|
|
211
|
+
_pid=$!
|
|
212
|
+
_port=''; _i=0
|
|
213
|
+
while [ "$_i" -lt 50 ]; do
|
|
214
|
+
_port=$(sed -n 's/^PORT //p' "$STUBDIR/$_mode.log" 2>/dev/null)
|
|
215
|
+
[ -n "$_port" ] && break
|
|
216
|
+
_i=$((_i + 1)); sleep 1
|
|
217
|
+
done
|
|
218
|
+
if [ -z "$_port" ]; then
|
|
219
|
+
kill "$_pid" 2>/dev/null || true
|
|
220
|
+
wait "$_pid" 2>/dev/null || true
|
|
221
|
+
PHASE2_OUT="stub console never reported a port for mode '$_mode'"; PHASE2_RC=127
|
|
222
|
+
return 0
|
|
223
|
+
fi
|
|
224
|
+
set +e
|
|
225
|
+
PHASE2_OUT=$(NANO_INSTALL_TEST_PHASE2_ONLY=1 \
|
|
226
|
+
NANO_INSTALL_CONSOLE_ORIGIN="http://127.0.0.1:$_port" \
|
|
227
|
+
NANO_INSTALL_APP_POLL_ATTEMPTS=3 NANO_INSTALL_APP_POLL_INTERVAL=0 \
|
|
228
|
+
sh "$SCRIPT" --yes "$@" 2>&1)
|
|
229
|
+
PHASE2_RC=$?
|
|
230
|
+
set -e
|
|
231
|
+
kill "$_pid" 2>/dev/null || true
|
|
232
|
+
wait "$_pid" 2>/dev/null || true
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if [ -z "$STUBDIR" ] || [ ! -f "$STUB" ]; then
|
|
236
|
+
fail "s8-s15: could not create the stub console"
|
|
237
|
+
else
|
|
238
|
+
# Scenario 8 — happy path: success asserted via /app/api/version, exit 0.
|
|
239
|
+
run_phase2 happy
|
|
240
|
+
if [ "$PHASE2_RC" -eq 0 ]; then pass "s8: happy path exits zero"; else
|
|
241
|
+
fail "s8: happy path should exit zero"; printf '%s\n' "$PHASE2_OUT" | sed 's/^/ | /' >&2; fi
|
|
242
|
+
assert_contains "s8: readiness asserted via /app/api/version" "app is up" "$PHASE2_OUT"
|
|
243
|
+
assert_contains "s8: project created (not 409)" "project 'Workforce' created" "$PHASE2_OUT"
|
|
244
|
+
|
|
245
|
+
# Scenario 9 — existing project (409): configure + run, never re-scaffold.
|
|
246
|
+
run_phase2 exists
|
|
247
|
+
if [ "$PHASE2_RC" -eq 0 ]; then pass "s9: 409-exists converges (exit zero)"; else
|
|
248
|
+
fail "s9: 409-exists path should exit zero"; printf '%s\n' "$PHASE2_OUT" | sed 's/^/ | /' >&2; fi
|
|
249
|
+
assert_contains "s9: existing project configured+run, not re-scaffolded" \
|
|
250
|
+
"already exists — configuring and running it (not re-scaffolding)" "$PHASE2_OUT"
|
|
251
|
+
|
|
252
|
+
# Scenario 10 — readiness poll timeout: clear message, exit non-zero.
|
|
253
|
+
run_phase2 timeout
|
|
254
|
+
if [ "$PHASE2_RC" -ne 0 ]; then pass "s10: readiness timeout exits non-zero"; else
|
|
255
|
+
fail "s10: readiness timeout should exit non-zero"; fi
|
|
256
|
+
assert_contains "s10: timeout is a clear readiness message" \
|
|
257
|
+
"did not answer 200 on /app/api/version" "$PHASE2_OUT"
|
|
258
|
+
assert_contains "s10: timeout names the self-heal-on-Run dependency" \
|
|
259
|
+
"self-heal-on-Run" "$PHASE2_OUT"
|
|
260
|
+
|
|
261
|
+
# Scenario 11 — console off/unreachable: early failure that mutates nothing.
|
|
262
|
+
set +e
|
|
263
|
+
OFF=$(NANO_INSTALL_TEST_PHASE2_ONLY=1 NANO_INSTALL_CONSOLE_ORIGIN="http://127.0.0.1:1" \
|
|
264
|
+
sh "$SCRIPT" --yes 2>&1)
|
|
265
|
+
OFFRC=$?
|
|
266
|
+
set -e
|
|
267
|
+
if [ "$OFFRC" -ne 0 ]; then pass "s11: console-off exits non-zero"; else
|
|
268
|
+
fail "s11: console-off should exit non-zero"; fi
|
|
269
|
+
assert_contains "s11: console-off is a clear message" "console unreachable" "$OFF"
|
|
270
|
+
assert_not_contains "s11: console-off mutates nothing (no extension install)" \
|
|
271
|
+
"extensions/install" "$OFF"
|
|
272
|
+
|
|
273
|
+
# Scenario 12 — schemeless console origin: fail early with a clear message,
|
|
274
|
+
# never emit the "localhost:8080://localhost:8080"-style invalid origin.
|
|
275
|
+
set +e
|
|
276
|
+
SCHEMELESS=$(NANO_INSTALL_TEST_PHASE2_ONLY=1 NANO_INSTALL_CONSOLE_ORIGIN="localhost:8080" \
|
|
277
|
+
sh "$SCRIPT" --yes 2>&1)
|
|
278
|
+
SCHEMELESSRC=$?
|
|
279
|
+
set -e
|
|
280
|
+
if [ "$SCHEMELESSRC" -ne 0 ]; then pass "s12: schemeless origin exits non-zero"; else
|
|
281
|
+
fail "s12: schemeless origin should exit non-zero"; fi
|
|
282
|
+
assert_contains "s12: schemeless origin is a clear message" \
|
|
283
|
+
"expected a URL with a scheme" "$SCHEMELESS"
|
|
284
|
+
assert_not_contains "s12: schemeless origin never doubles the authority" \
|
|
285
|
+
"localhost:8080://localhost:8080" "$SCHEMELESS"
|
|
286
|
+
|
|
287
|
+
# Scenario 13 — mktemp unavailable: api() must fall back to a safe noclobber
|
|
288
|
+
# temp file and the happy path must still converge. A PATH-shim `mktemp` that
|
|
289
|
+
# always fails forces mktemp_safe() down its fallback branch for the script.
|
|
290
|
+
SHIMDIR="$STUBDIR/shim"
|
|
291
|
+
mkdir -p "$SHIMDIR"
|
|
292
|
+
printf '#!/bin/sh\nexit 1\n' > "$SHIMDIR/mktemp"
|
|
293
|
+
chmod +x "$SHIMDIR/mktemp"
|
|
294
|
+
_oldpath="$PATH"
|
|
295
|
+
PATH="$SHIMDIR:$PATH"; export PATH
|
|
296
|
+
run_phase2 happy
|
|
297
|
+
PATH="$_oldpath"; export PATH
|
|
298
|
+
if [ "$PHASE2_RC" -eq 0 ]; then pass "s13: mktemp-unavailable fallback converges (exit zero)"; else
|
|
299
|
+
fail "s13: mktemp-unavailable fallback should exit zero"; printf '%s\n' "$PHASE2_OUT" | sed 's/^/ | /' >&2; fi
|
|
300
|
+
assert_contains "s13: fallback still asserts readiness via /app/api/version" "app is up" "$PHASE2_OUT"
|
|
301
|
+
|
|
302
|
+
# Scenario 14 — a GITHUB_TOKEN carrying a control character (e.g. a stray
|
|
303
|
+
# newline) must be rejected with a clear error before any JSON config body
|
|
304
|
+
# is emitted, never producing invalid JSON that fails the console API
|
|
305
|
+
# opaquely. The token flows through json_str() unredacted on the live PUT.
|
|
306
|
+
_ctrl_token="$(printf 'ghp_bad\ntoken')"
|
|
307
|
+
export GITHUB_TOKEN="$_ctrl_token"
|
|
308
|
+
run_phase2 happy
|
|
309
|
+
unset GITHUB_TOKEN
|
|
310
|
+
if [ "$PHASE2_RC" -ne 0 ]; then pass "s14: control-char token rejected (exit non-zero)"; else
|
|
311
|
+
fail "s14: control-char token should exit non-zero"; printf '%s\n' "$PHASE2_OUT" | sed 's/^/ | /' >&2; fi
|
|
312
|
+
assert_contains "s14: control-char token has a clear message" \
|
|
313
|
+
"control characters" "$PHASE2_OUT"
|
|
314
|
+
|
|
315
|
+
# Scenario 15 — the control-char gate covers EVERY interpolated config value,
|
|
316
|
+
# not just the token: a non-token field (here PR_REVIEW_PORT) carrying a
|
|
317
|
+
# control character must be rejected the same way, naming the offending key.
|
|
318
|
+
_ctrl_port="$(printf '3000\nbad')"
|
|
319
|
+
export PR_REVIEW_PORT="$_ctrl_port"
|
|
320
|
+
run_phase2 happy
|
|
321
|
+
unset PR_REVIEW_PORT
|
|
322
|
+
if [ "$PHASE2_RC" -ne 0 ]; then pass "s15: control-char non-token field rejected (exit non-zero)"; else
|
|
323
|
+
fail "s15: control-char non-token field should exit non-zero"; printf '%s\n' "$PHASE2_OUT" | sed 's/^/ | /' >&2; fi
|
|
324
|
+
assert_contains "s15: rejection names the offending field" \
|
|
325
|
+
"PR_REVIEW_PORT contains control characters" "$PHASE2_OUT"
|
|
326
|
+
|
|
327
|
+
rm -rf "$STUBDIR"
|
|
328
|
+
fi
|
|
329
|
+
else
|
|
330
|
+
pass "s8-s15: skipped (node or curl unavailable for the stubbed-console tests)"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [ "$FAILED" -ne 0 ]; then
|
|
334
|
+
printf '\nSMOKE TEST FAILED\n' >&2
|
|
335
|
+
exit 1
|
|
336
|
+
fi
|
|
337
|
+
printf '\nAll install.sh smoke assertions passed.\n'
|