296 lines
18 KiB
Bash
Executable File
296 lines
18 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'
|
|
|
|
# --- FED R2 (#1701, DESIGN.md section 3.1): the optional federation key ---
|
|
# fed_body FEDERATION-JSON: a config whose federation key is the argument.
|
|
fed_body() {
|
|
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m"},"federation":%s}' "$DATA_ROOT" "$1"
|
|
}
|
|
FED_VALID='{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"Software factory"}]}'
|
|
|
|
cfg fed-valid.json "$(fed_body "$FED_VALID")"
|
|
OUT="$(MOSAIC_CONFIG="$SANDBOX/fed-valid.json" timeout 20 $CONFIG_OP validate)"
|
|
[ $? -eq 0 ] && printf '%s' "$OUT" | grep -q '"self": "hub"' && printf '%s' "$OUT" | grep -q '"origin": "https://mosaic.woltje.com"'
|
|
check "valid federation key loads and is in the output" $?
|
|
|
|
# expect_fed_refusal NAME PATH-IN-MESSAGE FEDERATION-JSON: exit 2, and the message names the bad key path.
|
|
expect_fed_refusal() {
|
|
cfg fed-bad.json "$(fed_body "$3")"
|
|
local err rc
|
|
err="$(MOSAIC_CONFIG="$SANDBOX/fed-bad.json" timeout 20 $CONFIG_OP validate 2>&1 >/dev/null)"
|
|
rc=$?
|
|
[ "$rc" -eq 2 ] && printf '%s' "$err" | grep -qF -- "$2"
|
|
check "$1 (exit $rc)" $?
|
|
}
|
|
expect_fed_refusal "federation unknown sub-key exits 2 naming the key" 'unsupported federation key: "extra"' \
|
|
'{"self":"hub","peers":[],"extra":1}'
|
|
expect_fed_refusal "federation peer unknown sub-key exits 2 naming the key" 'unsupported federation.peers[0] key: "trust"' \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"F","trust":"full"}]}'
|
|
expect_fed_refusal "federation peer origin over http exits 2 naming the key" "federation.peers[0].origin" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"http://mosaic.woltje.com","label":"F"}]}'
|
|
expect_fed_refusal "federation peer origin with a path exits 2 naming the key" "federation.peers[0].origin" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com/console","label":"F"}]}'
|
|
expect_fed_refusal "federation peer origin with userinfo exits 2 naming the key" "federation.peers[0].origin" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://[email protected]","label":"F"}]}'
|
|
expect_fed_refusal "federation peer id equal to self exits 2 naming the key" "federation.peers[0].id" \
|
|
'{"self":"hub","peers":[{"id":"hub","origin":"https://mosaic.woltje.com","label":"F"}]}'
|
|
expect_fed_refusal "federation peer label empty exits 2 naming the key" "federation.peers[0].label" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":""}]}'
|
|
expect_fed_refusal "federation not an object exits 2 naming the key" '"federation" must be a JSON object' '"hub"'
|
|
expect_fed_refusal "federation self pattern exits 2 naming the key" "federation.self must match" \
|
|
'{"self":"Hub","peers":[]}'
|
|
expect_fed_refusal "federation peers not an array exits 2 naming the key" "federation.peers must be an array" \
|
|
'{"self":"hub","peers":{}}'
|
|
expect_fed_refusal "federation peer not an object exits 2 naming the key" "federation.peers[0] must be a JSON object" \
|
|
'{"self":"hub","peers":["factory"]}'
|
|
expect_fed_refusal "federation peer id pattern exits 2 naming the key" "federation.peers[0].id must match" \
|
|
'{"self":"hub","peers":[{"id":"Factory","origin":"https://mosaic.woltje.com","label":"F"}]}'
|
|
FED_LONG_LABEL="$(printf 'x%.0s' $(seq 81))"
|
|
expect_fed_refusal "federation peer label over 80 characters exits 2 naming the key" "federation.peers[0].label" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"'"$FED_LONG_LABEL"'"}]}'
|
|
expect_fed_refusal "federation duplicate peer id exits 2 naming the key" 'federation.peers[1].id "factory" is already named' \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"F"},{"id":"factory","origin":"https://other.woltje.com","label":"G"}]}'
|
|
expect_fed_refusal "federation peer origin with a trailing slash exits 2 naming the key" "federation.peers[0].origin" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com/","label":"F"}]}'
|
|
# A missing value: the pattern test coerces undefined to "undefined", which matches, so the typeof half is what refuses.
|
|
expect_fed_refusal "federation self missing exits 2 naming the key" "federation.self must match" \
|
|
'{"peers":[]}'
|
|
expect_fed_refusal "federation peer id missing exits 2 naming the key" "federation.peers[0].id must match" \
|
|
'{"self":"hub","peers":[{"origin":"https://mosaic.woltje.com","label":"F"}]}'
|
|
expect_fed_refusal "federation peer label missing exits 2 naming the key" "federation.peers[0].label must be" \
|
|
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com"}]}'
|
|
|
|
# Without the key: no federation in the output, and the config still loads (FED R2 boundary).
|
|
cfg no-federation.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
|
NO_FED_OUT="$(MOSAIC_CONFIG="$SANDBOX/no-federation.json" timeout 20 $CONFIG_OP validate)"
|
|
NO_FED_RC=$?
|
|
[ "$NO_FED_RC" -eq 0 ] && ! printf '%s' "$NO_FED_OUT" | grep -q '"federation"'
|
|
check "config without federation loads and has no federation output" $?
|
|
|
|
# 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" $?
|
|
|
|
# --- Row 270 r2 (mos-1352, DECISIONS Q222): the optional compactionReadBytes key ---
|
|
# The loader passes it on as written, a bad value too: the launcher takes the default for a
|
|
# missing or bad one (packages/seat/src/compactions.mjs readBudget), so it never refuses a config.
|
|
for v in 8388608 '"x"'; do
|
|
cfg budget.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"},"compactionReadBytes":'"$v"'}'
|
|
OUT="$(MOSAIC_CONFIG="$SANDBOX/budget.json" timeout 20 $CONFIG_OP validate 2>&1)"
|
|
[ $? -eq 0 ] && printf '%s' "$OUT" | grep -qxF " \"compactionReadBytes\": $v"
|
|
check "compactionReadBytes $v loads and is in the output as written" $?
|
|
done
|
|
# Row 270 r3 (sage-3038): compactionStreams the same way (readStreams).
|
|
for v in 2048 '"x"'; do
|
|
cfg streams.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"},"compactionStreams":'"$v"'}'
|
|
OUT="$(MOSAIC_CONFIG="$SANDBOX/streams.json" timeout 20 $CONFIG_OP validate 2>&1)"
|
|
[ $? -eq 0 ] && printf '%s' "$OUT" | grep -qxF " \"compactionStreams\": $v"
|
|
check "compactionStreams $v loads and is in the output as written" $?
|
|
done
|
|
|
|
# --- 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 ]
|