Stabilize auth flow and profile images

This commit is contained in:
Codex
2026-07-20 13:38:39 +09:00
parent 57caca8dc8
commit 5d3eee7a16
128 changed files with 28860 additions and 1468 deletions
+49
View File
@@ -0,0 +1,49 @@
# scripts 폴더 안내
이 폴더는 `tdc114plus` 앱 초기 설정, Flutter 실행 보조, 테스트 자동화용 shell script를 모아둔 위치이다.
## 파일별 용도
- `api-smoke.sh`: `TDC114_API_BASE` 대상 Baron SSO 연동 API에 최소 smoke test를 수행한다. `TDC114_SMOKE_PHONE`이 있으면 `TDC114_SMOKE_AUTH_FLOW`에 따라 legacy `phone-login` 호환 경로 또는 headless 링크 흐름을 확인한다.
- `smoke.env.example`: authenticated smoke용 로컬 env 예시 파일이다. `scripts/.env.smoke.local`로 복사해 `TDC114_API_BASE`, `TDC114_SMOKE_PHONE`을 넣어 사용할 수 있다.
- `smoke.staging.env.example`: staging Baron SSO 검증용 env 예시 파일이다. `scripts/.env.staging.local`로 복사해 `TDC114_API_BASE`, `TDC114_SMOKE_PHONE`과 optional expected label 값을 넣어 사용할 수 있다.
- `android-device.env.example`: Android 공기계 USB 테스트용 env 예시 파일이다. `scripts/.env.android-device.local`로 복사해 `adb reverse` 기준 `TDC114_API_BASE=http://127.0.0.1:5000`, `TDC114_AUTH_API_BASE=http://127.0.0.1:5001`을 사용한다.
- `android-device.staging.env.example`: Android 공기계/실폰의 USB 없는 staging 직접 로그인 테스트용 env 예시 파일이다. `scripts/.env.android-device.staging.local`로 복사해 공개 staging HTTPS base URL을 넣어 사용한다.
- `android-device.production.env.example`: Android 공기계/실폰의 USB 없는 production 직접 로그인 테스트용 env 예시 파일이다. `scripts/.env.android-device.production.local`로 복사해 공개 production HTTPS base URL을 넣어 사용한다.
- `bootstrap-baron-api-env.sh`: Baron SSO API worktree의 `.env.sample`을 바탕으로 로컬 smoke용 `.env`를 생성하고 localhost/알림 비활성 override를 덧붙인다.
- `check-baron-api-env.sh`: Baron SSO API worktree의 `.env`, compose, config, Docker runtime 준비 상태를 점검해 실제 API smoke 가능 여부를 빠르게 확인한다.
- `check-android-device-env.sh`: Android 공기계 USB 연결, Windows ADB server 공유, 선택적 `adb reverse` 상태를 점검한다.
- `host-adb.sh`: Docker를 거치지 않고 WSL 호스트의 Linux adb로 Windows ADB server에 접속한다. ADB 로그/스크린샷/reverse 조회처럼 Flutter가 필요 없는 작업에 사용한다.
- `install-host-adb-from-docker.sh`: Docker Flutter 이미지에서 Linux adb 바이너리를 `.tools/android-platform-tools/adb`로 1회 추출한다. 이후 ADB 조회 작업은 Docker 권한 없이 `host-adb.sh`로 처리한다.
- `startup.sh`: Baron SSO runtime 기동, 상태 확인, API smoke 재시도를 자동화한다. `check-baron-api-env.sh`에 warning이 남거나 `api-smoke.sh`가 실패하면 비정상 종료한다.
- `shutdown.sh`: 대상 helper 프로세스 종료, compose 로그 수집, Baron SSO runtime 중지, 선택적 컨테이너 정리와 권한 복구를 자동화한다.
- `bootstrap.sh`: 로컬 Flutter SDK가 설치된 환경에서 `app/` 프로젝트의 `flutter pub get`을 실행한다.
- `create-flutter-app.sh`: `app/` Flutter 프로젝트가 없을 때 Android/iOS 기반 앱 골격을 생성한다.
- `flutter-docker.sh`: Docker Flutter 이미지를 이용해 `app/` 디렉터리에서 Flutter 명령을 실행한다. `.android-adb/`, `.docker-cache/flutter/` 아래 ADB key, Gradle, pub, Android SDK 일부 cache를 재사용한다.
- `manual-postlogin-run.sh`: Android target에 `dart-define`을 포함한 `flutter run` 경로로 앱을 띄운다. 기본 정책은 Baron SSO Hosted Login + PKCE 진입이며, 필요 시에만 legacy local `phone-login` bootstrap으로 post-login 상태를 seed 한다.
- `format-dart.sh`: Docker Flutter 환경에서 Dart 코드 포맷을 적용한다.
- `generate-release-report.sh`: 현재 git 상태와 릴리스 점검 항목을 기반으로 report를 생성한다.
- `integration_tests.sh`: `app/integration_test/` smoke test를 실행한다. Android target 선행 확인은 실기기 기준이 기본이며 emulator는 fallback이다. `TDC114_API_BASE``TDC114_SMOKE_PHONE`을 넘기면 실제 API 로그인 smoke까지 확장할 수 있다.
- `mock-server.sh`: 향후 mock API 서버 제어용 scaffold 스크립트이다. 현재는 `status`, `stop`만 간단히 동작하고 `start`는 미구현 안내를 출력한다.
- `perf_smoke.sh`: 현재 앱/테스트 파일 수와 기본 상태를 빠르게 확인하는 경량 smoke 스크립트이다.
- `quality-gate.sh`: Docker Flutter 환경에서 `flutter analyze``flutter test`를 순서대로 실행한다.
- `save-snapshots.sh`: widget/integration snapshot 산출물이 있으면 지정한 report 경로로 복사해 보관한다.
- `test.sh`: 로컬 Flutter SDK가 설치된 환경에서 `flutter analyze``flutter test`를 실행한다.
- `redteam/run_all.sh`: 현재 앱 범위에서는 미사용인 AI/LLM redteam 자동화용 scaffold 스크립트이다.
## 참고
- 테스트 자동화 운영 기준은 `docs/guide_tdc114plus_script_automation_plan_2026-07-02.md`를 따른다.
- Docker 기반 Flutter 검증이 기본 경로이므로 일반적인 앱 변경 후에는 `format-dart.sh`, `quality-gate.sh`를 우선 사용한다.
- `api-smoke.sh`, `integration_tests.sh``scripts/.env.smoke.local` 파일이 있으면 자동으로 읽는다.
- staging 검증 시에는 `TDC114_SMOKE_ENV_FILE=scripts/.env.staging.local` 방식으로 env 파일을 명시해 실행한다.
- Android 공기계 USB 검증 시에는 `TDC114_SMOKE_ENV_FILE=scripts/.env.android-device.local``TDC114_FLUTTER_DEVICE_ID=<PHYSICAL_DEVICE_ID>`를 명시한다.
- ADB 로그 조회, `devices`, `reverse --list`, `screencap` 등 Flutter가 필요 없는 ADB 작업은 `flutter-docker.sh adb` 대신 `ADB_SERVER_SOCKET=tcp:172.21.128.1:5037 ./scripts/host-adb.sh ...`를 우선 사용한다.
- USB 없는 staging 직접 로그인 검증 시에는 `TDC114_SMOKE_ENV_FILE=scripts/.env.android-device.staging.local``TDC114_FLUTTER_DEVICE_ID=<PHYSICAL_DEVICE_ID>`를 명시한다.
- 로그인만 staging, 직원/조직 정보만 production으로 분리해야 할 경우 `TDC114_AUTH_API_BASE`, `TDC114_DIRECTORY_API_BASE`, `TDC114_ORGANIZATION_API_BASE`를 추가로 지정한다. 값을 비우면 모두 `TDC114_API_BASE`를 따른다.
- `startup.sh`, `shutdown.sh`는 테스트나 다른 worktree 재사용을 위해 `BARON_SSO_WORKTREE`, `TDC114_LOG_BASE`, `DOCKER_BIN` 같은 환경 변수 override를 지원한다.
- 업무 시작 절차에서는 앱 직접 App Link callback 테스트 서버를 더 이상 기동하지 않는다. 현재 로그인은 `tdc114plus-auth``/api/v1/auth/link/init`, `/api/v1/auth/link/poll` 경로를 사용한다.
- Windows Android Studio emulator + WSL/Docker Flutter 운영 기준은 `docs/policy_android_studio_wsl_adb_2026-07-03.md`를 따른다.
- Android 앱 설치/실행 재발 방지 기준은 `docs/policy_android_app_install_execution_2026-07-06.md`를 따른다.
- 반복 지연 대응 정리는 `docs/troubleshooting/flutter-docker-repeated-delay-countermeasures-2026-07-03.md`를 참고한다.
- `TDC114_SESSION_BOOTSTRAP_MODE=legacy-phone-login`은 옛 호환 점검용 예외 경로다. 신규 정책 기본값은 mock seed 또는 실제 Hosted Login + PKCE 검증이다.
+18
View File
@@ -0,0 +1,18 @@
# Copy this file to scripts/.env.android-device.local for physical Android device checks.
# Physical devices cannot use the Android emulator-only 10.0.2.2 host alias.
# Preferred path:
# adb reverse tcp:5000 tcp:5000
# adb reverse tcp:5001 tcp:5001
# TDC114_API_BASE=http://127.0.0.1:5000
# TDC114_AUTH_API_BASE=http://127.0.0.1:5001
#
# Fallback path:
# TDC114_API_BASE=http://<PC_LAN_IP>:5000
# TDC114_AUTH_API_BASE=http://<PC_LAN_IP>:5001
TDC114_API_BASE=http://127.0.0.1:5000
TDC114_AUTH_API_BASE=http://127.0.0.1:5001
# Use a test phone number that exists in the local Baron SSO test data.
TDC114_SMOKE_PHONE=01012345678
TDC114_SMOKE_ASSUME_LOGGED_IN=1
@@ -0,0 +1,20 @@
# Copy this file to scripts/.env.android-device.production.local for USB-free production-style tests.
# The app talks directly to the public Baron production API over HTTPS.
# No adb reverse is required in this mode.
TDC114_API_BASE=https://production.example.com
# Skip local phone-login bootstrap and open the normal in-app login flow.
TDC114_SKIP_SESSION_BOOTSTRAP=true
# Optional hints for smoke/integration helpers.
TDC114_SMOKE_AUTH_FLOW=link
# TDC114_SMOKE_PHONE=010xxxxxxxx
# TDC114_SMOKE_EXPECTED_NAME=홍길동
# TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3
# Optional split mode:
# Keep everything on production unless a temporary login/data split is required.
# TDC114_AUTH_API_BASE=https://production.example.com
# TDC114_DIRECTORY_API_BASE=https://production.example.com
# TDC114_ORGANIZATION_API_BASE=https://production.example.com
@@ -0,0 +1,24 @@
# Copy this file to scripts/.env.android-device.staging.local for USB-free staging login tests.
# The app talks directly to the public Baron staging API over HTTPS.
# No adb reverse is required in this mode.
TDC114_API_BASE=https://sso.hmac.kr
TDC114_ORG_CONTEXT_API_BASE=https://sadmin.hmac.kr
TDC114_ORG_CONTEXT_TENANT_SLUG=hanmac-family
TDC114_BARON_KEY_ID=replace-with-staging-key-id
TDC114_BARON_KEY_SECRET=replace-with-staging-key-secret
# Skip local phone-login bootstrap and open the normal in-app login flow.
TDC114_SKIP_SESSION_BOOTSTRAP=true
# Optional hints for smoke/integration helpers.
TDC114_SMOKE_AUTH_FLOW=link
TDC114_SMOKE_PHONE=010xxxxxxxx
# TDC114_SMOKE_EXPECTED_NAME=홍길동
# TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3
# Optional split mode:
# Login and org-context stay on staging by default.
# TDC114_AUTH_API_BASE=https://sso.hmac.kr
# TDC114_DIRECTORY_API_BASE=https://sadmin.hmac.kr
# TDC114_ORGANIZATION_API_BASE=https://sadmin.hmac.kr
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOCAL_ENV_FILE="${TDC114_SMOKE_ENV_FILE:-$ROOT_DIR/scripts/.env.smoke.local}"
if [ -f "$LOCAL_ENV_FILE" ]; then
# shellcheck disable=SC1090
source "$LOCAL_ENV_FILE"
fi
if [ -z "${TDC114_API_BASE:-}" ]; then
echo "TDC114_API_BASE is required for API smoke tests." >&2
echo "Example: TDC114_API_BASE=http://127.0.0.1:5000 $0" >&2
if [ -f "$LOCAL_ENV_FILE" ]; then
echo "Loaded local smoke env from $LOCAL_ENV_FILE" >&2
fi
exit 64
fi
if [ "${TDC114_API_BASE:-}" = "https://staging.example.com" ]; then
echo "TDC114_API_BASE still points to the example placeholder. Update $LOCAL_ENV_FILE first." >&2
exit 64
fi
BASE_URL="${TDC114_API_BASE%/}"
AUTH_FLOW="${TDC114_SMOKE_AUTH_FLOW:-phone-login}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
request() {
local method="$1"
local path="$2"
local body="${3:-}"
local token="${4:-}"
local output_file="$TMP_DIR/response"
local headers=(-H "Accept: application/json")
if [ -n "$token" ]; then
headers+=(-H "Authorization: Bearer $token")
fi
if [ -n "$body" ]; then
headers+=(-H "Content-Type: application/json")
curl -sS --max-time 10 -o "$output_file" -w "%{http_code}" \
-X "$method" "${headers[@]}" --data "$body" "$BASE_URL$path"
else
curl -sS --max-time 10 -o "$output_file" -w "%{http_code}" \
-X "$method" "${headers[@]}" "$BASE_URL$path"
fi
}
assert_status() {
local name="$1"
local actual="$2"
shift 2
for expected in "$@"; do
if [ "$actual" = "$expected" ]; then
echo "PASS $name: HTTP $actual"
return 0
fi
done
echo "FAIL $name: HTTP $actual" >&2
echo "Response body:" >&2
sed -n '1,40p' "$TMP_DIR/response" >&2
exit 1
}
extract_json_string() {
local key="$1"
sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" "$TMP_DIR/response" | head -n 1
}
echo "tdc114plus API smoke: $BASE_URL"
status="$(request GET "/api/v1/tdc114plus/directory/employees")"
assert_status "unauthorized directory guard" "$status" 401 403
status="$(request POST "/api/v1/tdc114plus/auth/phone-login" '{"phoneNumber":"123","device":{"platform":"smoke","appVersion":"0.1.0","deviceName":"api-smoke"}}')"
assert_status "invalid phone-login validation" "$status" 400
status="$(request POST "/api/v1/tdc114plus/auth/link/init" '{"phoneNumber":"123","device":{"platform":"smoke","appVersion":"0.1.0","deviceName":"api-smoke"}}')"
assert_status "invalid link-init validation" "$status" 400
status="$(request POST "/api/v1/tdc114plus/auth/link/poll" '{"pendingRef":"api-smoke-missing-ref"}')"
assert_status "link-poll expired pendingRef" "$status" 200
if [ "${TDC114_SKIP_AUTH_SMOKE:-false}" = "true" ]; then
echo "SKIP authenticated smoke: TDC114_SKIP_AUTH_SMOKE=true"
exit 0
fi
if [ -z "${TDC114_SMOKE_PHONE:-}" ]; then
echo "SKIP authenticated smoke: TDC114_SMOKE_PHONE is not set"
exit 0
fi
if [ "$AUTH_FLOW" = "link" ]; then
login_body="$(printf '{"phoneNumber":"%s","device":{"platform":"smoke","appVersion":"0.1.0","deviceName":"api-smoke"}}' "$TDC114_SMOKE_PHONE")"
status="$(request POST "/api/v1/tdc114plus/auth/link/init" "$login_body")"
assert_status "link-init success" "$status" 200
pending_ref="$(extract_json_string "pendingRef")"
if [ -z "$pending_ref" ]; then
echo "FAIL link-init success: pendingRef is missing" >&2
exit 1
fi
status="$(request POST "/api/v1/tdc114plus/auth/link/poll" "$(printf '{"pendingRef":"%s"}' "$pending_ref")")"
assert_status "link-poll pending" "$status" 200
poll_code="$(extract_json_string "code")"
if [ -n "$poll_code" ] && [ "$poll_code" != "authorization_pending" ] && [ "$poll_code" != "slow_down" ]; then
echo "FAIL link-poll pending: unexpected code=$poll_code" >&2
sed -n '1,40p' "$TMP_DIR/response" >&2
exit 1
fi
echo "PASS staging link smoke: init succeeded and poll is pending"
echo "NOTE manual approval is still required to complete end-to-end login."
exit 0
fi
if [ "$AUTH_FLOW" != "phone-login" ]; then
echo "Unknown TDC114_SMOKE_AUTH_FLOW=$AUTH_FLOW" >&2
echo "Expected one of: phone-login, link" >&2
exit 64
fi
login_body="$(printf '{"phoneNumber":"%s","device":{"platform":"smoke","appVersion":"0.1.0","deviceName":"api-smoke"}}' "$TDC114_SMOKE_PHONE")"
status="$(request POST "/api/v1/tdc114plus/auth/phone-login" "$login_body")"
assert_status "phone-login success" "$status" 200
token="$(extract_json_string "token")"
if [ -z "$token" ]; then
echo "FAIL phone-login success: token is missing" >&2
exit 1
fi
status="$(request GET "/api/v1/tdc114plus/directory/employees?limit=10" "" "$token")"
assert_status "employee list" "$status" 200
status="$(request GET "/api/v1/tdc114plus/organization/tenants" "" "$token")"
assert_status "tenant list" "$status" 200
status="$(request GET "/api/v1/tdc114plus/organization/orgchart" "" "$token")"
assert_status "orgchart" "$status" 200
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BARON_DIR="${BARON_SSO_WORKTREE:-/home/ubuntu/workspace/baron-sso-tdc114plus-api}"
SOURCE_FILE="$BARON_DIR/.env.sample"
TARGET_FILE="$BARON_DIR/.env"
if [ ! -f "$SOURCE_FILE" ]; then
echo "Baron SSO env sample not found: $SOURCE_FILE" >&2
exit 1
fi
if [ -f "$TARGET_FILE" ]; then
echo "Target env file already exists: $TARGET_FILE" >&2
echo "Remove or rename it first if you want to regenerate from the sample." >&2
exit 1
fi
cp "$SOURCE_FILE" "$TARGET_FILE"
cat >>"$TARGET_FILE" <<'EOF'
# --- tdc114plus local smoke overrides ---
# Added by scripts/bootstrap-baron-api-env.sh
APP_ENV=development
BACKEND_LOG_LEVEL=debug
CLIENT_LOG_DEBUG=true
CORS_ALLOWED_ORIGINS=http://localhost:5000
USERFRONT_URL=http://localhost:5000
BACKEND_PUBLIC_URL=http://localhost:5000
BACKEND_URL=http://localhost:5000
OATHKEEPER_PUBLIC_URL=http://localhost:5000
ADMINFRONT_URL=http://localhost:5173
DEVFRONT_URL=http://localhost:5174
ORGFRONT_URL=http://localhost:5175
KRATOS_UI_URL=http://localhost:5000
KRATOS_BROWSER_URL=http://localhost:5000/auth
WORKS_ADMIN_OAUTH_TOKEN_URL=https://auth.worksmobile.com/oauth2/v2.0/token
WORKSMOBILE_ENABLED=false
WORKSMOBILE_RELAY_ENABLED=false
LOGIN_LOGOUT_SMS_ENABLED=false
LOGIN_LOGOUT_NAVER_ENABLED=false
VITE_CLIENT_LOG_DEBUG=false
DEV_SERVER_WATCH_POLLING=true
EOF
echo "Created $TARGET_FILE from $SOURCE_FILE"
echo "A local smoke override block was appended to prefer localhost URLs and disable optional external notifications."
echo "Next steps:"
echo " 1. Review $TARGET_FILE and replace any placeholder secrets if your local backend requires real values."
echo " 2. Run ./scripts/check-baron-api-env.sh"
echo " 3. If runtime looks ready, run TDC114_API_BASE=http://127.0.0.1:5000 ./scripts/api-smoke.sh"
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
FLUTTER_DOCKER_SCRIPT="$ROOT_DIR/scripts/flutter-docker.sh"
HOST_ADB_SCRIPT="${TDC114_HOST_ADB_SCRIPT:-$ROOT_DIR/scripts/host-adb.sh}"
LOCAL_ENV_FILE="${TDC114_SMOKE_ENV_FILE:-$ROOT_DIR/scripts/.env.android-device.local}"
if [ -f "$LOCAL_ENV_FILE" ]; then
# shellcheck disable=SC1090
source "$LOCAL_ENV_FILE"
fi
ADB_SOCKET="${ADB_SERVER_SOCKET:-}"
DEVICE_ID="${TDC114_FLUTTER_DEVICE_ID:-}"
API_BASE="${TDC114_API_BASE:-}"
AUTH_API_BASE="${TDC114_AUTH_API_BASE:-}"
REVERSE_PORT="${TDC114_ANDROID_REVERSE_PORT:-5000}"
AUTH_REVERSE_PORT="${TDC114_AUTH_ANDROID_REVERSE_PORT:-5001}"
is_reverse_mode() {
[ "$API_BASE" = "http://127.0.0.1:${REVERSE_PORT}" ]
}
is_auth_reverse_mode() {
[ "$AUTH_API_BASE" = "http://127.0.0.1:${AUTH_REVERSE_PORT}" ]
}
print_header() {
echo "[android-device-preflight]"
if [ -n "$ADB_SOCKET" ]; then
echo "ADB server socket: $ADB_SOCKET"
else
echo "ADB server socket: default container adb server"
fi
if [ -n "$DEVICE_ID" ]; then
echo "Target device id: $DEVICE_ID"
else
echo "Target device id: first physical device in adb devices"
fi
if [ -n "$API_BASE" ]; then
echo "TDC114_API_BASE: $API_BASE"
if is_reverse_mode; then
echo "Connection mode: local adb-reverse"
else
echo "Connection mode: direct HTTPS/LAN"
fi
fi
if [ -n "$AUTH_API_BASE" ]; then
echo "TDC114_AUTH_API_BASE: $AUTH_API_BASE"
if is_auth_reverse_mode; then
echo "Auth connection mode: local adb-reverse"
else
echo "Auth connection mode: direct HTTPS/LAN"
fi
fi
echo
}
fail_with_help() {
local reason="$1"
print_header
echo "FAIL $reason"
echo
echo "Windows-side checklist:"
echo "1. Connect the Android phone by USB."
echo "2. Enable Developer options > USB debugging."
echo "3. Accept the RSA debugging prompt on the phone."
echo "4. In Windows PowerShell, run adb.exe devices and confirm the phone is device."
echo "5. If using local API through adb reverse, run adb reverse tcp:${REVERSE_PORT} tcp:${REVERSE_PORT} from the same ADB server path."
if is_auth_reverse_mode; then
echo " Also run adb reverse tcp:${AUTH_REVERSE_PORT} tcp:${AUTH_REVERSE_PORT} for the local auth server."
fi
echo "6. If using direct staging/production HTTPS mode, confirm TDC114_API_BASE points to the public app API host."
if [ -n "$ADB_SOCKET" ]; then
echo "7. If WSL reports connection refused to ${ADB_SOCKET}, recreate the Windows 5037 portproxy and firewall rule first."
fi
echo
echo "Recommended WSL command:"
echo " ADB_SERVER_SOCKET=tcp:<WINDOWS_HOST_IP>:5037 \\"
echo " TDC114_FLUTTER_DEVICE_ID=<PHYSICAL_DEVICE_ID> \\"
echo " TDC114_SMOKE_ENV_FILE=scripts/.env.android-device.local \\"
echo " ./scripts/manual-postlogin-run.sh"
}
fail_with_adb_help() {
local reason="$1"
print_header
echo "FAIL $reason"
echo
echo "WSL host-adb checklist:"
echo "1. Confirm $HOST_ADB_SCRIPT exists and is executable."
echo "2. If host adb is missing, run ./scripts/install-host-adb-from-docker.sh once."
echo "3. Confirm ADB_SERVER_SOCKET points to the Windows ADB server, usually tcp:172.21.128.1:5037."
echo "4. After host adb access recovers, rerun this script."
echo
echo "Recommended WSL command:"
echo " ADB_SERVER_SOCKET=tcp:172.21.128.1:5037 ./scripts/host-adb.sh devices"
}
if [ ! -x "$HOST_ADB_SCRIPT" ]; then
echo "Missing executable script: $HOST_ADB_SCRIPT" >&2
exit 1
fi
if [[ "$DEVICE_ID" == emulator-* ]]; then
fail_with_help "TDC114_FLUTTER_DEVICE_ID points to an emulator, not a physical Android device."
exit 1
fi
env_args=()
if [ -n "$ADB_SOCKET" ]; then
env_args+=("ADB_SERVER_SOCKET=$ADB_SOCKET")
fi
set +e
if [ "${#env_args[@]}" -gt 0 ]; then
OUTPUT="$(env "${env_args[@]}" "$HOST_ADB_SCRIPT" devices 2>&1)"
else
OUTPUT="$("$HOST_ADB_SCRIPT" devices 2>&1)"
fi
STATUS=$?
set -e
if [ "$STATUS" -ne 0 ]; then
if printf '%s\n' "$OUTPUT" | grep -Fq "Host adb is not available"; then
fail_with_adb_help "Host adb is not ready."
printf '%s\n' "$OUTPUT"
exit "$STATUS"
fi
fail_with_help "ADB devices command failed."
if printf '%s\n' "$OUTPUT" | grep -Fq "Connection refused"; then
echo
echo "Hint: Windows ADB itself may be alive, but WSL shared access to port 5037 is blocked."
echo "Rebuild portproxy: 0.0.0.0:5037 -> 127.0.0.1:5037, then retry."
fi
if printf '%s\n' "$OUTPUT" | grep -Fq "protocol fault (couldn't read status)"; then
echo
echo "Hint: Windows port 5037 may be owned by portproxy before adb.exe starts."
echo "In Windows Administrator PowerShell: delete the 5037 portproxy, start adb.exe, confirm adb.exe devices, then recreate the portproxy."
fi
if printf '%s\n' "$OUTPUT" | grep -Fq "failed to read response from server"; then
echo
echo "Hint: Windows adb.exe may be talking to a stale or non-ADB listener on port 5037."
echo "In Windows Administrator PowerShell: inspect netstat -ano | findstr 5037 before restarting adb.exe."
fi
printf '%s\n' "$OUTPUT"
exit "$STATUS"
fi
if [ -n "$DEVICE_ID" ]; then
DEVICE_PATTERN="^${DEVICE_ID}[[:space:]]+device$"
OFFLINE_PATTERN="^${DEVICE_ID}[[:space:]]+offline$"
UNAUTHORIZED_PATTERN="^${DEVICE_ID}[[:space:]]+unauthorized$"
else
DEVICE_PATTERN="^[[:alnum:]_.:-]+[[:space:]]+device$"
OFFLINE_PATTERN="^[[:alnum:]_.:-]+[[:space:]]+offline$"
UNAUTHORIZED_PATTERN="^[[:alnum:]_.:-]+[[:space:]]+unauthorized$"
fi
if [ -z "$DEVICE_ID" ]; then
DETECTED_UNAUTHORIZED_ID="$(printf '%s\n' "$OUTPUT" | awk '$2 == "unauthorized" && $1 !~ /^emulator-/ {print $1; exit}')"
if [ -n "$DETECTED_UNAUTHORIZED_ID" ]; then
fail_with_help "Physical Android device is unauthorized."
printf '%s\n' "$OUTPUT"
exit 1
fi
DETECTED_OFFLINE_ID="$(printf '%s\n' "$OUTPUT" | awk '$2 == "offline" && $1 !~ /^emulator-/ {print $1; exit}')"
if [ -n "$DETECTED_OFFLINE_ID" ]; then
fail_with_help "Physical Android device is offline."
printf '%s\n' "$OUTPUT"
exit 1
fi
DETECTED_DEVICE_ID="$(printf '%s\n' "$OUTPUT" | awk '$2 == "device" && $1 !~ /^emulator-/ {print $1; exit}')"
if [ -z "$DETECTED_DEVICE_ID" ]; then
fail_with_help "No usable physical Android device was detected."
printf '%s\n' "$OUTPUT"
exit 1
fi
DEVICE_ID="$DETECTED_DEVICE_ID"
else
if printf '%s\n' "$OUTPUT" | grep -Eq "$UNAUTHORIZED_PATTERN"; then
fail_with_help "Physical Android device is unauthorized."
printf '%s\n' "$OUTPUT"
exit 1
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "$OFFLINE_PATTERN"; then
fail_with_help "Physical Android device is offline."
printf '%s\n' "$OUTPUT"
exit 1
fi
if ! printf '%s\n' "$OUTPUT" | grep -Eq "$DEVICE_PATTERN"; then
fail_with_help "No usable physical Android device was detected."
printf '%s\n' "$OUTPUT"
exit 1
fi
fi
if [ -z "$DEVICE_ID" ]; then
fail_with_help "No usable physical Android device was detected."
printf '%s\n' "$OUTPUT"
exit 1
fi
if is_reverse_mode || is_auth_reverse_mode; then
reverse_args=("adb")
if [ -n "$DEVICE_ID" ]; then
reverse_args+=("-s" "$DEVICE_ID")
fi
reverse_args+=("reverse" "--list")
set +e
if [ "${#env_args[@]}" -gt 0 ]; then
REVERSE_OUTPUT="$(env "${env_args[@]}" "$HOST_ADB_SCRIPT" "${reverse_args[@]:1}" 2>&1)"
else
REVERSE_OUTPUT="$("$HOST_ADB_SCRIPT" "${reverse_args[@]:1}" 2>&1)"
fi
REVERSE_STATUS=$?
set -e
if [ "$REVERSE_STATUS" -ne 0 ]; then
fail_with_help "adb reverse tcp:${REVERSE_PORT} tcp:${REVERSE_PORT} is not active."
printf '%s\n' "$OUTPUT"
printf '%s\n' "$REVERSE_OUTPUT"
exit 1
fi
if is_reverse_mode && ! printf '%s\n' "$REVERSE_OUTPUT" | grep -Fq "tcp:${REVERSE_PORT} tcp:${REVERSE_PORT}"; then
fail_with_help "adb reverse tcp:${REVERSE_PORT} tcp:${REVERSE_PORT} is not active."
printf '%s\n' "$OUTPUT"
printf '%s\n' "$REVERSE_OUTPUT"
exit 1
fi
if is_auth_reverse_mode && ! printf '%s\n' "$REVERSE_OUTPUT" | grep -Fq "tcp:${AUTH_REVERSE_PORT} tcp:${AUTH_REVERSE_PORT}"; then
fail_with_help "adb reverse tcp:${AUTH_REVERSE_PORT} tcp:${AUTH_REVERSE_PORT} is not active."
printf '%s\n' "$OUTPUT"
printf '%s\n' "$REVERSE_OUTPUT"
exit 1
fi
fi
print_header
echo "PASS Physical Android device is ready for Docker/WSL Flutter checks."
printf '%s\n' "$OUTPUT"
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
DEFAULT_ANDROID_ADDRESS="${TDC114_STARTUP_ANDROID_ADDRESS:-172.21.128.1:5559}"
ADB_ADDRESS="${TDC114_ADB_CONNECT_ADDRESS:-$DEFAULT_ANDROID_ADDRESS}"
ADB_HOST="${ADB_ADDRESS%%:*}"
ADB_PORT="${ADB_ADDRESS##*:}"
ADB_SOCKET="${ADB_SERVER_SOCKET:-}"
FLUTTER_DEVICE_ID="${TDC114_FLUTTER_DEVICE_ID:-}"
FLUTTER_DOCKER_SCRIPT="$ROOT_DIR/scripts/flutter-docker.sh"
print_common_header() {
echo "[android-preflight]"
if [ -n "$ADB_SOCKET" ]; then
echo "Target ADB server socket: $ADB_SOCKET"
if [ -n "$FLUTTER_DEVICE_ID" ]; then
echo "Target Flutter device id: $FLUTTER_DEVICE_ID"
fi
echo "Policy default: Windows ADB server sharing via ADB_SERVER_SOCKET"
else
echo "Target emulator address: $ADB_ADDRESS"
echo "Fallback direct emulator bridge: $DEFAULT_ANDROID_ADDRESS"
fi
echo
}
print_success() {
print_common_header
echo "PASS Android emulator is reachable from Docker/WSL."
echo "Next step: continue runtime startup and later run integration/manual Android checks."
}
print_failure_no_address() {
echo "[android-preflight]"
echo "FAIL Android emulator address is empty."
echo "Set TDC114_ADB_CONNECT_ADDRESS first, for example:"
echo " TDC114_ADB_CONNECT_ADDRESS=$DEFAULT_ANDROID_ADDRESS ./scripts/startup.sh --auto --wait=40"
}
print_failure_instructions() {
local reason="$1"
print_common_header
echo "FAIL $reason"
echo
echo "Local PC work required before startup continues:"
echo "1. On Windows, open Android Studio."
echo "2. In Device Manager, keep only one manual-test emulator running."
echo "3. In Windows PowerShell, run adb.exe devices and confirm the emulator is in device state."
echo "4. If it is offline or missing there too, restart the emulator first."
if [ -n "$ADB_SOCKET" ]; then
echo "5. If Windows adb is healthy but WSL still fails, make sure portproxy forwards 5037 -> 127.0.0.1:5037 and the firewall rule for 5037 still exists."
else
echo "5. If Windows adb is healthy but WSL still fails, make sure portproxy forwards $ADB_PORT -> 127.0.0.1:$ADB_PORT and the firewall rule for that port still exists."
fi
echo "6. After Windows-side recovery, rerun:"
if [ -n "$ADB_SOCKET" ]; then
echo " ADB_SERVER_SOCKET=$ADB_SOCKET ./scripts/startup.sh --auto --wait=40"
else
echo " TDC114_ADB_CONNECT_ADDRESS=$ADB_ADDRESS ./scripts/startup.sh --auto --wait=40"
fi
echo
echo "Minimum Windows-side action I cannot do from here:"
echo "- Start or restart the emulator GUI."
if [ -n "$ADB_SOCKET" ]; then
echo "- If needed, fix Windows adb/portproxy state for port 5037."
else
echo "- If needed, fix Windows adb/portproxy state for port $ADB_PORT."
fi
echo
echo "What I can keep doing from WSL after that:"
echo "- Recheck Docker/WSL visibility."
echo "- Continue Baron runtime startup."
echo "- Run API smoke and integration commands."
}
if [ -z "$ADB_ADDRESS" ]; then
print_failure_no_address
exit 1
fi
if [ ! -x "$FLUTTER_DOCKER_SCRIPT" ]; then
echo "Missing executable script: $FLUTTER_DOCKER_SCRIPT" >&2
exit 1
fi
if [ -n "$ADB_SOCKET" ]; then
set +e
OUTPUT="$(ADB_SERVER_SOCKET="$ADB_SOCKET" "$FLUTTER_DOCKER_SCRIPT" adb devices 2>&1)"
STATUS=$?
set -e
if [ -n "$FLUTTER_DEVICE_ID" ]; then
DEVICE_PATTERN="^${FLUTTER_DEVICE_ID}[[:space:]]+device$"
OFFLINE_PATTERN="^${FLUTTER_DEVICE_ID}[[:space:]]+offline$"
UNAUTHORIZED_PATTERN="^${FLUTTER_DEVICE_ID}[[:space:]]+unauthorized$"
else
DEVICE_PATTERN="^emulator-[0-9]+[[:space:]]+device$"
OFFLINE_PATTERN="^emulator-[0-9]+[[:space:]]+offline$"
UNAUTHORIZED_PATTERN="^emulator-[0-9]+[[:space:]]+unauthorized$"
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "$DEVICE_PATTERN"; then
print_success
printf '%s\n' "$OUTPUT"
exit 0
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "$OFFLINE_PATTERN"; then
print_failure_instructions "Android emulator is visible through Windows ADB server but offline."
printf '%s\n' "$OUTPUT"
exit 1
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "$UNAUTHORIZED_PATTERN"; then
print_common_header
echo "FAIL Android emulator is unauthorized through Windows ADB server."
echo
echo "Local PC work required before startup continues:"
echo "1. Bring the emulator window to front."
echo "2. Accept the RSA debugging prompt if it is visible."
echo "3. Confirm Windows PowerShell adb.exe devices shows device."
echo
echo "After approval, rerun:"
echo " ADB_SERVER_SOCKET=$ADB_SOCKET ./scripts/startup.sh --auto --wait=40"
printf '%s\n' "$OUTPUT"
exit 1
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "protocol fault|Connection refused|cannot connect|failed to check server version"; then
print_failure_instructions "Docker/WSL could not use the Windows ADB server socket."
printf '%s\n' "$OUTPUT"
exit 1
fi
print_failure_instructions "Android emulator was not detected through the Windows ADB server socket."
printf '%s\n' "$OUTPUT"
exit "${STATUS:-1}"
fi
set +e
OUTPUT="$(TDC114_ADB_CONNECT_ADDRESS="$ADB_ADDRESS" "$FLUTTER_DOCKER_SCRIPT" adb-devices 2>&1)"
STATUS=$?
set -e
if printf '%s\n' "$OUTPUT" | grep -Eq "^${ADB_ADDRESS}[[:space:]]+device$"; then
print_success
printf '%s\n' "$OUTPUT"
exit 0
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "^${ADB_ADDRESS}[[:space:]]+offline$"; then
print_failure_instructions "Android emulator is visible but offline."
printf '%s\n' "$OUTPUT"
exit 1
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "^${ADB_ADDRESS}[[:space:]]+unauthorized$"; then
print_common_header
echo "FAIL Android emulator is unauthorized."
echo
echo "Local PC work required before startup continues:"
echo "1. Bring the emulator window to front."
echo "2. Accept the RSA debugging prompt if it is visible."
echo "3. If no prompt appears, restart the emulator once and check adb.exe devices again on Windows."
echo
echo "After approval, rerun:"
echo " TDC114_ADB_CONNECT_ADDRESS=$ADB_ADDRESS ./scripts/startup.sh --auto --wait=40"
echo
echo "What I can do after that:"
echo "- Retry WSL/Docker device detection."
echo "- Continue the rest of startup automatically."
printf '%s\n' "$OUTPUT"
exit 1
fi
if printf '%s\n' "$OUTPUT" | grep -Eq "failed to connect to ${ADB_HOST}:${ADB_PORT}|Connection refused|cannot connect|protocol fault"; then
print_failure_instructions "Docker/WSL could not connect to the Windows emulator bridge."
printf '%s\n' "$OUTPUT"
exit 1
fi
print_failure_instructions "Android emulator was not detected in a usable state."
printf '%s\n' "$OUTPUT"
exit "${STATUS:-1}"
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BARON_DIR="${BARON_SSO_WORKTREE:-/home/ubuntu/workspace/baron-sso-tdc114plus-api}"
failures=0
warnings=0
section() {
printf '\n[%s]\n' "$1"
}
pass() {
printf 'PASS %s\n' "$1"
}
warn() {
printf 'WARN %s\n' "$1"
warnings=$((warnings + 1))
}
fail() {
printf 'FAIL %s\n' "$1"
failures=$((failures + 1))
}
check_file() {
local path="$1"
local label="$2"
if [ -f "$path" ]; then
pass "$label: $path"
else
fail "$label: missing $path"
fi
}
section "worktree"
if [ -d "$BARON_DIR/.git" ] || [ -f "$BARON_DIR/.git" ]; then
pass "Baron SSO worktree found: $BARON_DIR"
else
fail "Baron SSO worktree not found: $BARON_DIR"
fi
check_file "$BARON_DIR/docker-compose.yaml" "compose file"
check_file "$BARON_DIR/.env.sample" "env sample"
section "runtime files"
if [ -f "$BARON_DIR/.env" ]; then
pass "runtime env file exists"
else
fail "runtime env file missing (.env)"
if [ -f "$BARON_DIR/config-restored/env.redacted" ]; then
warn "redacted env reference exists at config-restored/env.redacted"
fi
fi
if find "$BARON_DIR/config" -maxdepth 1 -type f ! -name '.gitkeep' | grep -q .; then
pass "config directory has runtime files"
else
warn "config directory has no runtime files beyond .gitkeep"
fi
if [ -f "$BARON_DIR/config-restored/compose/docker-compose.yaml" ]; then
pass "restored compose reference exists"
else
warn "restored compose reference is missing"
fi
section "docker"
if ! command -v docker >/dev/null 2>&1; then
warn "docker command not found"
else
if docker network inspect baron_net >/dev/null 2>&1; then
pass "docker network baron_net exists"
else
warn "docker network baron_net not found or not accessible"
fi
if docker network inspect ory-net >/dev/null 2>&1; then
pass "docker network ory-net exists"
else
warn "docker network ory-net not found or not accessible"
fi
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx 'baron_backend'; then
pass "baron_backend container is running"
else
warn "baron_backend container is not running"
fi
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx 'baron_gateway'; then
pass "baron_gateway container is running"
else
warn "baron_gateway container is not running"
fi
fi
section "next steps"
if [ "$failures" -gt 0 ]; then
printf 'RESULT blocked: %d failure(s), %d warning(s)\n' "$failures" "$warnings"
printf 'Hint: create %s/.env from .env.sample and prepare runtime config before API smoke.\n' "$BARON_DIR"
exit 1
fi
printf 'RESULT ready-ish: %d failure(s), %d warning(s)\n' "$failures" "$warnings"
printf 'Next command example: TDC114_API_BASE=http://127.0.0.1:5000 ./scripts/api-smoke.sh\n'
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
STATE_DIR="${TDC114_WSL_MAINTENANCE_STATE_DIR:-$ROOT_DIR/.tmp}"
STATE_FILE="${TDC114_WSL_MAINTENANCE_STATE_FILE:-$STATE_DIR/wsl-maintenance-last-check.txt}"
ANCHOR_DATE="${TDC114_WSL_MAINTENANCE_ANCHOR_DATE:-2026-07-16}"
INTERVAL_DAYS="${TDC114_WSL_MAINTENANCE_INTERVAL_DAYS:-14}"
MODE="${1:-status}"
TODAY="${TDC114_WSL_MAINTENANCE_TODAY:-$(date +%F)}"
usage() {
cat <<EOF
Usage: $0 [status|record|reset]
status : print whether the recurring WSL maintenance check is due
record : mark today's date as the completed maintenance date
reset : remove the stored completion date
EOF
}
require_date() {
local value="$1"
if ! date -d "$value" +%F >/dev/null 2>&1; then
echo "Invalid date: $value" >&2
exit 2
fi
}
require_non_negative_integer() {
local value="$1"
case "$value" in
''|*[!0-9]*)
echo "Interval must be a non-negative integer: $value" >&2
exit 2
;;
esac
}
days_since() {
local from_date="$1"
local to_date="$2"
local from_epoch
local to_epoch
from_epoch="$(date -d "$from_date" +%s)"
to_epoch="$(date -d "$to_date" +%s)"
echo $(((to_epoch - from_epoch) / 86400))
}
print_due_message() {
local base_date="$1"
local elapsed_days="$2"
local next_due_date
next_due_date="$(date -d "$base_date + $INTERVAL_DAYS days" +%F)"
cat <<EOF
WSL maintenance reminder: DUE
Base date: $base_date
Days elapsed: $elapsed_days
Due date: $next_due_date
General PowerShell:
1. Close VS Code, WSL terminals, Docker Desktop, and any \\\\wsl$ Explorer windows.
2. Run: wsl --shutdown
Administrator PowerShell:
1. Run: diskpart
2. Then run:
select vdisk file="%LOCALAPPDATA%\\Packages\\CanonicalGroupLimited.Ubuntu_79rhkp1fndgsc\\LocalState\\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit
After maintenance is complete, record it with:
./scripts/check-wsl-maintenance.sh record
EOF
}
print_not_due_message() {
local base_date="$1"
local elapsed_days="$2"
local remaining_days
local next_due_date
remaining_days=$((INTERVAL_DAYS - elapsed_days))
next_due_date="$(date -d "$base_date + $INTERVAL_DAYS days" +%F)"
cat <<EOF
WSL maintenance reminder: OK
Base date: $base_date
Days elapsed: $elapsed_days
Next due date: $next_due_date
Days remaining: $remaining_days
EOF
}
require_date "$ANCHOR_DATE"
require_date "$TODAY"
require_non_negative_integer "$INTERVAL_DAYS"
mkdir -p "$STATE_DIR"
case "$MODE" in
status)
base_date="$ANCHOR_DATE"
if [ -f "$STATE_FILE" ] && [ -s "$STATE_FILE" ]; then
base_date="$(tr -d '[:space:]' < "$STATE_FILE")"
require_date "$base_date"
fi
elapsed_days="$(days_since "$base_date" "$TODAY")"
if [ "$elapsed_days" -lt 0 ]; then
echo "Stored date is in the future: $base_date" >&2
exit 2
fi
if [ "$elapsed_days" -ge "$INTERVAL_DAYS" ]; then
print_due_message "$base_date" "$elapsed_days"
else
print_not_due_message "$base_date" "$elapsed_days"
fi
;;
record)
printf '%s\n' "$TODAY" > "$STATE_FILE"
echo "Recorded WSL maintenance date: $TODAY"
;;
reset)
rm -f "$STATE_FILE"
echo "Removed recorded WSL maintenance date: $STATE_FILE"
;;
--help|-h|help)
usage
;;
*)
echo "Unknown mode: $MODE" >&2
usage >&2
exit 2
;;
esac
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
import argparse
import csv
import json
from collections import defaultdict
from pathlib import Path
ROOT_SLUG = "hanmac-family"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Export Baron org-context JSON to CSV files for photo mapping."
)
parser.add_argument("--input", required=True, help="Path to org-context JSON file")
parser.add_argument(
"--output-dir", required=True, help="Directory where CSV files will be written"
)
return parser.parse_args()
def normalize_phone(phone: str) -> str:
digits = "".join(ch for ch in (phone or "") if ch.isdigit())
if digits.startswith("82") and len(digits) >= 11:
digits = "0" + digits[2:]
return digits
def format_phone(phone: str) -> str:
digits = normalize_phone(phone)
if len(digits) == 11:
return f"{digits[:3]}-{digits[3:7]}-{digits[7:]}"
if len(digits) == 10:
return f"{digits[:3]}-{digits[3:6]}-{digits[6:]}"
return phone or ""
def email_local_part(email: str) -> str:
if "@" not in (email or ""):
return ""
return email.split("@", 1)[0].strip()
def load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def build_tree_indexes(tree: dict) -> tuple[dict, dict, dict]:
tenant_by_id: dict[str, dict] = {}
parent_by_id: dict[str, str | None] = {}
children_by_id: dict[str, list[str]] = defaultdict(list)
def visit(node: dict, parent_id: str | None) -> None:
tenant_id = node.get("id", "")
if not tenant_id:
return
tenant_by_id[tenant_id] = node
parent_by_id[tenant_id] = parent_id
if parent_id:
children_by_id[parent_id].append(tenant_id)
for child in node.get("children", []) or []:
if isinstance(child, dict):
visit(child, tenant_id)
visit(tree, None)
return tenant_by_id, parent_by_id, children_by_id
def build_flat_tenant_index(tenants: list[dict]) -> dict[str, dict]:
return {
tenant.get("id", ""): tenant
for tenant in tenants
if isinstance(tenant, dict) and tenant.get("id")
}
def tenant_path(
tenant_id: str, tenant_by_id: dict[str, dict], parent_by_id: dict[str, str | None]
) -> list[dict]:
path: list[dict] = []
current_id = tenant_id
while current_id:
node = tenant_by_id.get(current_id)
if node is None:
break
path.append(node)
current_id = parent_by_id.get(current_id) or ""
path.reverse()
return path
def resolve_company(path: list[dict], tenant: dict) -> tuple[str, str]:
if not path:
return tenant.get("id", ""), tenant.get("name", "")
if len(path) == 1:
return path[0].get("id", ""), path[0].get("name", "")
for node in path[1:]:
node_type = (node.get("type") or "").upper()
if node_type in {"COMPANY", "ORGANIZATION"}:
return node.get("id", ""), node.get("name", "")
first_child = path[1]
return first_child.get("id", ""), first_child.get("name", "")
def row_sort_key(row: dict) -> tuple:
phone_key = row["phone_normalized"] or "99999999999"
return (
row["company_name"],
phone_key,
row["department_name"],
row["name"],
row["email"],
)
def primary_preference(row: dict) -> tuple:
return (
0 if row["is_primary"] == "true" else 1,
0 if row["phone_normalized"] else 1,
0 if row["grade"] else 1,
0 if row["position"] else 1,
row["company_name"],
row["department_name"],
row["email"],
)
def build_rows(payload: dict) -> tuple[list[dict], list[dict], list[dict]]:
tree = payload.get("tree") or {}
tenants = payload.get("tenants") or []
tree_tenant_by_id, parent_by_id, _children_by_id = build_tree_indexes(tree)
flat_tenant_by_id = build_flat_tenant_index(tenants)
for tenant_id, tenant in flat_tenant_by_id.items():
tree_tenant_by_id.setdefault(tenant_id, tenant)
membership_rows: list[dict] = []
for tenant in tenants:
if not isinstance(tenant, dict):
continue
tenant_id = tenant.get("id", "")
tenant_name = tenant.get("name", "")
path = tenant_path(tenant_id, tree_tenant_by_id, parent_by_id)
company_tenant_id, company_name = resolve_company(path, tenant)
path_names = " > ".join(
node.get("name", "")
for node in path
if node.get("name") and node.get("slug") != ROOT_SLUG
)
for member in tenant.get("members", []) or []:
if not isinstance(member, dict):
continue
raw_phone = (member.get("phone") or member.get("phoneNumber") or "").strip()
row = {
"company_tenant_id": company_tenant_id,
"company_name": company_name,
"department_tenant_id": tenant_id,
"department_name": tenant_name,
"department_path": path_names,
"email": (member.get("email") or "").strip(),
"email_id": email_local_part(member.get("email") or ""),
"phone_raw": raw_phone,
"phone_normalized": normalize_phone(raw_phone),
"phone_display": format_phone(raw_phone),
"name": (member.get("name") or "").strip(),
"grade": (member.get("grade") or "").strip(),
"position": (member.get("position") or "").strip(),
"member_department": (member.get("department") or "").strip(),
"is_primary": "true" if member.get("isPrimary") else "false",
"member_id": (member.get("id") or "").strip(),
}
membership_rows.append(row)
membership_rows.sort(key=row_sort_key)
primary_rows = [row for row in membership_rows if row["is_primary"] == "true"]
phone_index: dict[str, list[dict]] = defaultdict(list)
for row in membership_rows:
if row["phone_normalized"]:
phone_index[row["phone_normalized"]].append(row)
deduped_rows: list[dict] = []
for phone in sorted(phone_index):
best = sorted(phone_index[phone], key=primary_preference)[0]
duplicate_count = len(phone_index[phone])
selected = dict(best)
selected["duplicate_row_count_for_phone"] = str(duplicate_count)
deduped_rows.append(selected)
deduped_rows.sort(key=row_sort_key)
return membership_rows, primary_rows, deduped_rows
def write_csv(path: Path, rows: list[dict], fieldnames: list[str]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow({field: row.get(field, "") for field in fieldnames})
def write_summary(path: Path, membership_rows: list[dict], primary_rows: list[dict], deduped_rows: list[dict]) -> None:
company_counts: dict[str, int] = defaultdict(int)
for row in deduped_rows:
company_counts[row["company_name"]] += 1
lines = [
"Baron org-context CSV export summary",
f"membership_rows={len(membership_rows)}",
f"primary_rows={len(primary_rows)}",
f"deduped_phone_rows={len(deduped_rows)}",
"company_counts:",
]
for company_name in sorted(company_counts):
lines.append(f"- {company_name}: {company_counts[company_name]}")
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main() -> None:
args = parse_args()
payload = load_json(Path(args.input))
membership_rows, primary_rows, deduped_rows = build_rows(payload)
output_dir = Path(args.output_dir)
fieldnames = [
"company_tenant_id",
"company_name",
"department_tenant_id",
"department_name",
"department_path",
"email",
"email_id",
"phone_raw",
"phone_normalized",
"phone_display",
"name",
"grade",
"position",
"member_department",
"is_primary",
"member_id",
]
deduped_fieldnames = fieldnames + ["duplicate_row_count_for_phone"]
write_csv(output_dir / "baron_org_context_membership_rows.csv", membership_rows, fieldnames)
write_csv(output_dir / "baron_org_context_primary_rows.csv", primary_rows, fieldnames)
write_csv(output_dir / "baron_org_context_phone_mapping.csv", deduped_rows, deduped_fieldnames)
write_summary(
output_dir / "baron_org_context_export_summary.txt",
membership_rows,
primary_rows,
deduped_rows,
)
if __name__ == "__main__":
main()
+56 -1
View File
@@ -3,10 +3,65 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
IMAGE="${FLUTTER_DOCKER_IMAGE:-ghcr.io/cirruslabs/flutter:stable}"
ADB_KEY_DIR="$ROOT_DIR/.android-adb"
CACHE_ROOT="${FLUTTER_DOCKER_CACHE_ROOT:-$ROOT_DIR/.docker-cache/flutter}"
GRADLE_CACHE_DIR="$CACHE_ROOT/gradle"
PUB_CACHE_DIR="$CACHE_ROOT/pub"
ANDROID_LICENSES_DIR="$CACHE_ROOT/android-sdk/licenses"
ANDROID_NDK_DIR="$CACHE_ROOT/android-sdk/ndk"
ANDROID_CMAKE_DIR="$CACHE_ROOT/android-sdk/cmake"
MODE="${1:-flutter}"
if [ "$#" -gt 0 ]; then
shift || true
fi
DOCKER_ENV_ARGS=()
if [ -n "${ADB_SERVER_SOCKET:-}" ]; then
DOCKER_ENV_ARGS+=("-e" "ADB_SERVER_SOCKET=${ADB_SERVER_SOCKET}")
fi
if [ -n "${TDC114_ADB_CONNECT_ADDRESS:-}" ]; then
DOCKER_ENV_ARGS+=("-e" "TDC114_ADB_CONNECT_ADDRESS=${TDC114_ADB_CONNECT_ADDRESS}")
fi
mkdir -p "$ADB_KEY_DIR"
mkdir -p "$GRADLE_CACHE_DIR"
mkdir -p "$PUB_CACHE_DIR"
mkdir -p "$ANDROID_LICENSES_DIR"
mkdir -p "$ANDROID_NDK_DIR"
mkdir -p "$ANDROID_CMAKE_DIR"
docker run --rm \
-u 0:0 \
"${DOCKER_ENV_ARGS[@]}" \
-v "$ROOT_DIR:/workspace" \
-v "$ADB_KEY_DIR:/root/.android" \
-v "$GRADLE_CACHE_DIR:/root/.gradle" \
-v "$PUB_CACHE_DIR:/root/.pub-cache" \
-v "$ANDROID_LICENSES_DIR:/opt/android-sdk-linux/licenses" \
-v "$ANDROID_NDK_DIR:/opt/android-sdk-linux/ndk" \
-v "$ANDROID_CMAKE_DIR:/opt/android-sdk-linux/cmake" \
-w /workspace/app \
"$IMAGE" \
sh -lc "flutter pub get && flutter $* && chown -R 1000:1000 /workspace/app"
sh -lc '
set -e
if command -v sdkmanager >/dev/null 2>&1; then
yes | sdkmanager --licenses >/tmp/android-sdk-licenses.log 2>&1 || {
cat /tmp/android-sdk-licenses.log >&2
exit 1
}
fi
if [ -n "${TDC114_ADB_CONNECT_ADDRESS:-}" ]; then
adb connect "$TDC114_ADB_CONNECT_ADDRESS" || true
adb devices
fi
if [ "$1" = "adb-devices" ]; then
exit 0
fi
if [ "$1" = "adb" ]; then
shift
exec adb "$@"
fi
flutter pub get
flutter "$@"
chown -R 1000:1000 /workspace/app
' sh "$MODE" "$@"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOCAL_ADB_BIN="$ROOT_DIR/.tools/android-platform-tools/adb"
ADB_BIN="${TDC114_HOST_ADB_BIN:-}"
if [ -z "$ADB_BIN" ]; then
if [ -x "$LOCAL_ADB_BIN" ]; then
ADB_BIN="$LOCAL_ADB_BIN"
elif command -v adb >/dev/null 2>&1; then
ADB_BIN="$(command -v adb)"
fi
fi
if [ -z "$ADB_BIN" ] || [ ! -x "$ADB_BIN" ]; then
cat >&2 <<EOF
Host adb is not available.
Expected one of:
- $LOCAL_ADB_BIN
- adb in PATH
- TDC114_HOST_ADB_BIN=/path/to/adb
Run scripts/install-host-adb-from-docker.sh once to extract adb from the Flutter Docker image.
EOF
exit 127
fi
exec "$ADB_BIN" "$@"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
IMAGE="${FLUTTER_DOCKER_IMAGE:-ghcr.io/cirruslabs/flutter:stable}"
OUT_DIR="$ROOT_DIR/.tools/android-platform-tools"
mkdir -p "$OUT_DIR"
docker run --rm \
-v "$OUT_DIR:/out" \
"$IMAGE" \
sh -lc '
set -e
adb_path="$(command -v adb)"
cp "$adb_path" /out/adb
chmod 755 /out/adb
'
"$OUT_DIR/adb" version
+159 -1
View File
@@ -3,10 +3,20 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="$ROOT_DIR/app"
LOCAL_ENV_FILE="${TDC114_SMOKE_ENV_FILE:-$ROOT_DIR/scripts/.env.smoke.local}"
ANDROID_PREFLIGHT_SCRIPT="${TDC114_ANDROID_PREFLIGHT_SCRIPT:-$ROOT_DIR/scripts/check-android-device-env.sh}"
if [ -f "$LOCAL_ENV_FILE" ]; then
# shellcheck disable=SC1090
source "$LOCAL_ENV_FILE"
fi
if [ -z "${TDC114_API_BASE:-}" ]; then
echo "TDC114_API_BASE is required for integration tests." >&2
echo "Example: TDC114_API_BASE=https://staging.example.com $0" >&2
if [ -f "$LOCAL_ENV_FILE" ]; then
echo "Loaded local smoke env from $LOCAL_ENV_FILE" >&2
fi
exit 64
fi
@@ -16,4 +26,152 @@ if [ ! -d "$APP_DIR/integration_test" ]; then
exit 2
fi
"$ROOT_DIR/scripts/flutter-docker.sh" test integration_test
if [ -n "${TDC114_ADB_CONNECT_ADDRESS:-}" ] || [ -n "${TDC114_FLUTTER_DEVICE_ID:-}" ] || [ -n "${ADB_SERVER_SOCKET:-}" ]; then
"$ANDROID_PREFLIGHT_SCRIPT"
fi
TEST_ARGS=(
"drive"
"--dart-define=TDC114_API_BASE=${TDC114_API_BASE}"
)
append_optional_define() {
local key="$1"
local value="${!key:-}"
if [ -n "$value" ]; then
TEST_ARGS+=("--dart-define=${key}=${value}")
fi
}
append_optional_define "SSO_BASE_URL"
append_optional_define "TDC114_AUTH_API_BASE"
append_optional_define "TDC114_DIRECTORY_API_BASE"
append_optional_define "TDC114_ORGANIZATION_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_TENANT_SLUG"
append_optional_define "TDC114_BARON_KEY_ID"
append_optional_define "TDC114_BARON_KEY_SECRET"
TEST_DEVICE_ID="${TDC114_FLUTTER_DEVICE_ID:-${TDC114_ADB_CONNECT_ADDRESS:-}}"
if [ -n "$TEST_DEVICE_ID" ]; then
TEST_ARGS+=("-d" "$TEST_DEVICE_ID")
fi
if [ "${TDC114_SMOKE_ASSUME_LOGGED_IN:-}" = "1" ] || [ "${TDC114_SMOKE_ASSUME_LOGGED_IN:-}" = "true" ]; then
TEST_ARGS+=("--dart-define=TDC114_SMOKE_ASSUME_LOGGED_IN=true")
bootstrap_mode="${TDC114_SESSION_BOOTSTRAP_MODE:-mock}"
preauth_exports=""
if [ -z "${TDC114_SMOKE_PHONE:-}" ]; then
TDC114_SMOKE_PHONE=01012345678
fi
if [ "$bootstrap_mode" = "legacy-phone-login" ]; then
bootstrap_api_base="${TDC114_SESSION_BOOTSTRAP_API_BASE:-$TDC114_API_BASE}"
bootstrap_api_base="${bootstrap_api_base//10.0.2.2/127.0.0.1}"
bootstrap_api_base="${bootstrap_api_base//localhost/127.0.0.1}"
login_payload="$(printf '{"phoneNumber":"%s","device":{"platform":"android","appVersion":"integration-smoke","deviceName":"android-bootstrap"}}' "$TDC114_SMOKE_PHONE")"
set +e
login_response="$(
curl -fsS \
-H 'content-type: application/json' \
-d "$login_payload" \
"${bootstrap_api_base%/}/api/v1/tdc114plus/auth/phone-login" 2>/tmp/tdc114plus_integration_bootstrap.err
)"
bootstrap_status=$?
set -e
if [ "$bootstrap_status" -eq 0 ]; then
preauth_exports="$(
python3 -c '
import json, sys
data = json.loads(sys.argv[1])
user = data.get("user") or {}
fields = {
"TDC114_PREAUTH_TOKEN": data.get("token", ""),
"TDC114_PREAUTH_EXPIRES_AT": data.get("expiresAt", ""),
"TDC114_PREAUTH_USER_ID": user.get("id", ""),
"TDC114_PREAUTH_USER_NAME": user.get("name", ""),
"TDC114_PREAUTH_USER_PHONE": user.get("phoneNumber", ""),
"TDC114_PREAUTH_TENANT_ID": user.get("tenantId", ""),
"TDC114_PREAUTH_TENANT_NAME": user.get("tenantName", ""),
"TDC114_PREAUTH_TENANT_SLUG": user.get("tenantSlug", ""),
"TDC114_PREAUTH_DEPARTMENT": user.get("department", ""),
"TDC114_PREAUTH_GRADE": user.get("grade", ""),
"TDC114_PREAUTH_POSITION": user.get("position", ""),
"TDC114_PREAUTH_JOB_TITLE": user.get("jobTitle", ""),
}
for key, value in fields.items():
if value is None:
value = ""
print(f"{key}={value}")
' "$login_response"
)"
else
echo "WARN legacy integration bootstrap phone-login failed; falling back to mock directory mode." >&2
cat /tmp/tdc114plus_integration_bootstrap.err >&2 || true
fi
else
echo "INFO integration session bootstrap uses mock mode by default under the current headless-login policy." >&2
fi
if [ -z "$preauth_exports" ]; then
preauth_exports="$(cat <<'EOF'
TDC114_PREAUTH_TOKEN=mock-session-token
TDC114_PREAUTH_EXPIRES_AT=2099-12-31T23:59:59Z
TDC114_PREAUTH_USER_ID=user-001
TDC114_PREAUTH_USER_NAME=김하늘
TDC114_PREAUTH_USER_PHONE=+821012345678
TDC114_PREAUTH_TENANT_ID=tenant-hanmac
TDC114_PREAUTH_TENANT_NAME=한맥
TDC114_PREAUTH_TENANT_SLUG=hanmac
TDC114_PREAUTH_DEPARTMENT=기술연구소
TDC114_PREAUTH_GRADE=책임
TDC114_PREAUTH_POSITION=팀장
TDC114_PREAUTH_JOB_TITLE=Flutter 개발
EOF
)"
TEST_ARGS+=("--dart-define=TDC114_SMOKE_USE_MOCK_DIRECTORY=true")
fi
while IFS='=' read -r key value; do
[ -n "$key" ] || continue
TEST_ARGS+=("--dart-define=${key}=${value}")
done <<< "$preauth_exports"
fi
if [ -n "${TDC114_SMOKE_PHONE:-}" ]; then
TEST_ARGS+=("--dart-define=TDC114_SMOKE_PHONE=${TDC114_SMOKE_PHONE}")
fi
if [ -n "${TDC114_SMOKE_EXPECTED_NAME:-}" ]; then
TEST_ARGS+=("--dart-define=TDC114_SMOKE_EXPECTED_NAME=${TDC114_SMOKE_EXPECTED_NAME}")
fi
if [ -n "${TDC114_SMOKE_EXPECTED_TENANT_LABEL:-}" ]; then
TEST_ARGS+=("--dart-define=TDC114_SMOKE_EXPECTED_TENANT_LABEL=${TDC114_SMOKE_EXPECTED_TENANT_LABEL}")
fi
TEST_ARGS+=(
"--driver=test_driver/integration_driver.dart"
"--target=integration_test/app_smoke_test.dart"
)
OUTPUT_FILE="$(mktemp)"
trap 'rm -f "$OUTPUT_FILE"' EXIT
set +e
"$ROOT_DIR/scripts/flutter-docker.sh" "${TEST_ARGS[@]}" 2>&1 | tee "$OUTPUT_FILE"
status=$?
set -e
if [ "$status" -ne 0 ] && grep -q "No supported devices connected." "$OUTPUT_FILE"; then
echo >&2
echo "integration test requires a supported runtime target." >&2
echo "Current Flutter project only has Android/iOS runners." >&2
echo "Connect an Android physical device first, or use an emulator fallback, before retrying." >&2
exit 2
fi
exit "$status"
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOCAL_ENV_FILE="${TDC114_SMOKE_ENV_FILE:-$ROOT_DIR/scripts/.env.android-device.local}"
if [ -f "$LOCAL_ENV_FILE" ]; then
# shellcheck disable=SC1090
source "$LOCAL_ENV_FILE"
fi
APP_API_BASE="${TDC114_API_BASE:-}"
if [ -z "$APP_API_BASE" ]; then
echo "TDC114_API_BASE is required." >&2
exit 64
fi
if [ -z "${TDC114_ADB_CONNECT_ADDRESS:-}" ] && [ -z "${ADB_SERVER_SOCKET:-}" ]; then
echo "Either ADB_SERVER_SOCKET or TDC114_ADB_CONNECT_ADDRESS is required." >&2
echo "Example: ADB_SERVER_SOCKET=tcp:172.21.128.1:5037 TDC114_FLUTTER_DEVICE_ID=ce091719b0a418620d7e $0" >&2
exit 64
fi
if [ -n "${ADB_SERVER_SOCKET:-}" ] && [ -z "${TDC114_ADB_CONNECT_ADDRESS:-}" ] && [ -z "${TDC114_FLUTTER_DEVICE_ID:-}" ]; then
echo "TDC114_FLUTTER_DEVICE_ID is required when using ADB_SERVER_SOCKET." >&2
echo "Example: TDC114_FLUTTER_DEVICE_ID=ce091719b0a418620d7e" >&2
exit 64
fi
SMOKE_PHONE="${TDC114_SMOKE_PHONE:-01091365338}"
BOOTSTRAP_API_BASE="${TDC114_SESSION_BOOTSTRAP_API_BASE:-$APP_API_BASE}"
BOOTSTRAP_API_BASE="${BOOTSTRAP_API_BASE//10.0.2.2/127.0.0.1}"
BOOTSTRAP_API_BASE="${BOOTSTRAP_API_BASE//localhost/127.0.0.1}"
DEVICE_ID="${TDC114_FLUTTER_DEVICE_ID:-$TDC114_ADB_CONNECT_ADDRESS}"
SKIP_SESSION_BOOTSTRAP="${TDC114_SKIP_SESSION_BOOTSTRAP:-false}"
tmp_body="$(mktemp)"
tmp_err="$(mktemp)"
trap 'rm -f "$tmp_body" "$tmp_err"' EXIT
DEFINE_ARGS=(
"--dart-define=TDC114_API_BASE=${APP_API_BASE}"
)
append_optional_define() {
local key="$1"
local value="${!key:-}"
if [ -n "$value" ]; then
DEFINE_ARGS+=("--dart-define=${key}=${value}")
fi
}
append_optional_define "SSO_BASE_URL"
append_optional_define "TDC114_AUTH_API_BASE"
append_optional_define "TDC114_DIRECTORY_API_BASE"
append_optional_define "TDC114_ORGANIZATION_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_TENANT_SLUG"
append_optional_define "TDC114_BARON_KEY_ID"
append_optional_define "TDC114_BARON_KEY_SECRET"
if [ "$SKIP_SESSION_BOOTSTRAP" = "1" ] || [ "$SKIP_SESSION_BOOTSTRAP" = "true" ]; then
echo "Skipping session bootstrap; app will open its normal login flow." >&2
else
login_payload="$(printf '{"phoneNumber":"%s","device":{"platform":"android","appVersion":"manual-smoke","deviceName":"android-manual"}}' "$SMOKE_PHONE")"
set +e
http_code="$(
curl -sS \
-o "$tmp_body" \
-w '%{http_code}' \
-H 'content-type: application/json' \
-d "$login_payload" \
"${BOOTSTRAP_API_BASE%/}/api/v1/tdc114plus/auth/phone-login" 2>"$tmp_err"
)"
curl_status=$?
set -e
if [ "$curl_status" -eq 0 ] && [ "$http_code" = "200" ]; then
mapfile -t exported < <(
python3 - "$tmp_body" <<'PY'
import json
import sys
with open(sys.argv[1], 'r', encoding='utf-8') as fh:
data = json.load(fh)
user = data.get("user") or {}
fields = {
"TDC114_PREAUTH_TOKEN": data.get("token", ""),
"TDC114_PREAUTH_EXPIRES_AT": data.get("expiresAt", ""),
"TDC114_PREAUTH_USER_ID": user.get("id", ""),
"TDC114_PREAUTH_USER_NAME": user.get("name", ""),
"TDC114_PREAUTH_USER_PHONE": user.get("phoneNumber", ""),
"TDC114_PREAUTH_TENANT_ID": user.get("tenantId", ""),
"TDC114_PREAUTH_TENANT_NAME": user.get("tenantName", ""),
"TDC114_PREAUTH_TENANT_SLUG": user.get("tenantSlug", ""),
"TDC114_PREAUTH_DEPARTMENT": user.get("department", "") or "",
"TDC114_PREAUTH_GRADE": user.get("grade", "") or "",
"TDC114_PREAUTH_POSITION": user.get("position", "") or "",
"TDC114_PREAUTH_JOB_TITLE": user.get("jobTitle", "") or "",
}
for key, value in fields.items():
print(f"{key}={value}")
PY
)
for entry in "${exported[@]}"; do
DEFINE_ARGS+=("--dart-define=${entry}")
done
echo "Using real local phone-login bootstrap for manual post-login run." >&2
else
echo "WARN local phone-login bootstrap failed; using mock directory fallback." >&2
if [ -s "$tmp_err" ]; then
cat "$tmp_err" >&2
fi
if [ -s "$tmp_body" ]; then
cat "$tmp_body" >&2
fi
DEFINE_ARGS+=(
"--dart-define=TDC114_SMOKE_USE_MOCK_DIRECTORY=true"
"--dart-define=TDC114_PREAUTH_TOKEN=mock-session-token"
"--dart-define=TDC114_PREAUTH_EXPIRES_AT=2099-12-31T23:59:59Z"
"--dart-define=TDC114_PREAUTH_USER_ID=user-001"
"--dart-define=TDC114_PREAUTH_USER_NAME=김하늘"
"--dart-define=TDC114_PREAUTH_USER_PHONE=+821012345678"
"--dart-define=TDC114_PREAUTH_TENANT_ID=tenant-hanmac"
"--dart-define=TDC114_PREAUTH_TENANT_NAME=한맥"
"--dart-define=TDC114_PREAUTH_TENANT_SLUG=hanmac"
"--dart-define=TDC114_PREAUTH_DEPARTMENT=기술연구소"
"--dart-define=TDC114_PREAUTH_GRADE=책임"
"--dart-define=TDC114_PREAUTH_POSITION=팀장"
"--dart-define=TDC114_PREAUTH_JOB_TITLE=Flutter 개발"
)
fi
fi
echo "Launching app on device ${DEVICE_ID} with post-login seed..." >&2
exec "$ROOT_DIR/scripts/flutter-docker.sh" \
run \
-d "$DEVICE_ID" \
"${DEFINE_ARGS[@]}"
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# scripts/shutdown.sh
# Graceful / safe shutdown helper for tdc114plus development environment
# Usage: ./scripts/shutdown.sh [--dry-run] [--auto]
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BARON_DIR="${BARON_SSO_WORKTREE:-/home/ubuntu/workspace/baron-sso-tdc114plus-api}"
LOG_BASE_DIR="${TDC114_LOG_BASE:-$ROOT_DIR/logs}"
LOG_DIR="$LOG_BASE_DIR/$(date +%F)"
LOG_FILE="$LOG_DIR/shutdown.log"
DOCKER_BIN="${DOCKER_BIN:-docker}"
ADB_BIN="${ADB_BIN:-adb}"
ADB_DISCONNECT_ADDRESS="${TDC114_ADB_CONNECT_ADDRESS:-${TDC114_STARTUP_ANDROID_ADDRESS:-}}"
ADB_SERVER_SOCKET_VALUE="${ADB_SERVER_SOCKET:-}"
PKILL_BIN="${PKILL_BIN:-pkill}"
CHMOD_BIN="${CHMOD_BIN:-chmod}"
CHOWN_BIN="${CHOWN_BIN:-chown}"
AUTH_PORT="${TDC114_AUTH_PORT:-5001}"
DRY_RUN=true
AUTO=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--auto) DRY_RUN=false; AUTO=true ;;
--help|-h)
cat <<EOF
Usage: $0 [--dry-run] [--auto]
--dry-run : show commands and write non-destructive logs (default)
--auto : actually perform shutdown and cleanup
--help : show this message
EOF
exit 0
;;
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
esac
done
mkdir -p "$LOG_DIR"
log() {
echo "$*" | tee -a "$LOG_FILE"
}
action() {
log "+ $*"
if [ "$DRY_RUN" = false ]; then
set +e
bash -lc "$*" 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
return "$rc"
fi
}
action_cmd() {
log "+ $*"
if [ "$DRY_RUN" = false ]; then
set +e
"$@" 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
return "$rc"
fi
}
compose_cmd() {
printf "%s compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml %s" \
"$DOCKER_BIN" "$1"
}
log "Shutdown started: $(date)"
# 1) Stop only tdc114plus-related helper processes.
action_cmd "$PKILL_BIN" -f "$ROOT_DIR/scripts/integration_tests.sh" || true
action_cmd "$PKILL_BIN" -f "$ROOT_DIR/scripts/flutter-docker.sh" || true
action "lsof -nP -iTCP:$AUTH_PORT -sTCP:LISTEN -t 2>/dev/null | xargs -r kill || true"
# 2) Disconnect adb only when the current target address is explicit.
if [ -n "$ADB_DISCONNECT_ADDRESS" ]; then
action_cmd env TDC114_ADB_CONNECT_ADDRESS="$ADB_DISCONNECT_ADDRESS" "$ADB_BIN" disconnect "$ADB_DISCONNECT_ADDRESS" || true
else
log "Skipping adb disconnect because TDC114_ADB_CONNECT_ADDRESS is not set."
if [ -n "$ADB_SERVER_SOCKET_VALUE" ]; then
log "ADB_SERVER_SOCKET is in use; physical USB device remains managed by the shared host ADB server."
else
log "No explicit Android target connection was provided for disconnect."
fi
fi
# 3) Save compose logs before bringing the stack down.
action "cd '$BARON_DIR' && $(compose_cmd "logs --no-color") > '$LOG_DIR/baron-compose.log' 2>&1 || true"
# 4) Docker compose down for Baron SSO (safe stop).
action "cd '$BARON_DIR' && $(compose_cmd "down")"
# 5) Optional: force remove old containers (only when --auto).
if [ "$AUTO" = true ]; then
action "$DOCKER_BIN ps -a --format '{{.Names}}' | grep -iE 'baron|ory|tdc114' | xargs -r $DOCKER_BIN rm -f || true"
else
log "(skipping destructive container rm; pass --auto to enable)"
fi
# 6) Restore generated config ownership and permissions when needed.
action "cd '$BARON_DIR' && $CHMOD_BIN -R u+w config/.generated/ 2>/dev/null || true"
action "cd '$BARON_DIR' && $CHOWN_BIN -R \$(id -u):\$(id -g) config/.generated/ 2>/dev/null || true"
log "Shutdown finished: $(date)"
if [ "$DRY_RUN" = true ]; then
log "Dry-run mode: no shutdown actions were executed. Use --auto to run for real."
fi
exit 0
+4
View File
@@ -0,0 +1,4 @@
# Copy this file to scripts/.env.smoke.local for local authenticated smoke runs.
TDC114_API_BASE=http://127.0.0.1:5000
TDC114_SMOKE_PHONE=01012345678
+16
View File
@@ -0,0 +1,16 @@
TDC114_API_BASE=https://sso.hmac.kr
TDC114_ORG_CONTEXT_API_BASE=https://sadmin.hmac.kr
TDC114_ORG_CONTEXT_TENANT_SLUG=hanmac-family
TDC114_BARON_KEY_ID=replace-with-staging-key-id
TDC114_BARON_KEY_SECRET=replace-with-staging-key-secret
TDC114_SMOKE_PHONE=010xxxxxxxx
TDC114_SMOKE_AUTH_FLOW=link
# Optional: set these only when the staging account has stable expected labels.
# TDC114_SMOKE_EXPECTED_NAME=홍길동
# TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3
# Optional split mode:
# TDC114_AUTH_API_BASE=https://sso.hmac.kr
# TDC114_DIRECTORY_API_BASE=https://sadmin.hmac.kr
# TDC114_ORGANIZATION_API_BASE=https://sadmin.hmac.kr
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# Start the local tdc114plus-auth broker with one env file.
# Usage: ./scripts/start-auth-server.sh [--restart] [--env-file=PATH]
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
AUTH_DIR="${TDC114_AUTH_WORKTREE:-/home/ubuntu/workspace/tdc114plus-auth}"
ENV_FILE="$ROOT_DIR/scripts/.env.android-device.local"
NAVER_WORKS_ENV_FILE="${NAVER_WORKS_ENV_FILE:-$ROOT_DIR/secrets/naver_works_service_account.local.env}"
AUTH_GOCACHE="${AUTH_GOCACHE:-/tmp/tdc114plus-auth-gocache}"
LOG_BASE_DIR="${TDC114_LOG_BASE:-$ROOT_DIR/logs}"
LOG_DIR="$LOG_BASE_DIR/$(date +%F)"
LOG_FILE="$LOG_DIR/tdc114plus-auth.log"
PORT="${TDC114_AUTH_PORT:-5001}"
RESTART=false
for arg in "$@"; do
case "$arg" in
--restart) RESTART=true ;;
--env-file=*) ENV_FILE="${arg#*=}" ;;
--help|-h)
cat <<EOF
Usage: $0 [--restart] [--env-file=PATH]
--restart Stop the current listener on the auth port before starting.
--env-file=PATH Read local test values from this env file.
EOF
exit 0
;;
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
esac
done
mkdir -p "$LOG_DIR"
mkdir -p "$AUTH_GOCACHE"
log() {
echo "$*"
}
fail() {
echo "FAIL $*" >&2
exit 1
}
require_file() {
[ -f "$1" ] || fail "missing file: $1"
}
require_dir() {
[ -d "$1" ] || fail "missing directory: $1"
}
listener_pids() {
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null || true
}
wait_for_health() {
local i=1
while [ "$i" -le 20 ]; do
if curl -fsSL --max-time 2 "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
return 0
fi
sleep 0.5
i=$((i + 1))
done
return 1
}
require_dir "$AUTH_DIR"
require_file "$AUTH_DIR/cmd/server/main.go"
require_file "$ENV_FILE"
require_file "$AUTH_DIR/secrets/private_key.pem"
require_file "$AUTH_DIR/secrets/public_key.pem"
current_pids="$(listener_pids)"
if [ -n "$current_pids" ]; then
if [ "$RESTART" = true ]; then
log "Stopping existing auth server on :$PORT"
# shellcheck disable=SC2086
kill $current_pids 2>/dev/null || true
sleep 1
else
if wait_for_health; then
log "tdc114plus-auth is already running on :$PORT"
exit 0
fi
fail "port $PORT is already in use, but health check failed. Run with --restart."
fi
fi
set -a
# shellcheck source=/dev/null
source "$ENV_FILE"
set +a
if [ -f "$NAVER_WORKS_ENV_FILE" ]; then
set -a
# shellcheck source=/dev/null
source "$NAVER_WORKS_ENV_FILE"
set +a
if [ -n "${WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE:-}" ] && [[ "$WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE" != /* ]]; then
WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE="$ROOT_DIR/$WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE"
export WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE
fi
fi
: "${TDC114_ORG_CONTEXT_API_BASE:?TDC114_ORG_CONTEXT_API_BASE is required in $ENV_FILE}"
: "${TDC114_ORG_CONTEXT_TENANT_SLUG:?TDC114_ORG_CONTEXT_TENANT_SLUG is required in $ENV_FILE}"
: "${TDC114_BARON_KEY_ID:?TDC114_BARON_KEY_ID is required in $ENV_FILE}"
: "${TDC114_BARON_KEY_SECRET:?TDC114_BARON_KEY_SECRET is required in $ENV_FILE}"
UPSTREAM_ORG_CONTEXT_BASE="${TDC114_AUTH_UPSTREAM_ORG_CONTEXT_API_BASE:-}"
if [ -z "$UPSTREAM_ORG_CONTEXT_BASE" ]; then
UPSTREAM_ORG_CONTEXT_BASE="$TDC114_ORG_CONTEXT_API_BASE"
fi
log "Starting tdc114plus-auth on :$PORT"
log "Log file: $LOG_FILE"
(
cd "$AUTH_DIR"
setsid env \
GOCACHE="$AUTH_GOCACHE" \
PORT="$PORT" \
AUTH_PROVIDER=baron \
APP_SESSION_SECRET="${APP_SESSION_SECRET:-tdc114plus-dev-session}" \
BARON_BASE_URL="${BARON_BASE_URL:-https://sso.hmac.kr}" \
BARON_CLIENT_ID="${BARON_CLIENT_ID:-243f0b04-a417-4fbe-ae59-a90b16783a79}" \
BARON_OIDC_AUTHORIZATION_URL="${BARON_OIDC_AUTHORIZATION_URL:-https://sso.hmac.kr/oidc/oauth2/auth}" \
BARON_OIDC_TOKEN_URL="${BARON_OIDC_TOKEN_URL:-https://sso.hmac.kr/oidc/oauth2/token}" \
BARON_OIDC_REDIRECT_URI="${BARON_OIDC_REDIRECT_URI:-https://114-auth.hmac.kr/api/v1/auth/oidc/callback}" \
BARON_OIDC_SCOPES="${BARON_OIDC_SCOPES:-openid profile email}" \
BARON_PRIVATE_KEY_PATH="${BARON_PRIVATE_KEY_PATH:-./secrets/private_key.pem}" \
BARON_PUBLIC_KEY_PATH="${BARON_PUBLIC_KEY_PATH:-./secrets/public_key.pem}" \
BARON_JWKS_KID="${BARON_JWKS_KID:-tdc114plus-auth-key-1}" \
BARON_LINK_RETURN_URI="${BARON_LINK_RETURN_URI:-}" \
BARON_ORG_CONTEXT_BASE_URL="$UPSTREAM_ORG_CONTEXT_BASE" \
BARON_ORG_CONTEXT_TENANT_SLUG="$TDC114_ORG_CONTEXT_TENANT_SLUG" \
BARON_ORG_CONTEXT_KEY_ID="$TDC114_BARON_KEY_ID" \
BARON_ORG_CONTEXT_KEY_SECRET="$TDC114_BARON_KEY_SECRET" \
go run ./cmd/server >>"$LOG_FILE" 2>&1 < /dev/null &
)
if ! wait_for_health; then
tail -n 80 "$LOG_FILE" >&2 || true
fail "tdc114plus-auth did not become healthy on :$PORT"
fi
log "tdc114plus-auth ready on :$PORT"
+443
View File
@@ -0,0 +1,443 @@
#!/usr/bin/env bash
# scripts/startup.sh
# Startup helper for tdc114plus development environment
# Usage: ./scripts/startup.sh [--dry-run] [--auto] [--wait=N] [--skip-android-check]
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BARON_DIR="${BARON_SSO_WORKTREE:-/home/ubuntu/workspace/baron-sso-tdc114plus-api}"
LOG_BASE_DIR="${TDC114_LOG_BASE:-$ROOT_DIR/logs}"
LOG_DIR="$LOG_BASE_DIR/$(date +%F)"
LOG_FILE="$LOG_DIR/startup.log"
CHECK_SCRIPT="${TDC114_CHECK_SCRIPT:-$ROOT_DIR/scripts/check-baron-api-env.sh}"
API_SMOKE_SCRIPT="${TDC114_API_SMOKE_SCRIPT:-$ROOT_DIR/scripts/api-smoke.sh}"
DOCKER_BIN="${DOCKER_BIN:-docker}"
MAKE_BIN="${MAKE_BIN:-make}"
CHMOD_BIN="${CHMOD_BIN:-chmod}"
REQUIRE_AUTH_SMOKE="${TDC114_REQUIRE_AUTH_SMOKE:-false}"
ANDROID_PRECHECK_SCRIPT="${TDC114_ANDROID_PRECHECK_SCRIPT:-$ROOT_DIR/scripts/check-android-device-env.sh}"
AUTH_SERVER_SCRIPT="${TDC114_AUTH_SERVER_SCRIPT:-$ROOT_DIR/scripts/start-auth-server.sh}"
START_AUTH_SERVER="${TDC114_START_AUTH_SERVER:-true}"
DAILY_HANDOFF_DIR="${TDC114_DAILY_HANDOFF_DIR:-$ROOT_DIR/docs/daily-issues}"
REQUIRE_DAILY_HANDOFF="${TDC114_REQUIRE_DAILY_HANDOFF:-true}"
WSL_MAINTENANCE_SCRIPT="${TDC114_WSL_MAINTENANCE_SCRIPT:-$ROOT_DIR/scripts/check-wsl-maintenance.sh}"
WSL_MAINTENANCE_ENABLED="${TDC114_WSL_MAINTENANCE_ENABLED:-true}"
DRY_RUN=true
AUTO=false
WAIT=30
RETRIES=6
SLEEP_BETWEEN=10
SKIP_ANDROID_CHECK=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--auto) DRY_RUN=false; AUTO=true ;;
--wait=*) WAIT=${arg#*=} ;;
--skip-android-check|--skip-device-check) SKIP_ANDROID_CHECK=true ;;
--help|-h)
cat <<EOF
Usage: $0 [--dry-run] [--auto] [--wait=N] [--skip-android-check]
--dry-run : show commands and write non-destructive logs (default)
--auto : actually perform startup actions
--wait=N : seconds to wait for services to stabilize (default 30)
--skip-android-check : skip Android target preflight (not recommended)
--help : show this message
EOF
exit 0
;;
--wait) echo "Please use --wait=N" >&2; exit 2 ;;
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
esac
done
case "$WAIT" in
''|*[!0-9]*)
echo "--wait must be a non-negative integer" >&2
exit 2
;;
esac
mkdir -p "$LOG_DIR"
log() {
echo "$*" | tee -a "$LOG_FILE"
}
run_cmd() {
log "+ $*"
if [ "$DRY_RUN" = false ]; then
set +e
bash -lc "$*" 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
return "$rc"
fi
}
run_cmd_capture() {
local output_file="$1"
shift
log "+ $*"
if [ "$DRY_RUN" = false ]; then
set +e
bash -lc "$*" 2>&1 | tee "$output_file" | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
return "$rc"
fi
}
compose_cmd() {
printf "%s compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml %s" \
"$DOCKER_BIN" "$1"
}
ensure_required_paths() {
if [ ! -d "$BARON_DIR" ]; then
log "Startup aborted: Baron worktree not found at $BARON_DIR"
exit 1
fi
if [ ! -x "$CHECK_SCRIPT" ]; then
log "Startup aborted: check script is missing or not executable at $CHECK_SCRIPT"
exit 1
fi
if [ ! -x "$API_SMOKE_SCRIPT" ]; then
log "Startup aborted: api smoke script is missing or not executable at $API_SMOKE_SCRIPT"
exit 1
fi
if [ "$SKIP_ANDROID_CHECK" = false ] && [ ! -x "$ANDROID_PRECHECK_SCRIPT" ]; then
log "Startup aborted: Android precheck script is missing or not executable at $ANDROID_PRECHECK_SCRIPT"
exit 1
fi
if [ "$START_AUTH_SERVER" = true ] && [ ! -x "$AUTH_SERVER_SCRIPT" ]; then
log "Startup aborted: auth server script is missing or not executable at $AUTH_SERVER_SCRIPT"
exit 1
fi
if [ ! -d "$DAILY_HANDOFF_DIR" ]; then
log "Startup aborted: daily handoff directory is missing at $DAILY_HANDOFF_DIR"
exit 1
fi
if [ "$WSL_MAINTENANCE_ENABLED" = true ] && [ ! -x "$WSL_MAINTENANCE_SCRIPT" ]; then
log "Startup aborted: WSL maintenance script is missing or not executable at $WSL_MAINTENANCE_SCRIPT"
exit 1
fi
}
review_daily_handoff() {
local today
local handoff_file
today="$(date +%F)"
handoff_file="$(find "$DAILY_HANDOFF_DIR" -maxdepth 1 -type f -name '20*.md' ! -name "$today*" | sort | tail -n 1 || true)"
if [ -z "$handoff_file" ]; then
log "Daily handoff review: no previous handoff file found in $DAILY_HANDOFF_DIR"
if [ "$REQUIRE_DAILY_HANDOFF" = true ]; then
log "Startup aborted: previous daily handoff is required. Set TDC114_REQUIRE_DAILY_HANDOFF=false only for exceptional recovery."
exit 1
fi
return 0
fi
log "Daily handoff review required before startup"
log "Daily handoff file: $handoff_file"
log "----- daily handoff excerpt start -----"
sed -n '1,160p' "$handoff_file" | tee -a "$LOG_FILE"
log "----- daily handoff excerpt end -----"
}
review_wsl_maintenance() {
if [ "$WSL_MAINTENANCE_ENABLED" != true ]; then
log "WSL maintenance reminder skipped by TDC114_WSL_MAINTENANCE_ENABLED=$WSL_MAINTENANCE_ENABLED"
return 0
fi
log "WSL maintenance review started: $WSL_MAINTENANCE_SCRIPT status"
set +e
(
cd "$ROOT_DIR"
"$WSL_MAINTENANCE_SCRIPT" status
) 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
if [ "$rc" -ne 0 ]; then
log "Startup aborted: WSL maintenance review script failed"
exit 1
fi
}
run_android_precheck() {
if [ "$SKIP_ANDROID_CHECK" = true ]; then
log "Android target preflight skipped by option"
return 0
fi
log "Android target preflight started: $ANDROID_PRECHECK_SCRIPT"
set +e
(
cd "$ROOT_DIR"
"$ANDROID_PRECHECK_SCRIPT"
) 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
if [ "$rc" -ne 0 ]; then
log "Startup paused: Android target preflight did not pass"
log "Resolve the physical-device or ADB steps above, then rerun startup."
exit 1
fi
log "Android target preflight passed"
}
ensure_auth_server() {
if [ "$START_AUTH_SERVER" != true ]; then
log "tdc114plus-auth startup skipped by TDC114_START_AUTH_SERVER=$START_AUTH_SERVER"
return 0
fi
log "tdc114plus-auth startup started: $AUTH_SERVER_SCRIPT --restart"
set +e
(
cd "$ROOT_DIR"
"$AUTH_SERVER_SCRIPT" --restart
) 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
if [ "$rc" -ne 0 ]; then
log "Startup aborted: tdc114plus-auth did not start"
exit 1
fi
log "tdc114plus-auth ready"
}
ensure_generated_config_writable() {
local generated_dir="$BARON_DIR/config/.generated"
run_cmd "mkdir -p '$generated_dir'"
run_cmd "cd '$BARON_DIR' && $CHMOD_BIN -R u+w config/.generated 2>/dev/null || true"
if [ "$DRY_RUN" = false ] && [ ! -w "$generated_dir" ]; then
log "Startup aborted: $generated_dir is not writable by $(id -un). Fix ownership/permissions before startup."
exit 1
fi
}
sanitize_rendered_kratos_config() {
local kratos_config="$BARON_DIR/config/.generated/ory/kratos/kratos.yml"
if [ ! -f "$kratos_config" ]; then
log "Startup aborted: rendered Kratos config is missing at $kratos_config"
exit 1
fi
if grep -Fq " - []" "$kratos_config"; then
log "Rendered Kratos config contains invalid empty allowed_return_urls entry; removing it automatically"
set +e
perl -0pi -e 's/^[ ]*-\s*\[\]\n//mg' "$kratos_config"
local rc=$?
set -e
if [ "$rc" -ne 0 ]; then
log "Startup aborted: failed to sanitize $kratos_config"
exit 1
fi
fi
}
ensure_rendered_ory_files() {
local oathkeeper_entrypoint="$BARON_DIR/config/.generated/ory/oathkeeper/entrypoint.sh"
sanitize_rendered_kratos_config
if [ ! -x "$oathkeeper_entrypoint" ]; then
log "Startup aborted: rendered Oathkeeper entrypoint is missing or not executable at $oathkeeper_entrypoint"
exit 1
fi
}
capture_failure_diagnostics() {
local prefix="${1:-runtime}"
if [ "$DRY_RUN" = true ]; then
return 0
fi
run_cmd "mkdir -p '$LOG_DIR'"
run_cmd "cd '$BARON_DIR' && $(compose_cmd "ps") > '$LOG_DIR/${prefix}-compose-ps.log' 2>&1 || true"
run_cmd "$DOCKER_BIN ps -a --format '{{.Names}} {{.Status}}' | grep -E 'baron|ory' > '$LOG_DIR/${prefix}-docker-ps.log' 2>&1 || true"
run_cmd "$DOCKER_BIN logs --tail 120 ory_kratos > '$LOG_DIR/${prefix}-ory_kratos.log' 2>&1 || true"
run_cmd "$DOCKER_BIN logs --tail 120 ory_stack_check > '$LOG_DIR/${prefix}-ory_stack_check.log' 2>&1 || true"
run_cmd "$DOCKER_BIN logs --tail 120 oathkeeper > '$LOG_DIR/${prefix}-oathkeeper.log' 2>&1 || true"
run_cmd "$DOCKER_BIN logs --tail 120 baron_backend > '$LOG_DIR/${prefix}-baron_backend.log' 2>&1 || true"
}
run_check_script() {
local attempt="$1"
local output_file="$LOG_DIR/check-baron-api-env.attempt-${attempt}.log"
set +e
(
cd "$ROOT_DIR"
"$CHECK_SCRIPT"
) 2>&1 | tee "$output_file" | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
if [ "$rc" -ne 0 ]; then
log "check-baron-api-env.sh failed on attempt $attempt"
return 1
fi
if ! grep -Fq "RESULT ready-ish: 0 failure(s), 0 warning(s)" "$output_file"; then
log "check-baron-api-env.sh reported warnings on attempt $attempt"
return 1
fi
log "check-baron-api-env.sh passed with zero warnings"
return 0
}
run_api_smoke_script() {
local attempt="$1"
local output_file="$LOG_DIR/api-smoke.attempt-${attempt}.log"
local fallback_output_file="$LOG_DIR/api-smoke.base-only.attempt-${attempt}.log"
set +e
(
cd "$ROOT_DIR"
"$API_SMOKE_SCRIPT"
) 2>&1 | tee "$output_file" | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
set -e
if [ "$rc" -ne 0 ]; then
if [ "$REQUIRE_AUTH_SMOKE" != "true" ] && grep -Fq '"code":"login_failed"' "$output_file"; then
log "api-smoke authenticated login failed; retrying in base-smoke-only mode because TDC114_REQUIRE_AUTH_SMOKE is false"
set +e
(
cd "$ROOT_DIR"
TDC114_SKIP_AUTH_SMOKE=true "$API_SMOKE_SCRIPT"
) 2>&1 | tee "$fallback_output_file" | tee -a "$LOG_FILE"
rc=${PIPESTATUS[0]}
set -e
if [ "$rc" -eq 0 ]; then
log "api-smoke base-smoke-only mode passed"
return 0
fi
fi
log "api-smoke.sh failed on attempt $attempt"
return 1
fi
log "api-smoke.sh passed"
return 0
}
start_stack() {
local down_cmd="cd '$BARON_DIR' && $(compose_cmd "down")"
local up_cmd="cd '$BARON_DIR' && $(compose_cmd "up -d")"
local output_file="$LOG_DIR/compose-up.initial.log"
run_cmd_capture "$LOG_DIR/compose-down.initial.log" "$down_cmd" || true
if run_cmd_capture "$output_file" "$up_cmd"; then
return 0
fi
if grep -Eq "name conflict|is already in use by container" "$output_file"; then
log "compose up failed due to stale or conflicting containers; attempting automatic cleanup"
run_cmd "cd '$BARON_DIR' && $(compose_cmd "down") >/dev/null 2>&1 || true"
run_cmd "$DOCKER_BIN ps -a --format '{{.Names}}' | grep -iE 'baron|ory' | xargs -r $DOCKER_BIN rm -f || true"
run_cmd_capture "$LOG_DIR/compose-up.retry.log" "$up_cmd"
return $?
fi
if grep -Eq "dependency failed to start|didn't complete successfully|is unhealthy" "$output_file"; then
log "compose up reported dependency or health issues; continuing to stabilization checks before declaring failure"
return 0
fi
return 1
}
ensure_required_paths
log "Startup started: $(date)"
# 0) Always review the previous daily handoff before starting runtime work.
review_daily_handoff
review_wsl_maintenance
# 1) Verify Android target readiness before backend startup work
run_android_precheck
# 2) Ensure networks and config rendered (if Makefile target exists)
if [ -f "$BARON_DIR/Makefile" ]; then
ensure_generated_config_writable
run_cmd "cd '$BARON_DIR' && $MAKE_BIN ensure-networks || true"
if ! run_cmd "cd '$BARON_DIR' && $MAKE_BIN render-ory-config"; then
log "Startup aborted: make render-ory-config failed"
exit 1
fi
ensure_rendered_ory_files
fi
# 3) Start all necessary compose stacks, retrying once after conflict cleanup
start_stack
# 4) Wait for services to stabilize
run_cmd "sleep $WAIT"
# 5) Show container status
run_cmd "$DOCKER_BIN ps --format '{{.Names}} {{.Status}}' | grep -E 'baron|ory' || true"
# 5-1) Start local auth broker used by the app.
ensure_auth_server
# 6) Run health checks and smoke tests with retries
startup_ok=false
i=1
while [ "$i" -le "$RETRIES" ]; do
log "Health check attempt $i/$RETRIES"
if [ "$DRY_RUN" = false ]; then
if run_check_script "$i" && run_api_smoke_script "$i"; then
startup_ok=true
break
fi
else
log "(dry-run) would run check-baron-api-env.sh and api-smoke.sh"
startup_ok=true
break
fi
i=$((i + 1))
if [ "$i" -le "$RETRIES" ]; then
run_cmd "sleep $SLEEP_BETWEEN"
fi
done
# 7) Save logs
run_cmd "cd '$BARON_DIR' && $(compose_cmd "logs --no-color") > '$LOG_DIR/baron-compose.log' 2>&1 || true"
if [ "$startup_ok" = false ]; then
capture_failure_diagnostics "startup-failure"
log "Startup failed: health checks did not pass after $RETRIES attempt(s)"
exit 1
fi
log "Startup finished: $(date)"
if [ "$DRY_RUN" = true ]; then
log "Dry-run mode: no actions were actually performed. Use --auto to run for real."
fi
exit 0
+441
View File
@@ -0,0 +1,441 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TMP_ROOT="$(mktemp -d)"
trap 'rm -rf "$TMP_ROOT"' EXIT
PASS_COUNT=0
say() {
printf '%s\n' "$*"
}
fail() {
printf 'FAIL %s\n' "$*" >&2
exit 1
}
assert_file_contains() {
local file="$1"
local pattern="$2"
if ! grep -Fq "$pattern" "$file"; then
fail "expected '$pattern' in $file"
fi
}
assert_not_exists_or_empty() {
local file="$1"
if [ -e "$file" ] && [ -s "$file" ]; then
fail "expected empty or missing file: $file"
fi
}
new_fixture() {
local name="$1"
local dir="$TMP_ROOT/$name"
mkdir -p "$dir/bin" "$dir/baron/config/.generated/ory/kratos" "$dir/baron/config/.generated/ory/oathkeeper" "$dir/logs" "$dir/state"
touch "$dir/baron/Makefile"
cat > "$dir/baron/config/.generated/ory/kratos/kratos.yml" <<'EOF'
selfservice:
allowed_return_urls:
- http://localhost:5000
- []
EOF
cat > "$dir/baron/config/.generated/ory/oathkeeper/entrypoint.sh" <<'EOF'
#!/usr/bin/env sh
exit 0
EOF
cat > "$dir/bin/docker" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
log_file="${TEST_CMD_LOG:?}"
printf 'docker %s\n' "$*" >> "$log_file"
case "$*" in
"compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml up -d")
if [ "${TEST_DOCKER_UP_FAIL_ONCE:-0}" = "1" ] && [ ! -f "${TEST_STATE_DIR:?}/docker-up.failed" ]; then
touch "${TEST_STATE_DIR}/docker-up.failed"
echo "name conflict"
exit 1
fi
if [ "${TEST_DOCKER_UP_HEALTH_FAILURE:-0}" = "1" ] && [ ! -f "${TEST_STATE_DIR:?}/docker-up.health-failed" ]; then
touch "${TEST_STATE_DIR}/docker-up.health-failed"
echo "dependency failed to start: container baron_backend is unhealthy"
exit 1
fi
echo "compose up ok"
;;
"compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml down")
echo "compose down ok"
;;
"compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml logs --no-color")
echo "fake compose logs"
;;
"ps --format {{.Names}} {{.Status}}")
printf 'baron_backend Up (healthy)\nory_kratos Up\n'
;;
"ps -a --format {{.Names}}")
printf 'old_baron_backend\nold_ory_kratos\n'
;;
"ps --format {{.Names}}")
printf 'baron_backend\nbaron_gateway\n'
;;
"rm -f old_baron_backend old_ory_kratos")
echo "removed stale containers"
;;
*)
;;
esac
EOF
cat > "$dir/bin/make" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'make %s\n' "$*" >> "${TEST_CMD_LOG:?}"
EOF
cat > "$dir/bin/adb" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'adb %s\n' "$*" >> "${TEST_CMD_LOG:?}"
EOF
cat > "$dir/bin/pkill" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'pkill %s\n' "$*" >> "${TEST_CMD_LOG:?}"
EOF
cat > "$dir/bin/chmod" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'chmod %s\n' "$*" >> "${TEST_CMD_LOG:?}"
EOF
cat > "$dir/bin/chown" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'chown %s\n' "$*" >> "${TEST_CMD_LOG:?}"
EOF
cat > "$dir/check.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
state_dir="${TEST_STATE_DIR:?}"
attempt_file="$state_dir/check-attempts"
count=0
if [ -f "$attempt_file" ]; then
count="$(cat "$attempt_file")"
fi
count=$((count + 1))
printf '%s' "$count" > "$attempt_file"
mode="${CHECK_MODE:-pass}"
case "$mode" in
pass)
echo "RESULT ready-ish: 0 failure(s), 0 warning(s)"
;;
warn_then_pass)
if [ "$count" -eq 1 ]; then
echo "RESULT ready-ish: 0 failure(s), 1 warning(s)"
else
echo "RESULT ready-ish: 0 failure(s), 0 warning(s)"
fi
;;
fail)
echo "RESULT blocked: 1 failure(s), 0 warning(s)"
exit 1
;;
*)
echo "unknown CHECK_MODE=$mode" >&2
exit 2
;;
esac
EOF
cat > "$dir/api-smoke.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
state_dir="${TEST_STATE_DIR:?}"
attempt_file="$state_dir/api-attempts"
count=0
if [ -f "$attempt_file" ]; then
count="$(cat "$attempt_file")"
fi
count=$((count + 1))
printf '%s' "$count" > "$attempt_file"
mode="${API_MODE:-pass}"
case "$mode" in
pass)
echo "PASS api smoke"
;;
login_failed)
if [ "${TDC114_SKIP_AUTH_SMOKE:-false}" = "true" ]; then
echo "SKIP authenticated smoke: TDC114_SKIP_AUTH_SMOKE=true"
exit 0
fi
echo 'FAIL phone-login success: HTTP 401' >&2
echo 'Response body:' >&2
echo '{"error":"Login failed","code":"login_failed"}' >&2
exit 1
;;
fail)
echo "FAIL api smoke" >&2
exit 1
;;
*)
echo "unknown API_MODE=$mode" >&2
exit 2
;;
esac
EOF
cat > "$dir/wsl-maintenance.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
mode="${1:-status}"
case "$mode" in
status)
echo "${TEST_WSL_MAINTENANCE_OUTPUT:-WSL maintenance reminder: OK}"
;;
*)
echo "unsupported mode: $mode" >&2
exit 2
;;
esac
EOF
chmod +x "$dir/bin/docker" "$dir/bin/make" "$dir/bin/adb" "$dir/bin/pkill" \
"$dir/bin/chmod" "$dir/bin/chown" "$dir/check.sh" "$dir/api-smoke.sh" \
"$dir/wsl-maintenance.sh" "$dir/baron/config/.generated/ory/oathkeeper/entrypoint.sh"
printf '%s\n' "$dir"
}
run_case() {
local name="$1"
shift
say "CASE $name"
"$@"
PASS_COUNT=$((PASS_COUNT + 1))
}
test_startup_dry_run() {
local fixture
fixture="$(new_fixture startup-dry-run)"
local cmd_log="$fixture/commands.log"
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_CHECK_SCRIPT="$fixture/check.sh" \
TDC114_API_SMOKE_SCRIPT="$fixture/api-smoke.sh" \
TDC114_WSL_MAINTENANCE_SCRIPT="$fixture/wsl-maintenance.sh" \
TDC114_START_AUTH_SERVER=false \
DOCKER_BIN="$fixture/bin/docker" \
MAKE_BIN="$fixture/bin/make" \
bash "$ROOT_DIR/scripts/startup.sh" --dry-run --wait=0 --skip-android-check
assert_not_exists_or_empty "$cmd_log"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "WSL maintenance reminder: OK"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "Dry-run mode: no actions were actually performed"
}
test_startup_auto_retry_success() {
local fixture
fixture="$(new_fixture startup-auto-success)"
local cmd_log="$fixture/commands.log"
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
TEST_DOCKER_UP_FAIL_ONCE=1 \
CHECK_MODE=warn_then_pass \
API_MODE=pass \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_CHECK_SCRIPT="$fixture/check.sh" \
TDC114_API_SMOKE_SCRIPT="$fixture/api-smoke.sh" \
TDC114_WSL_MAINTENANCE_SCRIPT="$fixture/wsl-maintenance.sh" \
TDC114_START_AUTH_SERVER=false \
DOCKER_BIN="$fixture/bin/docker" \
MAKE_BIN="$fixture/bin/make" \
bash "$ROOT_DIR/scripts/startup.sh" --auto --wait=0 --skip-android-check
assert_file_contains "$cmd_log" "docker rm -f old_baron_backend old_ory_kratos"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "check-baron-api-env.sh reported warnings on attempt 1"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "check-baron-api-env.sh passed with zero warnings"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "api-smoke.sh passed"
if grep -Fq " - []" "$fixture/baron/config/.generated/ory/kratos/kratos.yml"; then
fail "expected startup to sanitize invalid empty allowed_return_urls entry"
fi
}
test_startup_auto_health_failure_tolerated() {
local fixture
fixture="$(new_fixture startup-auto-health-failure)"
local cmd_log="$fixture/commands.log"
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
TEST_DOCKER_UP_HEALTH_FAILURE=1 \
CHECK_MODE=pass \
API_MODE=pass \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_CHECK_SCRIPT="$fixture/check.sh" \
TDC114_API_SMOKE_SCRIPT="$fixture/api-smoke.sh" \
TDC114_WSL_MAINTENANCE_SCRIPT="$fixture/wsl-maintenance.sh" \
TDC114_START_AUTH_SERVER=false \
DOCKER_BIN="$fixture/bin/docker" \
MAKE_BIN="$fixture/bin/make" \
bash "$ROOT_DIR/scripts/startup.sh" --auto --wait=0 --skip-android-check
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "compose up reported dependency or health issues; continuing to stabilization checks before declaring failure"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "api-smoke.sh passed"
}
test_startup_login_failed_falls_back_to_base_smoke() {
local fixture
fixture="$(new_fixture startup-login-fallback)"
local cmd_log="$fixture/commands.log"
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
CHECK_MODE=pass \
API_MODE=login_failed \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_CHECK_SCRIPT="$fixture/check.sh" \
TDC114_API_SMOKE_SCRIPT="$fixture/api-smoke.sh" \
TDC114_WSL_MAINTENANCE_SCRIPT="$fixture/wsl-maintenance.sh" \
TDC114_START_AUTH_SERVER=false \
DOCKER_BIN="$fixture/bin/docker" \
MAKE_BIN="$fixture/bin/make" \
bash "$ROOT_DIR/scripts/startup.sh" --auto --wait=0 --skip-android-check
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "api-smoke authenticated login failed; retrying in base-smoke-only mode because TDC114_REQUIRE_AUTH_SMOKE is false"
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "api-smoke base-smoke-only mode passed"
}
test_startup_auto_failure() {
local fixture
fixture="$(new_fixture startup-auto-failure)"
local cmd_log="$fixture/commands.log"
set +e
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
CHECK_MODE=fail \
API_MODE=pass \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_CHECK_SCRIPT="$fixture/check.sh" \
TDC114_API_SMOKE_SCRIPT="$fixture/api-smoke.sh" \
TDC114_WSL_MAINTENANCE_SCRIPT="$fixture/wsl-maintenance.sh" \
TDC114_START_AUTH_SERVER=false \
DOCKER_BIN="$fixture/bin/docker" \
MAKE_BIN="$fixture/bin/make" \
bash "$ROOT_DIR/scripts/startup.sh" --auto --wait=0 --skip-android-check
local rc=$?
set -e
if [ "$rc" -eq 0 ]; then
fail "startup auto failure case should exit non-zero"
fi
assert_file_contains "$fixture/logs/$(date +%F)/startup.log" "Startup failed: health checks did not pass"
}
test_wsl_maintenance_schedule() {
local fixture
fixture="$(new_fixture wsl-maintenance-schedule)"
local state_dir="$fixture/state"
TDC114_WSL_MAINTENANCE_STATE_DIR="$state_dir" \
TDC114_WSL_MAINTENANCE_ANCHOR_DATE=2026-07-16 \
TDC114_WSL_MAINTENANCE_TODAY=2026-07-20 \
bash "$ROOT_DIR/scripts/check-wsl-maintenance.sh" status > "$fixture/status-not-due.txt"
assert_file_contains "$fixture/status-not-due.txt" "WSL maintenance reminder: OK"
assert_file_contains "$fixture/status-not-due.txt" "Next due date: 2026-07-30"
TDC114_WSL_MAINTENANCE_STATE_DIR="$state_dir" \
TDC114_WSL_MAINTENANCE_ANCHOR_DATE=2026-07-16 \
TDC114_WSL_MAINTENANCE_TODAY=2026-07-30 \
bash "$ROOT_DIR/scripts/check-wsl-maintenance.sh" status > "$fixture/status-due.txt"
assert_file_contains "$fixture/status-due.txt" "WSL maintenance reminder: DUE"
assert_file_contains "$fixture/status-due.txt" "./scripts/check-wsl-maintenance.sh record"
TDC114_WSL_MAINTENANCE_STATE_DIR="$state_dir" \
TDC114_WSL_MAINTENANCE_TODAY=2026-07-30 \
bash "$ROOT_DIR/scripts/check-wsl-maintenance.sh" record > "$fixture/record.txt"
assert_file_contains "$fixture/record.txt" "Recorded WSL maintenance date: 2026-07-30"
TDC114_WSL_MAINTENANCE_STATE_DIR="$state_dir" \
TDC114_WSL_MAINTENANCE_ANCHOR_DATE=2026-07-16 \
TDC114_WSL_MAINTENANCE_TODAY=2026-08-05 \
bash "$ROOT_DIR/scripts/check-wsl-maintenance.sh" status > "$fixture/status-after-record.txt"
assert_file_contains "$fixture/status-after-record.txt" "WSL maintenance reminder: OK"
assert_file_contains "$fixture/status-after-record.txt" "Base date: 2026-07-30"
}
test_shutdown_dry_run() {
local fixture
fixture="$(new_fixture shutdown-dry-run)"
local cmd_log="$fixture/commands.log"
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_ADB_CONNECT_ADDRESS=172.21.128.1:5562 \
DOCKER_BIN="$fixture/bin/docker" \
ADB_BIN="$fixture/bin/adb" \
PKILL_BIN="$fixture/bin/pkill" \
CHMOD_BIN="$fixture/bin/chmod" \
CHOWN_BIN="$fixture/bin/chown" \
bash "$ROOT_DIR/scripts/shutdown.sh" --dry-run
assert_not_exists_or_empty "$cmd_log"
assert_file_contains "$fixture/logs/$(date +%F)/shutdown.log" "Dry-run mode: no shutdown actions were executed"
}
test_shutdown_auto_order_and_scope() {
local fixture
fixture="$(new_fixture shutdown-auto)"
local cmd_log="$fixture/commands.log"
TEST_CMD_LOG="$cmd_log" \
TEST_STATE_DIR="$fixture/state" \
BARON_SSO_WORKTREE="$fixture/baron" \
TDC114_LOG_BASE="$fixture/logs" \
TDC114_ADB_CONNECT_ADDRESS=172.21.128.1:5562 \
DOCKER_BIN="$fixture/bin/docker" \
ADB_BIN="$fixture/bin/adb" \
PKILL_BIN="$fixture/bin/pkill" \
CHMOD_BIN="$fixture/bin/chmod" \
CHOWN_BIN="$fixture/bin/chown" \
bash "$ROOT_DIR/scripts/shutdown.sh" --auto
assert_file_contains "$cmd_log" "pkill -f $ROOT_DIR/scripts/integration_tests.sh"
assert_file_contains "$cmd_log" "pkill -f $ROOT_DIR/scripts/flutter-docker.sh"
assert_file_contains "$cmd_log" "adb disconnect 172.21.128.1:5562"
assert_file_contains "$cmd_log" "docker compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml logs --no-color"
assert_file_contains "$cmd_log" "docker compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml down"
assert_file_contains "$cmd_log" "docker rm -f old_baron_backend old_ory_kratos"
assert_file_contains "$cmd_log" "chmod -R u+w config/.generated/"
assert_file_contains "$cmd_log" "chown -R"
local logs_line
local down_line
logs_line="$(grep -n "docker compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml logs --no-color" "$cmd_log" | head -n1 | cut -d: -f1)"
down_line="$(grep -n "docker compose -f docker-compose.yaml -f compose.infra.yaml -f compose.ory.yaml down" "$cmd_log" | head -n1 | cut -d: -f1)"
if [ -z "$logs_line" ] || [ -z "$down_line" ] || [ "$logs_line" -ge "$down_line" ]; then
fail "expected compose logs collection before compose down"
fi
}
run_case "startup dry-run" test_startup_dry_run
run_case "startup auto retry success" test_startup_auto_retry_success
run_case "startup auto health failure tolerated" test_startup_auto_health_failure_tolerated
run_case "startup login failure falls back" test_startup_login_failed_falls_back_to_base_smoke
run_case "startup auto failure" test_startup_auto_failure
run_case "wsl maintenance schedule" test_wsl_maintenance_schedule
run_case "shutdown dry-run" test_shutdown_dry_run
run_case "shutdown auto order and scope" test_shutdown_auto_order_and_scope
say "PASS $PASS_COUNT case(s)"