Files
molecule-core/tests/e2e/_lib.sh
core-devops 467c10526b
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 4s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 2s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 4s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 10s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 16s
E2E Chat / detect-changes (pull_request) Successful in 15s
E2E API Smoke Test / detect-changes (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
qa-review / approved (pull_request_target) Failing after 5s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-tier-check / tier-check (pull_request_target) Failing after 3s
gate-check-v3 / gate-check (pull_request_target) Successful in 11s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
CI / Canvas (Next.js) (pull_request) Successful in 1s
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request_target) Failing after 11s
CI / Platform (Go) (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 5s
sop-checklist / all-items-acked (pull_request_target) Successful in 10s
CI / Canvas Deploy Status (pull_request) Has been skipped
E2E Chat / E2E Chat (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 11s
CI / all-required (pull_request) Successful in 7s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 57s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m0s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Failing after 1m5s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m13s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m12s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m33s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m2s
qa-review / approved (pull_request_review) Has been skipped
security-review / approved (pull_request_review) Has been skipped
sop-tier-check / tier-check (pull_request_review) Failing after 4s
audit-force-merge / audit (pull_request_target) Successful in 5s
e2e-api: send admin bearer on ALL admin-gated calls so the whole suite passes
Setting ADMIN_TOKEN on the e2e platform (head 8fb5dbed, needed so the mock arm
can org-import + mint tokens under REQUIRE_LIVE) flips isDevModeFailOpen() to
false (devmode.go:50), so EVERY AdminAuth-gated route now requires the exact
ADMIN_TOKEN as bearer — Tier-2b (wsauth_middleware.go:250) rejects workspace
bearers on admin routes. The other E2E API Smoke scripts sent no admin auth and
went 401 ("admin auth required"), reddening the job (test_api.sh's
GET /workspaces + POST /workspaces were the confirmed failers).

Fix: route every admin-gated call through the platform admin bearer
(MOLECULE_ADMIN_TOKEN, guarded if-set so fail-open dev still works), determined
against the router (workspace-server/internal/router/router.go):

- _lib.sh: new e2e_admin_auth_args helper; e2e_cleanup_all_workspaces (GET
  /workspaces) and e2e_delete_workspace's default path (DELETE /workspaces/:id)
  now inject the admin bearer when the caller passes no per-call auth. Fixes the
  cleanup-trap admin calls across poll-mode/notify/priority at once.
- test_api.sh: acurl now sends the platform admin bearer (was a workspace token,
  which Tier-2b rejects); admin routes (list/create/delete /workspaces, /events,
  /bundles export+import) go through acurl; WorkspaceAuth routes (PATCH
  /workspaces/:id, /activity) use the workspace's own token. Removed the
  ADMIN_TOKEN="" reset (platform-level ADMIN_TOKEN stays set → no fail-open).
- test_notify_attachments_e2e.sh: admin bearer on the pre-sweep GET /workspaces
  and the POST /workspaces create.
- test_priority_runtimes_e2e.sh: admin bearer on the pre-sweep GET /workspaces
  and every runtime POST /workspaces create (claude-code/hermes/openclaw/codex/
  minimax). run_mock's /org/import auth (8fb5dbed) unchanged.

Workspace-scoped routes (per-workspace Bearer, already authed) and the public
GET /workspaces/:id (router.go:155, no middleware) are left as-is.

Net effect: the entire E2E API Smoke suite runs WITH admin auth (more correct —
dev-mode-fail-open was a security shortcut) AND the mock validates end-to-end →
honest REQUIRE_LIVE gate.

Verified locally against PG+Redis+platform-server with ADMIN_TOKEN set (the CI
shape, dev-mode-fail-open=false): test_api.sh 61/0 pass; test_today_pr_coverage
8/0; test_notify_attachments 14/0; test_priority_runtimes 3/0 + "1 runtime
validated end-to-end" (mock); test_poll_mode_chat_upload 24/0. test_poll_mode's
Phase-3.5 ImportError is a pre-existing missing-pip-package gap (identical on the
unmodified _lib.sh; CI installs the parser before that step) — not auth-related.
bash -n + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:33:49 -07:00

122 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Common E2E helpers. Source this from every tests/e2e/*.sh.
#
# Usage:
# source "$(dirname "$0")/_lib.sh"
# e2e_cleanup_all_workspaces # call at top of script
# TOKEN=$(echo "$register_response" | e2e_extract_token)
#
# BASE defaults to http://localhost:8080. Set it before sourcing to override.
: "${BASE:=http://localhost:8080}"
export BASE
# Emit the auth_token from a /registry/register response on stdout.
# See _extract_token.py for the exact semantics.
e2e_extract_token() {
python3 "$(dirname "${BASH_SOURCE[0]}")/_extract_token.py"
}
# Populate a curl-args array with the platform admin bearer, IF one is set.
#
# AdminAuth (workspace-server/internal/middleware/wsauth_middleware.go:161)
# fail-opens ONLY while ADMIN_TOKEN is unset AND no workspace token exists yet
# (devmode.go:50). The e2e-api CI job now sets ADMIN_TOKEN on the platform and
# exports the matching MOLECULE_ADMIN_TOKEN here, which flips fail-open OFF — so
# every admin-gated route (GET/POST/DELETE /workspaces, /events, /bundles,
# /org/import, …) now requires the EXACT ADMIN_TOKEN as bearer (Tier-2b rejects
# workspace bearers, wsauth_middleware.go:250). Helpers that hit admin routes
# (e2e_cleanup_all_workspaces, e2e_delete_workspace's default path) must send it.
#
# Guarded if-set so a bootstrap/dev platform with no admin token (fail-open)
# still works with zero auth. Mirrors e2e_mint_workspace_token's admin_auth.
#
# Usage:
# local admin_auth=(); e2e_admin_auth_args admin_auth
# curl -s "$BASE/workspaces" ${admin_auth[@]+"${admin_auth[@]}"}
e2e_admin_auth_args() {
local _outname="$1"
local _bearer="${MOLECULE_ADMIN_TOKEN:-${ADMIN_TOKEN:-}}"
if [ -n "$_bearer" ]; then
eval "$_outname=(-H \"Authorization: Bearer \$_bearer\")"
else
eval "$_outname=()"
fi
}
# Delete every workspace currently on the platform. Use at the top of a
# script so count-based assertions are reproducible across runs.
# Mint a fresh workspace auth token via the real admin endpoint.
#
# Usage:
# TOKEN=$(e2e_mint_workspace_token "$workspace_id") || exit 1
e2e_mint_workspace_token() {
local wid="$1"
if [ -z "$wid" ]; then
echo "e2e_mint_workspace_token: workspace id required" >&2
return 2
fi
local body
local admin_bearer="${MOLECULE_ADMIN_TOKEN:-${ADMIN_TOKEN:-}}"
local admin_auth=()
[ -n "$admin_bearer" ] && admin_auth=(-H "Authorization: Bearer $admin_bearer")
body=$(curl -s -X POST -w "\n%{http_code}" "$BASE/admin/workspaces/$wid/tokens" ${admin_auth[@]+"${admin_auth[@]}"})
local code
code=$(printf '%s' "$body" | tail -n1)
local json
json=$(printf '%s' "$body" | sed '$d')
if [ "$code" != "201" ]; then
echo "e2e_mint_workspace_token: got HTTP $code from POST /admin/workspaces/:id/tokens" >&2
return 1
fi
printf '%s' "$json" | python3 -c "import json,sys; print(json.load(sys.stdin)['auth_token'])"
}
e2e_delete_workspace() {
local wid="$1"
local name="${2:-}"
shift 2 || true
local curl_args=("$@")
if [ -z "$wid" ]; then
return 0
fi
# DELETE /workspaces/:id and GET /workspaces/:id-for-name are both behind
# AdminAuth (router.go:155 GET single is public, but List/Delete are gated at
# router.go:165-167). Callers that already pass a per-workspace bearer (e.g.
# test_api.sh's NEW_TOKEN) authenticate themselves; the cleanup-trap callers
# in poll-mode/notify/priority pass NO curl args and rely on this fallback to
# the platform admin bearer so the DELETE doesn't 401 once ADMIN_TOKEN is set.
if [ "${#curl_args[@]}" -eq 0 ]; then
e2e_admin_auth_args curl_args
fi
# ${curl_args[@]+"…"} guard: under `set -u` an empty array expands to an
# "unbound variable" error on bash <4.4 (macOS 3.2, some Linux). This form
# expands to nothing when the array is empty. Callers from the priority-
# runtimes EXIT trap pass no extra curl args, so the array IS empty there —
# without the guard the trap aborts non-zero AFTER the gate already passed,
# turning a validated run RED. (Same idiom already used for CREATED_WSIDS.)
if [ -z "$name" ]; then
name=$(curl -s "$BASE/workspaces/$wid" ${curl_args[@]+"${curl_args[@]}"} | python3 -c "import json,sys
try:
print(json.load(sys.stdin).get('name',''))
except Exception:
pass" 2>/dev/null || true)
fi
curl -s -X DELETE "$BASE/workspaces/$wid?confirm=true" \
-H "X-Confirm-Name: $name" ${curl_args[@]+"${curl_args[@]}"} > /dev/null || true
}
e2e_cleanup_all_workspaces() {
# GET /workspaces (list) is AdminAuth-gated (router.go:165). Send the platform
# admin bearer if one is set so the list doesn't 401 → empty → no cleanup.
local _admin_auth=()
e2e_admin_auth_args _admin_auth
curl -s "$BASE/workspaces" ${_admin_auth[@]+"${_admin_auth[@]}"} | python3 -c "import json,sys
try:
[print(f\"{w.get('id','')}\\t{w.get('name','')}\") for w in json.load(sys.stdin)]
except Exception:
pass" 2>/dev/null | while IFS=$'\t' read -r _wid _name; do
e2e_delete_workspace "$_wid" "$_name"
done
}