Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
48 KiB
JSON
1 line
48 KiB
JSON
{"repo": "akhenda/ubuntu-openclaw-server", "files": {"tests/test_spacebot_provider_onboarding.sh": {"1c5ec5c119ea0e45978a5f309543520d279e5957": {"t": 1786068346.114249, "n": 45864, "h": "0e6072cd4d60164c", "c": "#!/usr/bin/env bash\nset -euo pipefail\n\nROOT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\ntmp_dir=\"$(mktemp -d)\"\ntrap 'rm -rf \"$tmp_dir\"' EXIT\n\nfail() { printf 'FAIL: %s\\n' \"$*\" >&2; exit 1; }\n\ncat >\"$tmp_dir/references.toml\" <<'EOF_REFS'\n[llm]\nminimax_key = \"secret:MINIMAX_API_KEY\"\ndeepseek_key = \"env:DEEPSEEK_API_KEY\"\nollama_key = \"secret:OLLAMA_API_KEY\"\ncerebras_key = \"env:CEREBRAS_API_KEY\"\nsambanova_key = \"secret:SAMBANOVA_API_KEY\"\ngemini_key = \"\"\nEOF_REFS\npython3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/references.toml\" >/dev/null || fail 'safe references rejected'\n\ncat >\"$tmp_dir/telegram-references.toml\" <<'EOF_TELEGRAM_REFS'\n[messaging.telegram]\nenabled = true\ntoken = \"secret:TELEGRAM_BOT_TOKEN\"\n\n[[messaging.telegram.instances]]\nname = \"McKay\"\nenabled = true\ntoken = \"env:MCKAY_TELEGRAM_BOT_TOKEN\"\nEOF_TELEGRAM_REFS\npython3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/telegram-references.toml\" >/dev/null || \\\n fail 'safe Telegram references rejected'\n\nliteral='fixture-provider-secret-value'\ncat >\"$tmp_dir/literal.toml\" <<EOF_LITERAL\n[llm]\nminimax_key = \"$literal\"\nEOF_LITERAL\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/literal.toml\" 2>&1)\"; then\n fail 'literal provider credential accepted'\nfi\n[[ \"$output\" == *'llm.minimax_key'* ]] || fail 'unsafe field name missing'\n[[ \"$output\" != *\"$literal\"* ]] || fail 'literal credential leaked'\n\ncat >\"$tmp_dir/telegram-literal.toml\" <<EOF_TELEGRAM_LITERAL\n[messaging.telegram]\nenabled = true\ntoken = \"$literal\"\nEOF_TELEGRAM_LITERAL\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/telegram-literal.toml\" 2>&1)\"; then\n fail 'literal Telegram credential accepted'\nfi\n[[ \"$output\" == *'messaging.telegram.token'* ]] || \\\n fail 'unsafe Telegram field name missing'\n[[ \"$output\" != *\"$literal\"* ]] || fail 'literal Telegram credential leaked'\n\ncat >\"$tmp_dir/telegram-instance-literal.toml\" <<EOF_TELEGRAM_INSTANCE_LITERAL\n[messaging.telegram]\nenabled = true\ntoken = \"secret:TELEGRAM_BOT_TOKEN\"\n\n[[messaging.telegram.instances]]\nname = \"McKay\"\nenabled = true\ntoken = \"$literal\"\nEOF_TELEGRAM_INSTANCE_LITERAL\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/telegram-instance-literal.toml\" 2>&1)\"; then\n fail 'literal named Telegram credential accepted'\nfi\n[[ \"$output\" == *'messaging.telegram.instances[0].token'* ]] || \\\n fail 'unsafe named Telegram field name missing'\n[[ \"$output\" != *\"$literal\"* ]] || fail 'named Telegram credential leaked'\n\nfor official_field in ollama_key cerebras_key sambanova_key; do\n cat >\"$tmp_dir/official-literal.toml\" <<EOF_OFFICIAL_LITERAL\n[llm]\n${official_field} = \"$literal\"\nEOF_OFFICIAL_LITERAL\n if output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/official-literal.toml\" 2>&1)\"; then\n fail \"literal ${official_field} credential accepted\"\n fi\n [[ \"$output\" == *\"llm.${official_field}\"* ]] || \\\n fail \"unsafe ${official_field} field name missing\"\n [[ \"$output\" != *\"$literal\"* ]] || \\\n fail \"literal ${official_field} credential leaked\"\ndone\n\ncat >\"$tmp_dir/malformed-reference.toml\" <<'EOF_MALFORMED_REFERENCE'\n[llm]\nollama_key = \"secret:\"\ncerebras_key = \"env:lowercase\"\nEOF_MALFORMED_REFERENCE\nif python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/malformed-reference.toml\" >/dev/null 2>&1; then\n fail 'malformed provider references accepted'\nfi\n\ncat >\"$tmp_dir/malformed.toml\" <<'EOF_MALFORMED_TOML'\n[llm\nollama_key = \"secret:OLLAMA_API_KEY\"\nEOF_MALFORMED_TOML\nif python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/malformed.toml\" >/dev/null 2>&1; then\n fail 'malformed TOML accepted'\nfi\n\ncat >\"$tmp_dir/nested-references.toml\" <<'EOF_NESTED_REFS'\n[llm.providers.custom]\napi_key = \"secret:CUSTOM_API_KEY\"\nclient_secret = \"env:CUSTOM_CLIENT_SECRET\"\nEOF_NESTED_REFS\npython3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/nested-references.toml\" >/dev/null || \\\n fail 'safe nested references rejected'\n\ncat >\"$tmp_dir/nested-literal.toml\" <<EOF_NESTED_LITERAL\n[llm.providers.custom]\napi_key = \"$literal\"\nEOF_NESTED_LITERAL\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/nested-literal.toml\" 2>&1)\"; then\n fail 'nested literal provider credential accepted'\nfi\n[[ \"$output\" == *'llm.providers.custom.api_key'* ]] || \\\n fail 'unsafe nested field name missing'\n[[ \"$output\" != *\"$literal\"* ]] || fail 'nested literal credential leaked'\n\ncat >\"$tmp_dir/providers-array.toml\" <<'EOF_PROVIDERS_ARRAY'\n[llm]\nproviders = []\nEOF_PROVIDERS_ARRAY\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/providers-array.toml\" 2>&1)\"; then\n fail 'array providers container accepted'\nfi\n[[ \"$output\" == *'llm.providers'* ]] || fail 'unsafe providers container missing'\n\ncat >\"$tmp_dir/provider-scalar.toml\" <<'EOF_PROVIDER_SCALAR'\n[llm]\nproviders = { custom = \"unexpected\" }\nEOF_PROVIDER_SCALAR\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/provider-scalar.toml\" 2>&1)\"; then\n fail 'scalar provider entry accepted'\nfi\n[[ \"$output\" == *'llm.providers.custom'* ]] || fail 'unsafe provider entry missing'\n\ncat >\"$tmp_dir/provider-array-table.toml\" <<'EOF_PROVIDER_ARRAY_TABLE'\n[[llm.providers.custom]]\napi_key = \"secret:CUSTOM_API_KEY\"\nEOF_PROVIDER_ARRAY_TABLE\nif output=\"$(python3 \"$ROOT_DIR/scripts/check_spacebot_secret_references.py\" \\\n \"$tmp_dir/provider-array-table.toml\" 2>&1)\"; then\n fail 'array-of-table provider entry accepted'\nfi\n[[ \"$output\" == *'llm.providers.custom'* ]] || \\\n fail 'unsafe array-of-table provider entry missing'\n\nsource \"$ROOT_DIR/scripts/lib/common.sh\"\nsource \"$ROOT_DIR/scripts/lib/config.sh\"\nsource \"$ROOT_DIR/scripts/lib/spacebot_routing.sh\"\n[[ -f \"$ROOT_DIR/scripts/lib/spacebot_provider_onboarding.sh\" ]] || \\\n fail 'onboarding library unavailable'\nsource \"$ROOT_DIR/scripts/lib/spacebot_provider_onboarding.sh\"\n\nSPACEBOT_AGENT_ID=mckay\nSPACEBOT_CARLA_ID=carla\nSPACEBOT_ADMIN_ID='admin'\nSPACEBOT_GERTRUDE_ID=gertrude\nSPACEBOT_API_PORT=19898\nSPACEBOT_CHANNEL_MODEL=minimax/MiniMax-M3\nSPACEBOT_BRANCH_MODEL=openai-chatgpt/gpt-5.6\nSPACEBOT_WORKER_MODEL=deepseek/deepseek-v4-flash\nSPACEBOT_COMPACTOR_MODEL=deepseek/deepseek-v4-flash\nSPACEBOT_CORTEX_MODEL=deepseek/deepseek-v4-flash\nSPACEBOT_CONFIG_SOURCE=\"$tmp_dir/references.toml\"\nSPACEBOT_ROOT_DIR=\"$tmp_dir/runtime\"\n\napi_tmp_dir=\"$tmp_dir/api-tmp\"\ndocker_args=\"$tmp_dir/docker-args\"\ndocker_stdin=\"$tmp_dir/docker-stdin\"\ndocker_modes=\"$tmp_dir/docker-modes\"\nmkdir -p \"$api_tmp_dir\"\ncaller_traps_before=\"$(trap -p EXIT INT TERM HUP)\"\n\nassert_tmp_empty() {\n local path=\"$1\" message=\"$2\"\n [[ -z \"$(find \"$path\" -mindepth 1 -print -quit)\" ]] || fail \"$message\"\n}\n\nfile_mode() {\n local path=\"$1\"\n if stat -f '%Lp' /dev/null >/dev/null 2>&1; then\n stat -f '%Lp' \"$path\"\n else\n stat -c '%a' \"$path\"\n fi\n}\n\nchmod() {\n local mode=\"$1\" path result\n command chmod \"$@\"\n result=$?\n if ((result == 0)) && [[ \"$mode\" == 0600 ]]; then\n shift\n for path in \"$@\"; do\n file_mode \"$path\" >>\"$docker_modes\"\n done\n fi\n return \"$result\"\n}\n\ndocker() {\n printf '%s\\n' \"$@\" >\"$docker_args\"\n cat >\"$docker_stdin\"\n printf '%s\\n%s' '{\"transport\":\"ok\"}' '201'\n}\n\nrequest_payload='{\"transport\":\"stdin-only\"}'\nTMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n POST /api/test \"$request_payload\" true || fail 'API transport failed'\n[[ \"$SPACEBOT_ONBOARDING_API_STATUS\" == 201 ]] || fail 'HTTP status not captured'\n[[ \"$SPACEBOT_ONBOARDING_API_BODY\" == '{\"transport\":\"ok\"}' ]] || \\\n fail 'HTTP body not captured'\n[[ \"$(cat \"$docker_stdin\")\" == \"$request_payload\" ]] || \\\n fail 'API payload not passed over stdin'\nif grep -Fq -- \"$request_payload\" \"$docker_args\"; then\n fail 'API payload placed on Docker command line'\nfi\ngrep -Fqx -- '--data-binary' \"$docker_args\" || fail 'stdin upload option missing'\ngrep -Fqx -- '@-' \"$docker_args\" || fail 'curl does not read payload from stdin'\ngrep -Fqx -- 'http://127.0.0.1:19898/api/test' \"$docker_args\" || \\\n fail 'API transport does not use numeric loopback'\nif grep -Fq -- 'localhost' \"$docker_args\"; then\n fail 'API transport still uses localhost'\nfi\n[[ \"$(sort -u \"$docker_modes\")\" == 600 ]] || fail 'API temporary file mode is not 0600'\nassert_tmp_empty \"$api_tmp_dir\" 'API temporary files not cleaned after success'\nTMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n GET /api/test '' false || fail 'body-free API transport failed'\n[[ -z \"$SPACEBOT_ONBOARDING_API_BODY\" ]] || fail 'API body captured when disabled'\n\ndocker() {\n cat >/dev/null\n return 1\n}\nif TMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n POST /api/test \"$request_payload\" true 2>/dev/null; then\n fail 'failed Docker transport accepted'\nfi\nassert_tmp_empty \"$api_tmp_dir\" 'API temporary files not cleaned after failure'\nunset -f docker\nunset -f chmod\n\ndocker() {\n cat >/dev/null\n printf '%s\\n%s' '{\"transport\":\"ok\"}' 'invalid-status'\n}\nif TMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n GET /api/test '' true 2>/dev/null; then\n fail 'malformed HTTP framing accepted'\nfi\nassert_tmp_empty \"$api_tmp_dir\" 'API temporary files not cleaned after malformed framing'\n\ndocker() {\n cat >/dev/null\n printf '%s\\n%s' '{\"transport\":\"ok\"}' '200'\n}\nsed() { return 1; }\nif TMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n GET /api/test '' true 2>/dev/null; then\n fail 'body-capture failure accepted'\nfi\nunset -f sed\nassert_tmp_empty \"$api_tmp_dir\" 'API temporary files not cleaned after body-capture failure'\n\nchmod() { return 1; }\nif TMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n POST /api/test \"$request_payload\" true 2>/dev/null; then\n fail 'chmod failure accepted'\nfi\nunset -f chmod\nassert_tmp_empty \"$api_tmp_dir\" 'API temporary files not cleaned after chmod failure'\n\nprintf() {\n if [[ \"${1:-}\" == '%s' && \"${2:-}\" == \"$request_payload\" ]]; then\n return 1\n fi\n builtin printf \"$@\"\n}\nif TMPDIR=\"$api_tmp_dir\" spacebot_onboarding_api_request \\\n POST /api/test \"$request_payload\" true 2>/dev/null; then\n fail 'request-write failure accepted'\nfi\nunset -f printf\nassert_tmp_empty \"$api_tmp_dir\" 'API temporary files not cleaned after request-write failure'\nunset -f docker\n\ncat >\"$tmp_dir/api-fault.sh\" <<'EOF_API_FAULT'\n#!/usr/bin/env bash\nset -u\nsource \"$TEST_ROOT/scripts/lib/common.sh\"\nsource \"$TEST_ROOT/scripts/lib/spacebot_provider_onboarding.sh\"\n\nSPACEBOT_API_PORT=19898\nSPACEBOT_ROOT_DIR=\"$TEST_RUNTIME\"\n\ncase \"$1\" in\n unset)\n unset SPACEBOT_ROOT_DIR\n ;;\n INT|TERM)\n TEST_SIGNAL=\"$1\"\n docker() {\n bash -c 'kill -s \"$1\" \"$PPID\"' _ \"$TEST_SIGNAL\"\n }\n ;;\n *) exit 2 ;;\nesac\n\nspacebot_onboarding_api_request GET /api/test '' true\nEOF_API_FAULT\n\nfor fault in unset INT TERM; do\n fault_tmp=\"$tmp_dir/api-fault-${fault}\"\n mkdir \"$fault_tmp\"\n if TEST_ROOT=\"$ROOT_DIR\" TEST_RUNTIME=\"$tmp_dir/runtime\" \\\n TMPDIR=\"$fault_tmp\" bash \"$tmp_dir/api-fault.sh\" \"$fault\" \\\n >/dev/null 2>&1; then\n fail \"API ${fault} fault accepted\"\n fi\n assert_tmp_empty \"$fault_tmp\" \"API temporary files not cleaned after ${fault} fault\"\ndone\n\ncaller_traps_after=\"$(trap -p EXIT INT TERM HUP)\"\n[[ \"$caller_traps_after\" == \"$caller_traps_before\" ]] || \\\n fail 'API transport changed caller traps'\n\nexpected_models='minimax/MiniMax-M3\nopenai-chatgpt/gpt-5.6\ndeepseek/deepseek-v4-flash\ndeepseek/deepseek-v4-flash\ndeepseek/deepseek-v4-flash'\n[[ \"$(spacebot_onboarding_models)\" == \"$expected_models\" ]] || \\\n fail 'onboarding model list mismatch'\n\ntrace_payload_marker='fixture-xtrace-payload-marker'\ntrace_response_marker='fixture-xtrace-response-marker'\noriginal_channel_model=\"$SPACEBOT_CHANNEL_MODEL\"\nSPACEBOT_CHANNEL_MODEL=\"minimax/${trace_payload_marker}\"\ndocker() {\n local argument body request_method=''\n cat >/dev/null\n while (($# > 0)); do\n argument=\"$1\"\n shift\n if [[ \"$argument\" == '--request' ]]; then\n request_method=\"$1\"\n shift\n fi\n done\n if [[ \"$request_method\" == PUT ]]; then\n printf '%s\\n%s' \"{\\\"marker\\\":\\\"${trace_response_marker}\\\"}\" '200'\n return 0\n fi\n body=\"$(jq -nc \\\n --arg marker \"$trace_response_marker\" \\\n --arg channel \"$SPACEBOT_CHANNEL_MODEL\" \\\n --arg branch \"$SPACEBOT_BRANCH_MODEL\" \\\n --arg worker \"$SPACEBOT_WORKER_MODEL\" \\\n --arg compactor \"$SPACEBOT_COMPACTOR_MODEL\" \\\n --arg cortex \"$SPACEBOT_CORTEX_MODEL\" \\\n '{marker:$marker,routing:{channel:$channel,branch:$branch,worker:$worker,compactor:$compactor,cortex:$cortex}}')\"\n printf '%s\\n%s' \"$body\" '200'\n}\n\ntrace_log=\"$tmp_dir/api-xtrace\"\n{\n set -x\n spacebot_onboarding_reconcile_routing\n trace_state=\"$-\"\n set +x\n} 2>\"$trace_log\" || fail 'routing reconciliation failed under xtrace'\n[[ \"$trace_state\" == *x* ]] || fail 'API transport did not restore caller xtrace'\nif grep -Fq -- \"$trace_payload_marker\" \"$trace_log\"; then\n fail 'API payload leaked through xtrace'\nfi\nif grep -Fq -- \"$trace_response_marker\" \"$trace_log\"; then\n fail 'API response leaked through xtrace'\nfi\nSPACEBOT_CHANNEL_MODEL=\"$original_channel_model\"\nunset -f docker\n\nrequests=\"$tmp_dir/requests\"\nput_payloads=\"$tmp_dir/put-payloads\"\nmock_api_status=200\nmock_carla_put_status=200\nmock_provider_body='{\"has_any\":true,\"providers\":{\"minimax\":true,\"openai_chatgpt\":true,\"deepseek\":true}}'\nmock_agents_body='{\"agents\":[{\"id\":\"mckay\"},{\"id\":\"carla\"}]}'\nmock_humans_body='{\"humans\":[{\"id\":\"admin\"},{\"id\":\"gertrude\"}]}'\nmock_topology_body='{\"agents\":[{\"id\":\"mckay\"},{\"id\":\"carla\"}],\"humans\":[{\"id\":\"admin\"},{\"id\":\"gertrude\"}],\"links\":[{\"from\":\"admin\",\"to\":\"mckay\",\"direction\":\"one_way\",\"kind\":\"hierarchical\"},{\"from\":\"gertrude\",\"to\":\"carla\",\"direction\":\"one_way\",\"kind\":\"hierarchical\"}]}'\nmock_migration_body='{\"migrated\":[{\"config_key\":\"llm.minimax_key\",\"secret_name\":\"MINIMAX_API_KEY\",\"category\":\"system\"}],\"skipped\":[],\"message\":\"Migrated 1 secrets.\"}'\nmock_mckay_config_body='{\"routing\":{\"channel\":\"minimax/MiniMax-M3\",\"branch\":\"openai-chatgpt/gpt-5.6\",\"worker\":\"deepseek/deepseek-v4-flash\",\"compactor\":\"deepseek/deepseek-v4-flash\",\"cortex\":\"deepseek/deepseek-v4-flash\"}}'\nmock_carla_config_body=\"$mock_mckay_config_body\"\n: >\"$requests\"\n: >\"$put_payloads\"\nspacebot_onboarding_api_request() {\n local xtrace_was_enabled=false agent_id\n case \"$-\" in\n *x*) xtrace_was_enabled=true; set +x ;;\n esac\n printf '%s %s\\n' \"$1\" \"$2\" >>\"$requests\"\n SPACEBOT_ONBOARDING_API_STATUS=\"$mock_api_status\"\n case \"$2\" in\n /api/providers)\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_provider_body\"\n ;;\n /api/agents)\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_agents_body\"\n ;;\n /api/links/humans)\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_humans_body\"\n ;;\n /api/topology)\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_topology_body\"\n ;;\n /api/secrets/migrate)\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_migration_body\"\n ;;\n '/api/agents/config?agent_id=mckay')\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_mckay_config_body\"\n ;;\n '/api/agents/config?agent_id=carla')\n SPACEBOT_ONBOARDING_API_BODY=\"$mock_carla_config_body\"\n ;;\n /api/agents/config)\n agent_id=\"$(jq -er '.agent_id' <<<\"$3\")\" || return 1\n printf '%s\\n' \"$3\" >>\"$put_payloads\"\n if [[ \"$agent_id\" == \"$SPACEBOT_CARLA_ID\" ]]; then\n SPACEBOT_ONBOARDING_API_STATUS=\"$mock_carla_put_status\"\n fi\n SPACEBOT_ONBOARDING_API_BODY='{}'\n ;;\n *)\n [[ \"$xtrace_was_enabled\" == false ]] || set -x\n return 1\n ;;\n esac\n [[ \"$xtrace_was_enabled\" == false ]] || set -x\n}\n\nprovider_trace_marker='fixture-provider-response-xtrace-marker'\nmock_provider_body=\"{\\\"marker\\\":\\\"${provider_trace_marker}\\\",\\\"has_any\\\":true,\\\"providers\\\":{\\\"minimax\\\":true,\\\"openai_chatgpt\\\":true,\\\"deepseek\\\":true}}\"\nprovider_trace_log=\"$tmp_dir/provider-xtrace\"\n{\n set -x\n spacebot_onboarding_require_providers\n provider_trace_state=\"$-\"\n set +x\n} 2>\"$provider_trace_log\" || fail 'provider preflight failed under xtrace'\n[[ \"$provider_trace_state\" == *x* ]] || \\\n fail 'provider preflight did not restore caller xtrace'\n! grep -Fq \"$provider_trace_marker\" \"$provider_trace_log\" || \\\n fail 'provider response leaked through xtrace'\nmock_provider_body='{\"has_any\":true,\"providers\":{\"minimax\":true,\"openai_chatgpt\":true,\"deepseek\":true}}'\n\nruntime_trace_marker='fixture-runtime-response-xtrace-marker'\nmock_topology_body=\"$(jq -c --arg marker \"$runtime_trace_marker\" '. + {marker:$marker}' \\\n <<<\"$mock_topology_body\")\"\nruntime_trace_log=\"$tmp_dir/runtime-preflight-xtrace\"\n{\n set -x\n spacebot_onboarding_require_runtime_state\n runtime_trace_state=\"$-\"\n set +x\n} 2>\"$runtime_trace_log\" || fail 'runtime-state preflight failed under xtrace'\n[[ \"$runtime_trace_state\" == *x* ]] || \\\n fail 'runtime-state preflight did not restore caller xtrace'\n! grep -Fq \"$runtime_trace_marker\" \"$runtime_trace_log\" || \\\n fail 'runtime-state response leaked through xtrace'\nmock_topology_body=\"$(jq -c 'del(.marker)' <<<\"$mock_topology_body\")\"\n\nmigration_trace_marker='fixture-migration-response-xtrace-marker'\nmock_migration_body=\"{\\\"migrated\\\":[],\\\"skipped\\\":[],\\\"marker\\\":\\\"${migration_trace_marker}\\\"}\"\nmigration_trace_log=\"$tmp_dir/migration-xtrace\"\n{\n set -x\n spacebot_onboarding_migrate_secrets\n migration_trace_state=\"$-\"\n set +x\n} 2>\"$migration_trace_log\" || fail 'secret migration failed under xtrace'\n[[ \"$migration_trace_state\" == *x* ]] || \\\n fail 'secret migration did not restore caller xtrace'\n! grep -Fq \"$migration_trace_marker\" \"$migration_trace_log\" || \\\n fail 'secret migration metadata leaked through xtrace'\nmock_migration_body='{\"migrated\":[{\"config_key\":\"llm.minimax_key\",\"secret_name\":\"MINIMAX_API_KEY\",\"category\":\"system\"}],\"skipped\":[],\"message\":\"Migrated 1 secrets.\"}'\n\nverify_trace_marker='fixture-routing-response-xtrace-marker'\nmock_mckay_config_body=\"{\\\"marker\\\":\\\"${verify_trace_marker}\\\",\\\"routing\\\":{\\\"channel\\\":\\\"minimax/MiniMax-M3\\\",\\\"branch\\\":\\\"openai-chatgpt/gpt-5.6\\\",\\\"worker\\\":\\\"deepseek/deepseek-v4-flash\\\",\\\"compactor\\\":\\\"deepseek/deepseek-v4-flash\\\",\\\"cortex\\\":\\\"deepseek/deepseek-v4-flash\\\"}}\"\nmock_carla_config_body=\"$mock_mckay_config_body\"\nverify_trace_log=\"$tmp_dir/verify-routing-xtrace\"\n{\n set -x\n spacebot_onboarding_verify_routing\n verify_trace_state=\"$-\"\n set +x\n} 2>\"$verify_trace_log\" || fail 'post-close routing verification failed under xtrace'\n[[ \"$verify_trace_state\" == *x* ]] || \\\n fail 'post-close routing verification did not restore caller xtrace'\n! grep -Fq \"$verify_trace_marker\" \"$verify_trace_log\" || \\\n fail 'post-close routing response leaked through xtrace'\nmock_mckay_config_body='{\"routing\":{\"channel\":\"minimax/MiniMax-M3\",\"branch\":\"openai-chatgpt/gpt-5.6\",\"worker\":\"deepseek/deepseek-v4-flash\",\"compactor\":\"deepseek/deepseek-v4-flash\",\"cortex\":\"deepseek/deepseek-v4-flash\"}}'\nmock_carla_config_body=\"$mock_mckay_config_body\"\n\n: >\"$requests\"\n: >\"$put_payloads\"\nspacebot_onboarding_require_runtime_state || fail 'runtime-state preflight failed'\nspacebot_onboarding_migrate_secrets || fail 'secret migration failed'\nspacebot_onboarding_reconcile_routing || fail 'routing reconciliation failed'\ngrep -Fqx 'GET /api/providers' \"$requests\" || fail 'provider status not requested'\ngrep -Fqx 'POST /api/secrets/migrate' \"$requests\" || fail 'migration not requested'\nexpected_reconcile_requests='GET /api/providers\nGET /api/agents\nGET /api/links/humans\nGET /api/topology\nGET /api/agents/config?agent_id=mckay\nGET /api/agents/config?agent_id=carla\nPOST /api/secrets/migrate\nPUT /api/agents/config\nGET /api/agents/config?agent_id=mckay\nPUT /api/agents/config\nGET /api/agents/config?agent_id=carla'\n[[ \"$(cat \"$requests\")\" == \"$expected_reconcile_requests\" ]] || \\\n fail 'two-agent routing request order mismatch'\njq -se '\n length == 2\n and .[0].agent_id == \"mckay\"\n and .[1].agent_id == \"carla\"\n and all(.[];\n .routing.channel == \"minimax/MiniMax-M3\"\n and .routing.branch == \"openai-chatgpt/gpt-5.6\"\n and .routing.worker == \"deepseek/deepseek-v4-flash\"\n and .routing.compactor == \"deepseek/deepseek-v4-flash\"\n and .routing.cortex == \"deepseek/deepseek-v4-flash\"\n )\n' \"$put_payloads\" >/dev/null || fail 'two-agent routing payload mismatch'\n\nassert_runtime_preflight_failure_without_writes() {\n : >\"$requests\"\n if spacebot_onboarding_require_runtime_state 2>/dev/null; then\n fail 'runtime-state preflight accepted an invalid organization state'\n fi\n ! grep -Eq '^(PUT|POST|DELETE) ' \"$requests\" || \\\n fail 'invalid runtime state reached an API mutation'\n}\n\nbaseline_agents_body=\"$mock_agents_body\"\nbaseline_humans_body=\"$mock_humans_body\"\nbaseline_topology_body=\"$mock_topology_body\"\nbaseline_mckay_config_body=\"$mock_mckay_config_body\"\nbaseline_carla_config_body=\"$mock_carla_config_body\"\n\nmock_agents_body='{\"agents\":[{\"id\":\"mckay\"}]}'\nassert_runtime_preflight_failure_without_writes\nmock_agents_body='{\"agents\":[{\"id\":\"mckay\"},{\"id\":\"carla\"},{\"id\":\"extra\"}]}'\nassert_runtime_preflight_failure_without_writes\nmock_agents_body=\"$baseline_agents_body\"\n\nmock_humans_body='{\"humans\":[{\"id\":\"admin\"}]}'\nassert_runtime_preflight_failure_without_writes\nmock_humans_body='{\"humans\":[{\"id\":\"admin\"},{\"id\":\"gertrude\"},{\"id\":\"extra\"}]}'\nassert_runtime_preflight_failure_without_writes\nmock_humans_body=\"$baseline_humans_body\"\n\nmock_topology_body=\"$(jq -c '.links += [{from:\"admin\",to:\"carla\",direction:\"one_way\",kind:\"hierarchical\"}]' \\\n <<<\"$baseline_topology_body\")\"\nassert_runtime_preflight_failure_without_writes\nmock_topology_body=\"$(jq -c '.links |= map(select(.from != \"gertrude\"))' \\\n <<<\"$baseline_topology_body\")\"\nassert_runtime_preflight_failure_without_writes\nmock_topology_body=\"$(jq -c '.agents = [{id:\"mckay\"}]' \\\n <<<\"$baseline_topology_body\")\"\nassert_runtime_preflight_failure_without_writes\nmock_topology_body=\"$(jq -c '.humans += [{id:\"extra\"}]' \\\n <<<\"$baseline_topology_body\")\"\nassert_runtime_preflight_failure_without_writes\nmock_topology_body=\"$baseline_topology_body\"\n\nmock_mckay_config_body='{\"routing\":\"invalid\"}'\nassert_runtime_preflight_failure_without_writes\nmock_mckay_config_body=\"$baseline_mckay_config_body\"\nmock_carla_config_body='{\"routing\":{\"channel\":\"only-one-field\"}}'\nassert_runtime_preflight_failure_without_writes\nmock_carla_config_body=\"$baseline_carla_config_body\"\n\n: >\"$requests\"\nspacebot_onboarding_verify_routing || fail 'post-close two-agent routing verification failed'\nexpected_verify_requests='GET /api/agents/config?agent_id=mckay\nGET /api/agents/config?agent_id=carla'\n[[ \"$(cat \"$requests\")\" == \"$expected_verify_requests\" ]] || \\\n fail 'post-close routing verification did not independently read both agents'\n\n: >\"$requests\"\n: >\"$put_payloads\"\nmock_carla_put_status=503\nif spacebot_onboarding_reconcile_routing 2>/dev/null; then\n fail 'routing reconciliation accepted Carla PUT failure'\nfi\nmock_carla_put_status=200\n[[ \"$(cat \"$requests\")\" == 'PUT /api/agents/config\nGET /api/agents/config?agent_id=mckay\nPUT /api/agents/config' ]] || fail 'Carla PUT failure did not stop before readback'\n\n: >\"$requests\"\n: >\"$put_payloads\"\nmock_carla_config_body='{\"routing\":{\"channel\":\"wrong/model\",\"branch\":\"openai-chatgpt/gpt-5.6\",\"worker\":\"deepseek/deepseek-v4-flash\",\"compactor\":\"deepseek/deepseek-v4-flash\",\"cortex\":\"deepseek/deepseek-v4-flash\"}}'\nif spacebot_onboarding_reconcile_routing 2>/dev/null; then\n fail 'routing reconciliation accepted Carla readback mismatch'\nfi\nmock_carla_config_body=\"$mock_mckay_config_body\"\ngrep -Fqx 'GET /api/agents/config?agent_id=carla' \"$requests\" || \\\n fail 'Carla routing was not read back after mutation'\n\nmock_api_status=503\nif spacebot_onboarding_require_providers 2>/dev/null; then\n fail 'provider preflight accepted non-200 status'\nfi\nmock_api_status=200\nmock_provider_body='not-json'\nif spacebot_onboarding_require_providers 2>/dev/null; then\n fail 'provider preflight accepted malformed JSON'\nfi\nmock_provider_body='{\"has_any\":true,\"providers\":{\"minimax\":true,\"openai_chatgpt\":true,\"deepseek\":true}}'\nmock_migration_body='not-json'\nif spacebot_onboarding_migrate_secrets 2>/dev/null; then\n fail 'secret migration accepted malformed JSON'\nfi\nmock_migration_body='{\"migrated\":[],\"skipped\":[]}'\nmock_mckay_config_body='not-json'\nif spacebot_onboarding_reconcile_routing 2>/dev/null; then\n fail 'routing reconciliation accepted malformed JSON'\nfi\nmock_mckay_config_body='{\"routing\":{\"channel\":\"minimax/MiniMax-M3\",\"branch\":\"openai-chatgpt/gpt-5.6\",\"worker\":\"deepseek/deepseek-v4-flash\",\"compactor\":\"deepseek/deepseek-v4-flash\",\"cortex\":\"deepseek/deepseek-v4-flash\"}}'\nmock_carla_config_body=\"$mock_mckay_config_body\"\n\nSPACEBOT_ROOT_DIR=\"$tmp_dir/runtime\"\nSPACEBOT_DATA_DIR=\"$SPACEBOT_ROOT_DIR/data\"\nSPACEBOT_CONFIG_SOURCE=\"$SPACEBOT_ROOT_DIR/config/config.toml\"\nSPACEBOT_IMAGE=ghcr.io/spacedriveapp/spacebot:v0.5.0\nSPACEBOT_CONTAINER_NAME=mckay-spacebot\nSPACEBOT_API_PORT=19898\nSPACEBOT_CRON_TIMEZONE=Africa/Nairobi\nSPACEBOT_USER_TIMEZONE=Africa/Nairobi\nSPACEBOT_SANDBOX_REQUIRED=true\nSPACEBOT_HOST=mckay.example.com\nSPACEBOT_IP=172.30.0.10\nEDGE_NETWORK_NAME=mckay-edge\nSPACEBOT_ONBOARDING_RUN_DIR=\"$tmp_dir/run/provider-onboarding\"\nmkdir -p \"$SPACEBOT_ROOT_DIR/config\" \"$SPACEBOT_ONBOARDING_RUN_DIR\"\n: >\"$SPACEBOT_ROOT_DIR/docker-compose.yml\"\nprintf '%s\\n' '[defaults.opencode]' 'enabled = false' >\"$SPACEBOT_CONFIG_SOURCE\"\n\nunsafe_run_target=\"$tmp_dir/unsafe-run-target\"\nunsafe_run_link=\"$tmp_dir/unsafe-run-link\"\nmkdir \"$unsafe_run_target\"\nln -s \"$unsafe_run_target\" \"$unsafe_run_link\"\nif output=\"$(spacebot_onboarding_prepare_run_dir \"$unsafe_run_link\" 2>&1)\"; then\n fail 'symbolic-link onboarding run directory accepted'\nfi\n[[ \"$output\" == *'unsafe onboarding run directory'* ]] || \\\n fail 'symbolic-link onboarding run directory rejection was unclear'\n\n# Tests run without root privileges, so preserve the production install boundary\n# while replacing only its root-ownership side effect.\nspacebot_onboarding_install_override() {\n command install -m 0600 \"$1\" \"$2\"\n}\nspacebot_onboarding_prepare_run_dir() {\n command install -d -m 0700 \"$1\"\n}\n\noverride=\"$tmp_dir/override.yml\"\nspacebot_onboarding_render_override \"$override\"\n[[ \"$(file_mode \"$override\")\" == 600 ]] || fail 'override mode is not 0600'\ngrep -Fq -- \"$SPACEBOT_CONFIG_SOURCE:/data/config.toml:rw\" \"$override\" || \\\n fail 'override does not replace the config mount'\nfor forbidden in ports privileged cap_add devices network_mode docker.sock; do\n ! grep -Fq \"$forbidden\" \"$override\" || fail \"override contains $forbidden\"\ndone\n\nwrite_inspect_fixture() {\n local target=\"$1\" writable=\"$2\"\n cat >\"$target\" <<EOF_INSPECT_FIXTURE\n[{\n \"Config\": {\n \"Image\": \"$SPACEBOT_IMAGE\",\n \"Entrypoint\": [\"/usr/local/bin/mckay-spacebot-entrypoint\"],\n \"Cmd\": [\"spacebot\", \"start\", \"--foreground\"],\n \"User\": \"\",\n \"Env\": [\n \"SPACEBOT_DIR=/data\",\n \"SPACEBOT_DEPLOYMENT=docker\",\n \"SPACEBOT_CRON_TIMEZONE=Africa/Nairobi\",\n \"SPACEBOT_USER_TIMEZONE=Africa/Nairobi\",\n \"SPACEBOT_SANDBOX_REQUIRED=true\",\n \"SPACEBOT_DENY_BIN_DIR=/opt/mckay/deny-bin\"\n ]\n },\n \"State\": {\"Health\": {\"Status\": \"healthy\"}},\n \"HostConfig\": {\n \"Privileged\": false,\n \"CapAdd\": null,\n \"Devices\": null,\n \"DeviceRequests\": null,\n \"DeviceCgroupRules\": null,\n \"PidMode\": \"\",\n \"IpcMode\": \"private\",\n \"UTSMode\": \"\",\n \"UsernsMode\": \"\",\n \"CgroupnsMode\": \"private\",\n \"NetworkMode\": \"mckay-edge\",\n \"PortBindings\": {},\n \"SecurityOpt\": [\"no-new-privileges:true\"],\n \"NanoCpus\": 2000000000,\n \"Memory\": 2147483648,\n \"PidsLimit\": 512,\n \"Init\": true\n },\n \"Mounts\": [\n {\"Source\": \"$SPACEBOT_CONFIG_SOURCE\", \"Destination\": \"/data/config.toml\", \"RW\": $writable},\n {\"Source\": \"$SPACEBOT_DATA_DIR\", \"Destination\": \"/data\", \"RW\": true},\n {\"Source\": \"$SPACEBOT_ROOT_DIR/entrypoint.sh\", \"Destination\": \"/usr/local/bin/mckay-spacebot-entrypoint\", \"RW\": false},\n {\"Source\": \"$SPACEBOT_ROOT_DIR/deny-bin\", \"Destination\": \"/opt/mckay/deny-bin\", \"RW\": false}\n ],\n \"NetworkSettings\": {\n \"Ports\": {},\n \"Networks\": {\"mckay-edge\": {\"IPAddress\": \"172.30.0.10\"}}\n }\n}]\nEOF_INSPECT_FIXTURE\n}\n\nfocused_inspect=\"$tmp_dir/focused-inspect.json\"\nwrite_inspect_fixture \"$focused_inspect\" true\nspacebot_onboarding_inspect_json() { command cat \"$focused_inspect\"; }\nspacebot_onboarding_verify_mount_mode rw || fail 'valid maintenance inspect rejected'\njq '.[0].Mounts += [{\"Source\":\"/var/run/docker.sock\",\"Destination\":\"/var/run/docker.sock\",\"RW\":true}]' \\\n \"$focused_inspect\" >\"$focused_inspect.tmp\"\nmv \"$focused_inspect.tmp\" \"$focused_inspect\"\n! spacebot_onboarding_verify_mount_mode rw 2>/dev/null || \\\n fail 'Docker socket inspect accepted'\n\n# Restore the production inspection boundary after the focused injected fixtures.\nunset -f spacebot_onboarding_inspect_json\nsource \"$ROOT_DIR/scripts/lib/spacebot_provider_onboarding.sh\"\nspacebot_onboarding_install_override() {\n command install -m 0600 \"$1\" \"$2\"\n}\nspacebot_onboarding_prepare_run_dir() {\n command install -d -m 0700 \"$1\"\n}\n\nfake_bin=\"$tmp_dir/fake-bin\"\nfake_docker_log=\"$tmp_dir/fake-docker.log\"\nmerged_fixture=\"$tmp_dir/merged-compose.json\"\ninspect_fixture=\"$tmp_dir/inspect.json\"\nmkdir -p \"$fake_bin\"\ncat >\"$fake_bin/docker\" <<'EOF_FAKE_DOCKER'\n#!/usr/bin/env bash\nset -euo pipefail\nprintf '%s\\n' \"$*\" >>\"$FAKE_DOCKER_LOG\"\nif [[ \"${1:-}\" == compose ]]; then\n if [[ \" $* \" == *' config --format json '* ]]; then\n cat \"$FAKE_DOCKER_MERGED_JSON\"\n fi\n exit 0\nfi\nif [[ \"${1:-}\" == inspect && \"${2:-}\" == -f ]]; then\n printf '%s\\n' healthy\n exit 0\nfi\nif [[ \"${1:-}\" == inspect ]]; then\n cat \"$FAKE_DOCKER_INSPECT_JSON\"\n exit 0\nfi\nexit 2\nEOF_FAKE_DOCKER\nchmod 0755 \"$fake_bin/docker\"\n\ncat >\"$merged_fixture\" <<EOF_MERGED_COMPOSE\n{\n \"name\": \"mckay\",\n \"networks\": {\n \"mckay-edge\": {\"name\":\"mckay-edge\",\"ipam\":{},\"external\":true}\n },\n \"services\": {\n \"spacebot\": {\n \"cpus\": 2,\n \"command\": [\"spacebot\",\"start\",\"--foreground\"],\n \"container_name\": \"mckay-spacebot\",\n \"entrypoint\": [\"/usr/local/bin/mckay-spacebot-entrypoint\"],\n \"environment\": {\n \"SPACEBOT_CRON_TIMEZONE\": \"Africa/Nairobi\",\n \"SPACEBOT_DENY_BIN_DIR\": \"/opt/mckay/deny-bin\",\n \"SPACEBOT_DEPLOYMENT\": \"docker\",\n \"SPACEBOT_DIR\": \"/data\",\n \"SPACEBOT_SANDBOX_REQUIRED\": \"true\",\n \"SPACEBOT_USER_TIMEZONE\": \"Africa/Nairobi\"\n },\n \"healthcheck\": {\n \"test\": [\"CMD\",\"curl\",\"-f\",\"http://localhost:19898/api/health\"],\n \"timeout\": \"5s\",\n \"interval\": \"30s\",\n \"retries\": 5,\n \"start_period\": \"1m0s\"\n },\n \"image\": \"ghcr.io/spacedriveapp/spacebot:v0.5.0\",\n \"init\": true,\n \"labels\": {\n \"traefik.docker.network\": \"mckay-edge\",\n \"traefik.enable\": \"true\",\n \"traefik.http.routers.spacebot.entrypoints\": \"web\",\n \"traefik.http.routers.spacebot.rule\": \"Host(\\`mckay.example.com\\`)\",\n \"traefik.http.services.spacebot.loadbalancer.server.port\": \"19898\"\n },\n \"logging\": {\"driver\":\"json-file\",\"options\":{\"max-file\":\"5\",\"max-size\":\"10m\"}},\n \"mem_limit\": \"2147483648\",\n \"networks\": {\"mckay-edge\":{\"ipv4_address\":\"172.30.0.10\"}},\n \"pids_limit\": 512,\n \"restart\": \"unless-stopped\",\n \"security_opt\": [\"no-new-privileges:true\"],\n \"stop_grace_period\": \"45s\",\n \"volumes\": [\n {\"type\":\"bind\",\"source\":\"$SPACEBOT_DATA_DIR\",\"target\":\"/data\",\"bind\":{}},\n {\"type\":\"bind\",\"source\":\"$SPACEBOT_CONFIG_SOURCE\",\"target\":\"/data/config.toml\",\"bind\":{}},\n {\"type\":\"bind\",\"source\":\"$SPACEBOT_ROOT_DIR/entrypoint.sh\",\"target\":\"/usr/local/bin/mckay-spacebot-entrypoint\",\"read_only\":true,\"bind\":{}},\n {\"type\":\"bind\",\"source\":\"$SPACEBOT_ROOT_DIR/deny-bin\",\"target\":\"/opt/mckay/deny-bin\",\"read_only\":true,\"bind\":{}}\n ]\n }\n }\n}\nEOF_MERGED_COMPOSE\n\nwrite_inspect_fixture \"$inspect_fixture\" true\n\nexport FAKE_DOCKER_LOG=\"$fake_docker_log\"\nexport FAKE_DOCKER_MERGED_JSON=\"$merged_fixture\"\nexport FAKE_DOCKER_INSPECT_JSON=\"$inspect_fixture\"\nPATH=\"$fake_bin:$PATH\"\n\n: >\"$fake_docker_log\"\nspacebot_onboarding_open_window || fail 'maintenance window did not open'\ndefault_override=\"$SPACEBOT_ONBOARDING_RUN_DIR/provider-onboarding.override.yml\"\nconfig_line=\"$(grep -nF \"compose -f $SPACEBOT_ROOT_DIR/docker-compose.yml -f $default_override config --format json\" \"$fake_docker_log\" | cut -d: -f1)\"\nup_line=\"$(grep -nF \"compose -f $SPACEBOT_ROOT_DIR/docker-compose.yml -f $default_override up -d --force-recreate spacebot\" \"$fake_docker_log\" | cut -d: -f1)\"\nhealth_line=\"$(grep -nF 'inspect -f {{.State.Health.Status}} mckay-spacebot' \"$fake_docker_log\" | cut -d: -f1)\"\ninspect_line=\"$(grep -nFx 'inspect mckay-spacebot' \"$fake_docker_log\" | cut -d: -f1)\"\n[[ -n \"$config_line\" && -n \"$up_line\" && -n \"$health_line\" && -n \"$inspect_line\" ]] || \\\n fail 'maintenance open command sequence is incomplete'\n((config_line < up_line && up_line < health_line && health_line < inspect_line)) || \\\n fail 'maintenance open command sequence is unsafe'\n\njq '.[0].Mounts |= map(if .Destination == \"/data/config.toml\" then .RW = false else . end)' \\\n \"$inspect_fixture\" >\"$inspect_fixture.tmp\"\nmv \"$inspect_fixture.tmp\" \"$inspect_fixture\"\n: >\"$fake_docker_log\"\nspacebot_onboarding_close_window || fail 'maintenance window did not close'\nbase_up=\"compose -f $SPACEBOT_ROOT_DIR/docker-compose.yml up -d --force-recreate spacebot\"\nbase_up_line=\"$(grep -nFx \"$base_up\" \"$fake_docker_log\" | cut -d: -f1)\"\nhealth_line=\"$(grep -nF 'inspect -f {{.State.Health.Status}} mckay-spacebot' \"$fake_docker_log\" | cut -d: -f1)\"\ninspect_line=\"$(grep -nFx 'inspect mckay-spacebot' \"$fake_docker_log\" | cut -d: -f1)\"\n[[ -n \"$base_up_line\" && -n \"$health_line\" && -n \"$inspect_line\" ]] || \\\n fail 'maintenance close command sequence is incomplete'\n((base_up_line < health_line && health_line < inspect_line)) || \\\n fail 'maintenance close command sequence is unsafe'\n! grep -Fq -- \"-f $default_override\" \"$fake_docker_log\" || \\\n fail 'maintenance close reused the writable override'\n[[ ! -e \"$default_override\" ]] || fail 'maintenance override remains after close'\n\njq '.services.spacebot.privileged = true' \"$merged_fixture\" >\"$merged_fixture.tmp\"\nmv \"$merged_fixture.tmp\" \"$merged_fixture\"\n: >\"$fake_docker_log\"\nif spacebot_onboarding_open_window 2>/dev/null; then\n fail 'unsafe merged Compose accepted'\nfi\n! grep -Fq ' up ' \"$fake_docker_log\" || \\\n fail 'unsafe merged Compose was recreated before rejection'\n\n# A validated override cannot be replaced between Compose rendering and use.\njq 'del(.services.spacebot.privileged)' \"$merged_fixture\" >\"$merged_fixture.tmp\"\nmv \"$merged_fixture.tmp\" \"$merged_fixture\"\ncat >\"$fake_bin/docker\" <<'EOF_REPLACING_DOCKER'\n#!/usr/bin/env bash\nset -euo pipefail\nprintf '%s\\n' \"$*\" >>\"$FAKE_DOCKER_LOG\"\nif [[ \"${1:-}\" == compose && \" $* \" == *' config --format json '* ]]; then\n cat \"$FAKE_DOCKER_MERGED_JSON\"\n rm -f \"$SPACEBOT_TEST_OVERRIDE\"\n printf 'services: {}\\n' >\"$SPACEBOT_TEST_OVERRIDE\"\n exit 0\nfi\nif [[ \"${1:-}\" == compose ]]; then\n exit 0\nfi\nexit 2\nEOF_REPLACING_DOCKER\nchmod 0755 \"$fake_bin/docker\"\n: >\"$fake_docker_log\"\nexport SPACEBOT_TEST_OVERRIDE=\"$default_override\"\nif spacebot_onboarding_open_window 2>/dev/null; then\n fail 'replaced maintenance override accepted'\nfi\n! grep -Fq ' up ' \"$fake_docker_log\" || \\\n fail 'replaced maintenance override reached recreation'\n\norchestration_trace=\"$tmp_dir/orchestration-trace\"\nexpected_success_order='lock\nrepair-read-only\npreflight\npre-backup\nopen-window\noperator-confirmation\nruntime-state\nmigrate-secrets\nreconcile-routing\nclose-window\nproviders\nverify-routing\npost-backup\nstrict-verify'\n\nrun_orchestration() (\n local fail_at=\"${1:-}\" providers_call=0\n : >\"$orchestration_trace\"\n record_step() { printf '%s\\n' \"$1\" >>\"$orchestration_trace\"; }\n fail_if_selected() { [[ \"$fail_at\" != \"$1\" ]]; }\n\n spacebot_onboarding_require_tty() { return 0; }\n spacebot_onboarding_acquire_lock() { record_step lock; }\n spacebot_onboarding_repair_read_only() {\n record_step repair-read-only\n }\n spacebot_onboarding_preflight() { record_step preflight; }\n spacebot_onboarding_take_pre_backup() { record_step pre-backup; }\n spacebot_onboarding_open_window() { record_step open-window; }\n spacebot_onboarding_operator_confirmation() {\n record_step operator-confirmation\n fail_if_selected operator-confirmation\n }\n spacebot_onboarding_require_runtime_state() {\n record_step runtime-state\n fail_if_selected runtime-state\n }\n spacebot_onboarding_require_providers() {\n providers_call=$((providers_call + 1))\n record_step providers\n fail_if_selected \"providers-${providers_call}\"\n }\n spacebot_onboarding_migrate_secrets() {\n record_step migrate-secrets\n fail_if_selected migrate-secrets\n }\n spacebot_onboarding_reconcile_routing() {\n record_step reconcile-routing\n fail_if_selected reconcile-routing\n }\n spacebot_onboarding_close_window() {\n record_step close-window\n fail_if_selected close-window\n }\n spacebot_onboarding_verify_routing() {\n record_step verify-routing\n fail_if_selected verify-routing\n }\n spacebot_onboarding_take_post_backup() {\n record_step post-backup\n fail_if_selected post-backup\n }\n spacebot_onboarding_strict_verify() {\n record_step strict-verify\n fail_if_selected strict-verify\n }\n spacebot_onboarding_restore_backup() { record_step automatic-restore; }\n\n spacebot_provider_onboarding_main\n)\n\nrun_orchestration >/dev/null 2>&1 || fail 'successful orchestration failed'\n[[ \"$(cat \"$orchestration_trace\")\" == \"$expected_success_order\" ]] || \\\n fail 'successful orchestration order mismatch'\n\nfor failure in \\\n operator-confirmation runtime-state migrate-secrets reconcile-routing \\\n close-window providers-1 verify-routing post-backup strict-verify; do\n set +e\n run_orchestration \"$failure\" >/dev/null 2>&1\n failure_status=$?\n set -e\n [[ \"$failure_status\" -ne 0 ]] || \\\n fail \"orchestration accepted ${failure} failure\"\n if [[ \"$failure\" == close-window ]]; then\n [[ \"$failure_status\" -eq 78 ]] || \\\n fail 'read-only restoration failure did not return status 78'\n else\n [[ \"$failure_status\" -eq 1 ]] || \\\n fail \"orchestration did not preserve ${failure} failure status\"\n fi\n grep -Fqx close-window \"$orchestration_trace\" || \\\n fail \"write window not closed after ${failure} failure\"\n ! grep -Fqx automatic-restore \"$orchestration_trace\" || \\\n fail \"backup automatically restored after ${failure} failure\"\ndone\n\ncleanup_trace=\"$tmp_dir/cleanup-trace\"\n: >\"$cleanup_trace\"\nspacebot_onboarding_close_window() { printf 'close\\n' >>\"$cleanup_trace\"; }\nSPACEBOT_ONBOARDING_WINDOW_OPEN=true\nset +e\nspacebot_onboarding_cleanup 143\ncleanup_status=$?\nset -e\n[[ \"$cleanup_status\" -eq 143 ]] || fail 'cleanup changed supplied status'\n[[ \"$SPACEBOT_ONBOARDING_WINDOW_OPEN\" == false ]] || \\\n fail 'cleanup left the write-window state open'\n[[ \"$(cat \"$cleanup_trace\")\" == close ]] || \\\n fail 'cleanup did not close the write window exactly once'\n\nbackup_root=\"$tmp_dir/backup-root\"\nbackup_archive=\"$tmp_dir/spacebot-backup.tar.gz\"\nmkdir -p \"$backup_root/scripts\"\nprintf 'verified backup fixture\\n' >\"$backup_archive\"\nwrite_sha256 \"$backup_archive\" \"${backup_archive}.sha256\"\ncat >\"$backup_root/scripts/backup_spacebot.sh\" <<'EOF_FAKE_BACKUP'\n#!/usr/bin/env bash\nprintf '[backup] completed\\n%s\\n' \"$TEST_BACKUP_ARCHIVE\"\nEOF_FAKE_BACKUP\nspacebot_repo_root() { printf '%s' \"$backup_root\"; }\nexport TEST_BACKUP_ARCHIVE=\"$backup_archive\"\nCONFIG_FILE=\"$tmp_dir/config.env\"\n[[ \"$(spacebot_onboarding_backup)\" == \"$backup_archive\" ]] || \\\n fail 'verified backup path was not returned'\nprintf 'tampered\\n' >>\"$backup_archive\"\nif spacebot_onboarding_backup >/dev/null 2>&1; then\n fail 'backup with an invalid checksum was accepted'\nfi\nTEST_BACKUP_ARCHIVE='relative-backup.tar.gz'\nif spacebot_onboarding_backup >/dev/null 2>&1; then\n fail 'relative backup path was accepted'\nfi\n\nstale_repair_trace=\"$tmp_dir/stale-repair-trace\"\n: >\"$stale_repair_trace\"\nspacebot_onboarding_config_mount_mode() { printf 'rw'; }\nspacebot_onboarding_close_window() { printf 'close\\n' >>\"$stale_repair_trace\"; }\nspacebot_onboarding_repair_read_only >/dev/null 2>&1 || \\\n fail 'stale writable state was not repaired'\n[[ \"$(cat \"$stale_repair_trace\")\" == close ]] || \\\n fail 'stale writable repair did not close the window exactly once'\n: >\"$stale_repair_trace\"\nspacebot_onboarding_config_mount_mode() { printf 'ro'; }\nspacebot_onboarding_repair_read_only || fail 'read-only state was rejected'\n[[ ! -s \"$stale_repair_trace\" ]] || \\\n fail 'read-only state triggered an unnecessary recreation'\n\nstale_failure_trace=\"$tmp_dir/stale-failure-trace\"\nset +e\n(\n SPACEBOT_ONBOARDING_WINDOW_OPEN=false\n spacebot_onboarding_require_tty() { return 0; }\n spacebot_onboarding_acquire_lock() { printf 'lock\\n' >>\"$stale_failure_trace\"; }\n spacebot_onboarding_config_mount_mode() { printf 'rw'; }\n spacebot_onboarding_close_window() {\n printf 'close\\n' >>\"$stale_failure_trace\"\n return 1\n }\n spacebot_provider_onboarding_main\n) >/dev/null 2>&1\nstale_failure_status=$?\nset -e\n[[ \"$stale_failure_status\" -eq 78 ]] || \\\n fail 'unproved stale-window restoration did not return status 78'\n[[ \"$(grep -c '^close$' \"$stale_failure_trace\")\" -eq 2 ]] || \\\n fail 'failed stale-window repair was not retried by EXIT cleanup'\n\nsignal_fixture=\"$tmp_dir/onboarding-signal-fixture.sh\"\ncat >\"$signal_fixture\" <<'EOF_SIGNAL_FIXTURE'\n#!/usr/bin/env bash\nset -euo pipefail\n\nsource \"$TEST_ROOT/scripts/lib/common.sh\"\nsource \"$TEST_ROOT/scripts/lib/config.sh\"\nsource \"$TEST_ROOT/scripts/lib/spacebot.sh\"\nsource \"$TEST_ROOT/scripts/lib/spacebot_routing.sh\"\nsource \"$TEST_ROOT/scripts/lib/spacebot_provider_onboarding.sh\"\n\nfixture_mode=\"$1\"\nsignal_name=\"$2\"\ntrace=\"$3\"\nclose_calls=0\nSPACEBOT_ONBOARDING_WINDOW_OPEN=false\n: >\"$trace\"\nrecord_signal_step() { printf '%s\\n' \"$1\" >>\"$trace\"; }\nspacebot_onboarding_require_tty() { return 0; }\nspacebot_onboarding_acquire_lock() { record_signal_step lock; }\nspacebot_onboarding_close_window() {\n close_calls=$((close_calls + 1))\n if [[ \"$fixture_mode\" == stale && \"$close_calls\" -eq 1 ]]; then\n record_signal_step repair-close\n kill -s \"$signal_name\" \"$$\"\n return 99\n fi\n record_signal_step close-window\n}\n\nif [[ \"$fixture_mode\" == stale ]]; then\n spacebot_onboarding_config_mount_mode() {\n record_signal_step mount-rw\n printf 'rw'\n }\nelse\n spacebot_onboarding_repair_read_only() { record_signal_step repair-read-only; }\n spacebot_onboarding_preflight() { record_signal_step preflight; }\n spacebot_onboarding_take_pre_backup() { record_signal_step pre-backup; }\n spacebot_onboarding_open_window() { record_signal_step open-window; }\n spacebot_onboarding_operator_confirmation() {\n record_signal_step \"signal-${signal_name}\"\n kill -s \"$signal_name\" \"$$\"\n return 99\n }\nfi\n\nspacebot_provider_onboarding_main\nEOF_SIGNAL_FIXTURE\nchmod 0755 \"$signal_fixture\"\n\nstale_interrupt_trace=\"$tmp_dir/stale-interrupt-trace\"\nset +e\nTEST_ROOT=\"$ROOT_DIR\" bash \"$signal_fixture\" stale TERM \\\n \"$stale_interrupt_trace\" >\"$tmp_dir/stale-signal-output\" 2>&1\nstale_interrupt_status=$?\nset -e\n[[ \"$stale_interrupt_status\" -eq 143 ]] || \\\n fail \"TERM during stale repair returned ${stale_interrupt_status}, expected 143: output=$(tr '\\n' ' ' <\"$tmp_dir/stale-signal-output\"); trace=$(tr '\\n' ',' <\"$stale_interrupt_trace\")\"\nexpected_stale_interrupt='lock\nmount-rw\nrepair-close\nclose-window'\n[[ \"$(cat \"$stale_interrupt_trace\")\" == \"$expected_stale_interrupt\" ]] || \\\n fail 'TERM during stale repair did not retry closure in order'\n\nsignal_trace=\"$tmp_dir/signal-trace\"\nfor signal_case in 'INT 130' 'HUP 129'; do\n read -r signal_name expected_signal_status <<<\"$signal_case\"\n set +e\n TEST_ROOT=\"$ROOT_DIR\" bash \"$signal_fixture\" open \"$signal_name\" \\\n \"$signal_trace\" >/dev/null 2>&1\n signal_status=$?\n set -e\n [[ \"$signal_status\" -eq \"$expected_signal_status\" ]] || \\\n fail \"${signal_name} trap returned ${signal_status}, expected ${expected_signal_status}\"\n expected_signal_trace=\"lock\nrepair-read-only\npreflight\npre-backup\nopen-window\nsignal-${signal_name}\nclose-window\"\n [[ \"$(cat \"$signal_trace\")\" == \"$expected_signal_trace\" ]] || \\\n fail \"${signal_name} trap did not close the write window in order\"\ndone\n\nwrapper_trace_marker='fixture-wrapper-config-xtrace-marker'\nwrapper_trace_config=\"$tmp_dir/wrapper-trace.env\"\nwrapper_trace_log=\"$tmp_dir/wrapper-config-xtrace\"\ncat >\"$wrapper_trace_config\" <<EOF_WRAPPER_TRACE\nDEPLOYMENT_ENV=production\nSPACEBOT_ENABLE=true\nSYNTHETIC_SECRET=${wrapper_trace_marker}\nEOF_WRAPPER_TRACE\nif ! (\n set_default_config() { :; }\n validate_config() { :; }\n spacebot_provider_onboarding_main() { :; }\n {\n set -x\n spacebot_provider_onboarding_run \"$wrapper_trace_config\"\n wrapper_trace_state=\"$-\"\n } 2>\"$wrapper_trace_log\"\n [[ \"$wrapper_trace_state\" != *x* ]]\n); then\n fail 'wrapper config loader failed to suppress inherited xtrace'\nfi\n! grep -Fq \"$wrapper_trace_marker\" \"$wrapper_trace_log\" || \\\n fail 'wrapper config value leaked through xtrace'\n\nwrapper=\"$ROOT_DIR/scripts/spacebot_provider_onboarding.sh\"\nwrapper_log=\"$tmp_dir/wrapper-mutations\"\nwrapper_output=\"$tmp_dir/wrapper-output\"\n: >\"$wrapper_log\"\ncat >\"$fake_bin/sudo\" <<'EOF_FAKE_SUDO'\n#!/usr/bin/env bash\nprintf 'sudo\\n' >>\"$WRAPPER_MUTATION_LOG\"\nexit 99\nEOF_FAKE_SUDO\nchmod 0755 \"$fake_bin/sudo\"\nexport WRAPPER_MUTATION_LOG=\"$wrapper_log\"\nif PATH=\"$fake_bin:$PATH\" bash \"$wrapper\" --config \"$tmp_dir/config.env\" \\\n </dev/null >\"$wrapper_output\" 2>&1; then\n fail 'non-TTY provider onboarding accepted'\nfi\ngrep -Fq 'interactive terminal' \"$wrapper_output\" || \\\n fail 'non-TTY provider onboarding refusal is unclear'\n[[ ! -s \"$wrapper_log\" ]] || \\\n fail 'non-TTY provider onboarding reached sudo or Docker mutation'\n\nprintf 'PASS: test_spacebot_provider_onboarding.sh\\n'\n"}}}} |