#!/usr/bin/env bash # # scripts/setup.sh --deploy flag + scripts/next_steps.sh --manifest / # --recipe flags. Runs hermetically against a sandboxed $HOME with # stubbed scripts and a stubbed orchestrator checkout carrying both # contributor_setup_steps.json and operator_setup_steps.json. # # Operator welcome scaffold dispatch contract: # - `just setup` (no --deploy) ALWAYS uses operator_setup_steps.json # and operator-setup. It STOPS after the clone (prints the plan as # informational text; does NOT prompt; does NOT invoke any recipe). # Operators who want the handoff use `just deploy`. # - `just setup --deploy` (== `just deploy`) ALWAYS uses operator_* # and prompts [Y/n] (default Y) before auto-handing off to # `just operator-setup` inside the orchestrator checkout. # - The contributor manifest never appears in the operator scaffold's # dispatch surface; the bug fixed here was setup.sh defaulting to # contributor_setup_steps.json when --deploy was absent. set -euo pipefail here="$(cd "$(dirname "$0")" && pwd -P)" root="$here/.." pass=0 fail=0 assert() { local msg="$1"; shift if "$@"; then printf '[ok] %s\n' "$msg" pass=$((pass + 1)) else printf '[FAIL] %s\n' "$msg" fail=$((fail + 1)) fi } # -- syntax --------------------------------------------------------------- assert 'setup.sh parses as valid bash' bash -n "$root/scripts/setup.sh" assert 'next_steps.sh parses as valid bash' bash -n "$root/scripts/next_steps.sh" # -- --help documents --deploy and --manifest/--recipe -------------------- # NOTE: capture help output to a file; inlining it into `bash -c` would # expose backticks in the text (e.g. `just operator-setup`) to command # substitution. Use files or here-strings instead. setup_help_file="$(mktemp)" bash "$root/scripts/setup.sh" --help >"$setup_help_file" 2>&1 assert 'setup --help documents --deploy' \ grep -q -- '--deploy' "$setup_help_file" assert 'setup --help mentions operator-setup' \ grep -q 'operator-setup' "$setup_help_file" assert 'setup --help mentions operator_setup_steps.json' \ grep -q 'operator_setup_steps.json' "$setup_help_file" rm -f "$setup_help_file" next_help_file="$(mktemp)" bash "$root/scripts/next_steps.sh" --help >"$next_help_file" 2>&1 assert 'next_steps --help documents --manifest' \ grep -q -- '--manifest' "$next_help_file" assert 'next_steps --help documents --recipe' \ grep -q -- '--recipe' "$next_help_file" rm -f "$next_help_file" # -- sandbox -------------------------------------------------------------- tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT mkdir -p "$tmp/scripts" "$tmp/home" "$tmp/tokens" "$tmp/workspace" touch "$tmp/Justfile" cp "$root/scripts/setup.sh" "$tmp/scripts/setup.sh" cp "$root/scripts/next_steps.sh" "$tmp/scripts/next_steps.sh" cp "$root/scripts/common.sh" "$tmp/scripts/common.sh" cat >"$tmp/.env.example" <<'EOF' FORGE_GITEA_URL="http://127.0.0.1:1" FORGE_GITEA_ORG="x" FORGE_GITEA_USERNAME="sandbox-user" FORGE_ORCHESTRATOR_REPO_URL="http://127.0.0.1:1/x/y.git" FORGE_WORKSPACE_ROOT="./workspace" FSDGG_CLI_CLIENT_ID="sandbox-client" FSDGG_CLI_REDIRECT_URI="http://127.0.0.1:38111/callback" EOF cp "$tmp/.env.example" "$tmp/.env" # stub doctor/login/helper for name in doctor.sh forge_login.sh install-git-credential-helper.sh; do cat >"$tmp/scripts/$name" <"$tmp/scripts/forge_auth.py" <<'EOF' #!/usr/bin/env python3 import sys sys.exit(0) EOF chmod +x "$tmp/scripts/forge_auth.py" # curl stub mkdir -p "$tmp/fakebin" cat >"$tmp/fakebin/curl" <<'EOF' #!/usr/bin/env bash for arg in "$@"; do case "$arg" in *"/api/v1/version") echo '{"version":"0.0.0-stub"}' exit 0;; esac done exec /usr/bin/curl "$@" EOF chmod +x "$tmp/fakebin/curl" # git stub: ls-remote OK; clone → seed the fake orchestrator with both manifests cat >"$tmp/fakebin/git" <"\$dest/scripts/contributor_setup_steps.json" <<'JSON' {"schema_version":1,"steps":[{"id":"noop","title":"Contributor noop","cmd":["true"]}]} JSON # Operator manifest (plain echo of one step) cat >"\$dest/scripts/operator_setup_steps.json" <<'JSON' {"schema_version":1,"steps":[{"id":"op-step","title":"Operator noop","cmd":["true"]}]} JSON # Fake Justfile exposing both recipes. # The test records invocations to verify the selected recipe. cat >"\$dest/Justfile" <<'JUST' contributor-setup *args: @echo "[stub-orchestrator] called: contributor-setup \$@" >>.invocations operator-setup *args: @echo "[stub-orchestrator] called: operator-setup \$@" >>.invocations JUST exit 0;; esac exec /usr/bin/git "\$@" EOF chmod +x "$tmp/fakebin/git" # setup.sh deploy path calls `exec bash next_steps.sh --run --manifest ... --recipe ...`. # That path reaches `exec just `. The stub keeps the test # deterministic and avoids a dependency on the sandboxed orchestrator. cat >"$tmp/fakebin/just" <>"$tmp/just.calls" exit 0 EOF chmod +x "$tmp/fakebin/just" export HOME="$tmp/home" export PATH="$tmp/fakebin:/usr/bin:/bin" export FSDGG_AUTH_STORE_PATH="$tmp/tokens/client-auth.json" export FORGE_SETUP_YES=1 export FORGE_GITEA_URL="http://127.0.0.1:1" export FORGE_GITEA_ORG="x" export FORGE_GITEA_USERNAME="sandbox-user" export FORGE_ORCHESTRATOR_REPO_URL="http://127.0.0.1:1/x/y.git" export FORGE_ORCHESTRATOR_BRANCH="" export FORGE_WORKSPACE_ROOT="$tmp/workspace" export FSDGG_CLI_CLIENT_ID="sandbox-client" export FSDGG_CLI_REDIRECT_URI="http://127.0.0.1:38111/callback" # Live stored token so setup.sh skips the login step (FORGE_SETUP_YES+headless # would otherwise trip the guard). mkdir -p "$tmp/tokens" cat >"$tmp/tokens/client-auth.json" <<'JSON' {"username":"sandbox-user","gitea_access_token":"live","_forge_refresh_token":"r", "gitea_token_expires_at":32503680000} JSON chmod 0600 "$tmp/tokens/client-auth.json" # -- A) default path: operator manifest, no auto-run -------------------- # Without --deploy the operator scaffold ALWAYS uses the operator # manifest and STOPS after the clone. The contributor manifest must # never be referenced; no recipe must be invoked. rm -f "$tmp/just.calls" set +e FORGE_SETUP_YES=1 bash "$tmp/scripts/setup.sh" --headless \ >"$tmp/out_default.out" 2>"$tmp/out_default.err" rc=$? set -e assert 'default (no --deploy) exits 0' bash -c "[ $rc -eq 0 ]" assert 'default path renders operator_setup_steps.json' \ grep -qF 'operator_setup_steps.json' "$tmp/out_default.err" assert 'default path mentions just operator-setup in the handoff hint' \ grep -qF 'just operator-setup' "$tmp/out_default.err" assert 'default path does NOT reference contributor_setup_steps.json' \ bash -c "! grep -qF 'contributor_setup_steps.json' \"$tmp/out_default.err\"" assert 'default path does NOT mention contributor-setup' \ bash -c "! grep -qF 'contributor-setup' \"$tmp/out_default.err\"" assert 'default path does NOT prompt for an operator-setup handoff' \ bash -c "! grep -qF 'Hand off to' \"$tmp/out_default.err\"" assert 'default path does NOT invoke any just recipe (no auto-run)' \ bash -c "! [ -f \"$tmp/just.calls\" ]" # -- B) --deploy path: operator manifest, default-Y prompt, hands off --- # Under FORGE_SETUP_YES=1 the [Y/n] prompt's default Y is taken, so the # stubbed `just operator-setup` is invoked inside the orchestrator cwd. rm -f "$tmp/just.calls" set +e FORGE_SETUP_YES=1 bash "$tmp/scripts/setup.sh" --headless --deploy \ >"$tmp/out_deploy.out" 2>"$tmp/out_deploy.err" rc=$? set -e assert '--deploy exits 0' bash -c "[ $rc -eq 0 ]" assert '--deploy renders operator_setup_steps.json' \ grep -qF 'operator_setup_steps.json' "$tmp/out_deploy.err" assert '--deploy prompt names operator-setup' \ grep -qF 'operator-setup' "$tmp/out_deploy.err" assert '--deploy prompt is [Y/n] (default Y, handoff is the default)' \ grep -qE 'Hand off to.*\[Y/n\]' "$tmp/out_deploy.err" assert '--deploy invokes just operator-setup (FORGE_SETUP_YES=1 takes the default Y)' \ bash -c "[ -f \"$tmp/just.calls\" ] && grep -qE '^just operator-setup' \"$tmp/just.calls\"" assert '--deploy does NOT invoke just contributor-setup' \ bash -c "! { [ -f \"$tmp/just.calls\" ] && grep -qE '^just contributor-setup' \"$tmp/just.calls\"; }" assert '--deploy hands off in the cloned orchestrator cwd (basename of FORGE_ORCHESTRATOR_REPO_URL)' \ grep -qF "(cwd=$tmp/workspace/y)" "$tmp/just.calls" # -- C) run-mode wiring: next_steps.sh --run --manifest --recipe ----- # Invoke next_steps.sh directly with --run to prove the runner execs # `just operator-setup` (not `just contributor-setup`) when --recipe is set. rm -f "$tmp/just.calls" set +e bash "$tmp/scripts/next_steps.sh" --run --manifest operator_setup_steps.json --recipe operator-setup \ >"$tmp/out_run_deploy.out" 2>"$tmp/out_run_deploy.err" rc=$? set -e assert 'next_steps --run --recipe operator-setup exits 0' bash -c "[ $rc -eq 0 ]" assert 'next_steps --run invoked `just operator-setup`' \ grep -q '^just operator-setup' "$tmp/just.calls" assert 'next_steps --run did NOT invoke `just contributor-setup`' \ bash -c "! grep -q '^just contributor-setup' \"$tmp/just.calls\"" # -- D) print mode shows updated skip-autorun hints --------------------- set +e bash "$tmp/scripts/next_steps.sh" --manifest operator_setup_steps.json --recipe operator-setup \ >"$tmp/out_print.out" 2>"$tmp/out_print.err" rc=$? set -e assert 'next_steps print mode exits 0' bash -c "[ $rc -eq 0 ]" assert 'print hint mentions operator-setup recipe' \ grep -q "just operator-setup" "$tmp/out_print.err" assert 'print hint mentions --recipe flag' \ grep -q "run-next-steps --manifest operator_setup_steps.json --recipe operator-setup" "$tmp/out_print.err" printf '\n%d pass / %d fail\n' "$pass" "$fail" [ "$fail" -eq 0 ]