#!/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 < "$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 ]