Files
stack/scripts/test-config.sh
T
jason.woltje 48359dbfaf webui: Q175 phase 1, the Console verifies sign-in itself under --sso (row 192, #1690)
The Console is an OIDC relying party on oauth4webapi 3.8.8 (pinned, root lock).
console.sso in config.json, client secrets from slot files, session cookie,
public Host only on the human listener, tester listener refuses every sign-in
header. Rulings sage-1340 F1/F2/F4 and sage-1347 F7/F8. Tests use a fake OP on
loopback only. Packet: agents/launchpad/work/queue-192/BUILD.md.
2026-10-20 14:35:52 -05:00

213 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Fast, sandboxed selftests for the configuration layer.
#
# No Docker, no network, no credentials: every case runs against a
# temporary config via MOSAIC_CONFIG. Suitable for frequent local runs.
set -uo pipefail
cd "$(dirname "$0")/.."
SANDBOX="$(mktemp -d)"
trap 'rm -rf "$SANDBOX"' EXIT
PASS=0
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
else
C_OK=""; C_FAIL=""; C_RESET=""
fi
FAIL=0
check() {
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
}
# expect_exit NAME EXPECTED_RC -- command...
expect_exit() {
local name="$1" expected="$2"
shift 3 # name, expected, "--"
local rc
"$@" >/dev/null 2>&1
rc=$?
if [ "$rc" -eq "$expected" ]; then
PASS=$((PASS + 1))
echo "${C_OK}OK${C_RESET} $name (exit $rc)"
else
FAIL=$((FAIL + 1))
echo "${C_FAIL}FAIL${C_RESET} $name (exit $rc, expected $expected)"
fi
}
CONFIG_OP="node scripts/mosaic-config.mjs"
valid_body() {
cat <<EOF
{"configVersion":1,"environment":"development","dataRoot":"$1","execution":{"backend":"docker","provider":"zai","model":"glm-5.3-flash"}}
EOF
}
cfg() { printf '%s' "$2" > "$SANDBOX/$1"; }
DATA_ROOT="$SANDBOX/data"
# --- adapter selection (M4) ---
cfg default-adapter.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
MOSAIC_CONFIG="$SANDBOX/default-adapter.json" $CONFIG_OP validate | grep -q '"adapter": "pi"'
check "absent adapter defaults to pi" $?
cfg mock-adapter.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m","adapter":"mock"}}'
expect_exit "adapter mock validates" 0 -- env MOSAIC_CONFIG="$SANDBOX/mock-adapter.json" $CONFIG_OP validate
cfg bad-adapter.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m","adapter":"claude"}}'
expect_exit "unsupported adapter exits 2" 2 -- env MOSAIC_CONFIG="$SANDBOX/bad-adapter.json" $CONFIG_OP validate
MOSAIC_CONFIG="$SANDBOX/mock-adapter.json" $CONFIG_OP env | grep -q "MOSAIC_ADAPTER='mock'"
check "env exports adapter" $?
# --- bootstrap ---
rm -f "$SANDBOX/config.json"
expect_exit "bootstrap creates default when absent" 0 -- \
env MOSAIC_CONFIG="$SANDBOX/config.json" $CONFIG_OP bootstrap
[ -f "$SANDBOX/config.json" ] && { PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} bootstrap wrote config file"; } \
|| { FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} bootstrap wrote config file"; }
SUM_BEFORE=$(sha256sum "$SANDBOX/config.json" | cut -d' ' -f1)
MTIME_BEFORE=$(stat -c %Y "$SANDBOX/config.json")
sleep 1.1
expect_exit "bootstrap is idempotent on existing config" 0 -- \
env MOSAIC_CONFIG="$SANDBOX/config.json" $CONFIG_OP bootstrap
SUM_AFTER=$(sha256sum "$SANDBOX/config.json" | cut -d' ' -f1)
MTIME_AFTER=$(stat -c %Y "$SANDBOX/config.json")
if [ "$SUM_BEFORE" = "$SUM_AFTER" ] && [ "$MTIME_BEFORE" = "$MTIME_AFTER" ]; then
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} bootstrap did not rewrite existing config"
else
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} bootstrap rewrote existing config"
fi
# --- validate ---
expect_exit "validate missing config exits 3" 3 -- \
env MOSAIC_CONFIG="$SANDBOX/absent.json" $CONFIG_OP validate
cfg invalid.json '{'
expect_exit "malformed JSON exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/invalid.json" $CONFIG_OP validate
cfg badversion.json '{"configVersion":2,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "unsupported configVersion exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/badversion.json" $CONFIG_OP validate
cfg unknownkey.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","extra":true,"execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "unknown top-level key exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/unknownkey.json" $CONFIG_OP validate
cfg unknownexec.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m","extra":1}}'
expect_exit "unknown execution key exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/unknownexec.json" $CONFIG_OP validate
cfg badbackend.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"podman","provider":"zai","model":"m"}}'
expect_exit "unsupported backend exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/badbackend.json" $CONFIG_OP validate
cfg badenv.json '{"configVersion":1,"environment":"staging","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "unsupported environment exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/badenv.json" $CONFIG_OP validate
cfg relative.json '{"configVersion":1,"environment":"development","dataRoot":"relative/path","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "relative dataRoot exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/relative.json" $CONFIG_OP validate
cfg traversal.json '{"configVersion":1,"environment":"development","dataRoot":"/tmp/../home/x","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "non-canonical dataRoot exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/traversal.json" $CONFIG_OP validate
cfg root.json '{"configVersion":1,"environment":"development","dataRoot":"/","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "filesystem root dataRoot exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/root.json" $CONFIG_OP validate
cfg home.json '{"configVersion":1,"environment":"development","dataRoot":"'$HOME'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "home directory dataRoot exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/home.json" $CONFIG_OP validate
cfg cfgdir.json '{"configVersion":1,"environment":"development","dataRoot":"'$SANDBOX'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
expect_exit "dataRoot containing config dir exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/cfgdir.json" $CONFIG_OP validate
cfg ctrlchar.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"z\nai","model":"m"}}'
expect_exit "control character in provider exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/ctrlchar.json" $CONFIG_OP validate
cfg symlink.json 'placeholder'
ln -sf "$SANDBOX/invalid.json" "$SANDBOX/symlink.json"
expect_exit "symlinked config file exits 2" 2 -- \
env MOSAIC_CONFIG="$SANDBOX/symlink.json" $CONFIG_OP validate
# --- env resolution ---
cfg valid.json "$(valid_body "$DATA_ROOT")"
EVAL_OUT="$(MOSAIC_CONFIG="$SANDBOX/valid.json" $CONFIG_OP env)" || true
if eval "$EVAL_OUT" 2>/dev/null && [ "$MOSAIC_DATA_ROOT" = "$DATA_ROOT" ] \
&& [ "$MOSAIC_PROVIDER" = "zai" ] && [ "$MOSAIC_MODEL" = "glm-5.3-flash" ]; then
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} env exports resolve correctly"
else
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} env exports resolve correctly"
fi
# --- Q175 phase 1 (#1690), design test 25: the optional console key ---
# Shape only. The slot path is a FIFO, so a loader that opened the slot would block and the
# timeout would fail the case; no case reads a slot file.
mkdir -p "$SANDBOX/secrets/mosaic-stack"
mkfifo "$SANDBOX/secrets/mosaic-stack/console-sso-test"
sso_body() {
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m"},"console":%s}' "$DATA_ROOT" "$1"
}
# provider [ALGS] [CLIENT_SECRET]: one provider entry; an empty argument takes the valid value.
provider() {
local algs='["RS256"]' secret='{"slot":"console-sso-test"}'
[ -n "${1:-}" ] && algs="$1"
[ -n "${2:-}" ] && secret="$2"
printf '{"id":"fake","issuer":"https://op.test/app/","clientId":"client-1","clientSecret":%s,"algs":%s,"subjects":[{"sub":"user-1","name":"jason.woltje"}]}' "$secret" "$algs"
}
cfg sso-valid.json "$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider)"']}}')"
OUT="$(MOSAIC_CONFIG="$SANDBOX/sso-valid.json" timeout 20 $CONFIG_OP validate 2>&1)"
[ $? -eq 0 ] && printf '%s' "$OUT" | grep -q '"publicOrigin": "https://console.test"'
check "valid console.sso loads and is in the output" $?
# expect_refusal NAME PATH-IN-MESSAGE BODY: exit 2, and the message names the bad key path.
expect_refusal() {
cfg sso-bad.json "$2"
local err rc
err="$(MOSAIC_CONFIG="$SANDBOX/sso-bad.json" timeout 20 $CONFIG_OP validate 2>&1 >/dev/null)"
rc=$?
[ "$rc" -eq 2 ] && printf '%s' "$err" | grep -qF -- "$3"
check "$1 (exit $rc)" $?
}
expect_refusal "console.sso HS256 alg exits 2 naming the path" \
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider '["HS256"]')"']}}')" 'console.sso.providers[0].algs[0] must be one of'
expect_refusal "console.sso alg none exits 2 naming the path" \
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider '["RS256","none"]')"']}}')" 'console.sso.providers[0].algs[1] must be one of'
expect_refusal "console.sso missing slot field exits 2 naming the path" \
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider '' '{}')"']}}')" 'console.sso.providers[0].clientSecret.slot is required'
expect_refusal "console.sso slot name outside the pattern exits 2 naming the path" \
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider '' '{"slot":"../x"}')"']}}')" 'console.sso.providers[0].clientSecret.slot must match'
expect_refusal "unknown key under console exits 2 naming the path" \
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider)"']},"extra":1}')" 'console.extra is not a supported key'
# Without the key: the output is exactly what the loader printed before phase 1 (sage-1340, F2).
cfg no-console.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
EXPECTED="$(printf '{\n "configVersion": 1,\n "environment": "development",\n "dataRoot": "%s",\n "execution": {\n "backend": "docker",\n "provider": "zai",\n "model": "m",\n "adapter": "pi"\n }\n}' "$DATA_ROOT")"
[ "$(MOSAIC_CONFIG="$SANDBOX/no-console.json" timeout 20 $CONFIG_OP validate)" = "$EXPECTED" ]
check "config without console loads with unchanged output" $?
# --- validation must not modify the file ---
SUM_INVALID_BEFORE=$(sha256sum "$SANDBOX/invalid.json" | cut -d' ' -f1)
MOSAIC_CONFIG="$SANDBOX/invalid.json" $CONFIG_OP validate >/dev/null 2>&1
SUM_INVALID_AFTER=$(sha256sum "$SANDBOX/invalid.json" | cut -d' ' -f1)
if [ "$SUM_INVALID_BEFORE" = "$SUM_INVALID_AFTER" ]; then
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} failed validation modified nothing"
else
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} failed validation modified the file"
fi
echo
echo "selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]