diff --git a/.dockerignore b/.dockerignore index 60640f516..8fc469dd1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,7 +11,8 @@ **/.vscode *.env* !/.env -# Streamlit ignores +# Generated local deployment credentials +**/.skyvern **/secrets*.toml # Skyvern @@ -25,4 +26,4 @@ skyvern-frontend/node_modules .dockerignore .gitignore -Dockerfile \ No newline at end of file +Dockerfile diff --git a/.gitignore b/.gitignore index 21047c4b9..b3f08ff9e 100644 --- a/.gitignore +++ b/.gitignore @@ -172,7 +172,8 @@ postgres-data files/ temp/ -# Streamlit ignores +# Generated local deployment credentials +.skyvern/ **/secrets*.toml ## Frontend diff --git a/.streamlit/config.toml b/.streamlit/config.toml deleted file mode 100644 index d465886db..000000000 --- a/.streamlit/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[theme] - -# The preset Streamlit theme that your custom theme inherits from. -# One of "light" or "dark". -base = "dark" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ff6f99ac8..f16df0201 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,8 @@ services: - ./videos:/data/videos - ./har:/data/har - ./log:/data/log - - ./.streamlit:/app/.streamlit + # Generated credentials allow the UI to pick up the local API key on first startup. + - ./.skyvern:/app/.skyvern # Uncomment the following two lines if you want to connect to any local changes # - ./skyvern:/app/skyvern # - ./alembic:/app/alembic @@ -130,10 +131,11 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD", "test", "-f", "/app/.streamlit/secrets.toml"] + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/v1/heartbeat', timeout=5)"] interval: 5s timeout: 5s - retries: 5 + retries: 24 + start_period: 180s skyvern-ui: image: public.ecr.aws/skyvern/skyvern-ui:latest restart: on-failure @@ -144,7 +146,8 @@ services: - ./artifacts:/data/artifacts - ./videos:/data/videos - ./har:/data/har - - ./.streamlit:/app/.streamlit + # Generated credentials allow the UI to pick up the local API key on first startup. + - ./.skyvern:/app/.skyvern # User secrets and frontend config live in skyvern-frontend/.env # (loaded via env_file). No inline environment for the UI service. env_file: diff --git a/docs/developers/self-hosted/docker.mdx b/docs/developers/self-hosted/docker.mdx index bcb28d3c2..3321fd40c 100644 --- a/docs/developers/self-hosted/docker.mdx +++ b/docs/developers/self-hosted/docker.mdx @@ -56,7 +56,7 @@ Copy the frontend environment file: cp skyvern-frontend/.env.example skyvern-frontend/.env ``` -The default values work for local development. You'll update `VITE_SKYVERN_API_KEY` after the first startup. +The default values work for local development. Docker Compose mounts the generated local credentials into the UI container, so you can leave `VITE_SKYVERN_API_KEY` as `YOUR_API_KEY` for the first startup. ### 4. Start the services @@ -84,10 +84,10 @@ All three services should show `healthy` in the STATUS column. The `skyvern` con Once healthy, retrieve your API key: ```bash -cat .streamlit/secrets.toml +cat .skyvern/credentials.toml ``` -This file is auto-generated by Skyvern on first startup. The `.streamlit` path is a legacy artifact. The credentials inside are standard Skyvern API keys. +This file is auto-generated by Skyvern on first startup. The credentials inside are standard Skyvern API keys. You'll see output like: @@ -100,21 +100,7 @@ configs = [ The `host` value uses the Docker-internal hostname `skyvern`. From your machine, use `http://localhost:8000` instead. You only need the `cred` value. This is your API key. -### 6. Update frontend configuration - -Add your API key to the frontend environment: - -```bash skyvern-frontend/.env -VITE_SKYVERN_API_KEY=eyJhbGciOiJIUzI1... -``` - -Restart the UI to pick up the change: - -```bash -docker compose restart skyvern-ui -``` - -### 7. Verify the installation +### 6. Verify the installation Open [http://localhost:8080](http://localhost:8080) in your browser. You should see the Skyvern dashboard. @@ -162,7 +148,7 @@ Docker Compose mounts several directories for persistent storage: | `./videos` | `/data/videos` | Browser session recordings | | `./har` | `/data/har` | HTTP Archive files for debugging | | `./log` | `/data/log` | Application logs | -| `./.streamlit` | `/app/.streamlit` | Generated API credentials | +| `./.skyvern` | `/app/.skyvern` | Generated API credentials | --- @@ -280,7 +266,7 @@ This deletes all data including task history, credentials, and recordings. ```bash docker compose down -v -rm -rf postgres-data artifacts videos har log .streamlit +rm -rf postgres-data artifacts videos har log .skyvern docker compose up -d ``` @@ -342,7 +328,7 @@ Common causes: The API key is missing, malformed, or doesn't match the organization. Verify: 1. The `x-api-key` header is included in your request -2. The key matches exactly what's in `.streamlit/secrets.toml` +2. The key matches exactly what's in `.skyvern/credentials.toml` 3. No extra whitespace or newlines in the key ```bash @@ -359,9 +345,9 @@ The API key format is invalid (JWT decode failed). This usually means: Regenerate credentials by resetting the installation: ```bash -rm -rf .streamlit +rm -rf .skyvern docker compose restart skyvern -cat .streamlit/secrets.toml # Get new key +cat .skyvern/credentials.toml # Get new key ``` @@ -371,7 +357,7 @@ Check that `skyvern-frontend/.env` has the correct values: ```bash VITE_API_BASE_URL=http://localhost:8000/api/v1 VITE_WSS_BASE_URL=ws://localhost:8000/api/v1 -VITE_SKYVERN_API_KEY= +VITE_SKYVERN_API_KEY= ``` After updating, restart the UI: diff --git a/docs/developers/self-hosted/kubernetes.mdx b/docs/developers/self-hosted/kubernetes.mdx index 016e17b0b..ce7a127a8 100644 --- a/docs/developers/self-hosted/kubernetes.mdx +++ b/docs/developers/self-hosted/kubernetes.mdx @@ -198,7 +198,7 @@ The backend pod takes 1-2 minutes to become ready as it runs database migrations Wait for the backend pod to show `1/1` in the `READY` column of `kubectl get pods -n skyvern` before running this command. The API key file is generated during startup and won't exist until the pod is ready. ```bash -kubectl exec -n skyvern deployment/skyvern-backend -- cat /app/.streamlit/secrets.toml +kubectl exec -n skyvern deployment/skyvern-backend -- cat /app/.skyvern/credentials.toml ``` Copy the `cred` value and update `frontend/frontend-secrets.yaml`: @@ -434,7 +434,7 @@ This removes all resources in the `skyvern` namespace. To clean up host storage (if using hostPath): ```bash -rm -rf /data/artifacts /data/videos /data/har /data/log /app/.streamlit +rm -rf /data/artifacts /data/videos /data/har /data/log /app/.skyvern ``` --- diff --git a/entrypoint-skyvern.sh b/entrypoint-skyvern.sh index 59fd1d0be..0e97184fc 100755 --- a/entrypoint-skyvern.sh +++ b/entrypoint-skyvern.sh @@ -62,13 +62,15 @@ if [ "$run_migration" = true ]; then alembic check fi -if [ ! -f ".streamlit/secrets.toml" ]; then +SKYVERN_CREDENTIALS_FILE="${SKYVERN_CREDENTIALS_FILE:-/app/.skyvern/credentials.toml}" +mkdir -p "$(dirname "$SKYVERN_CREDENTIALS_FILE")" + +if [ ! -f "$SKYVERN_CREDENTIALS_FILE" ]; then echo "Creating organization and API token..." org_output=$(python scripts/create_organization.py Skyvern-Open-Source) api_token=$(echo "$org_output" | awk '/token=/{gsub(/.*token='\''|'\''.*/, ""); print}') - # Update the secrets-open-source.toml file - echo -e "[skyvern]\nconfigs = [\n {\"env\" = \"local\", \"host\" = \"http://skyvern:8000/api/v1\", \"orgs\" = [{name=\"Skyvern\", cred=\"$api_token\"}]}\n]" > .streamlit/secrets.toml - echo ".streamlit/secrets.toml file updated with organization details." + echo -e "[skyvern]\nconfigs = [\n {\"env\" = \"local\", \"host\" = \"http://skyvern:8000/api/v1\", \"orgs\" = [{name=\"Skyvern\", cred=\"$api_token\"}]}\n]" > "$SKYVERN_CREDENTIALS_FILE" + echo "$SKYVERN_CREDENTIALS_FILE file updated with organization details." fi _kill_xvfb_on_term() { diff --git a/entrypoint-skyvernui.sh b/entrypoint-skyvernui.sh index a9850c3b8..6deefae86 100644 --- a/entrypoint-skyvernui.sh +++ b/entrypoint-skyvernui.sh @@ -8,18 +8,19 @@ VITE_WSS_BASE_URL="${VITE_WSS_BASE_URL:-ws://localhost:8000/api/v1}" VITE_ARTIFACT_API_BASE_URL="${VITE_ARTIFACT_API_BASE_URL:-http://localhost:9090}" # Priority for VITE_SKYVERN_API_KEY: -# 1. .streamlit/secrets.toml (generated by the backend on first run) -# 2. Environment variable (from .env file or docker-compose environment), +# 1. Environment variable (from .env file or docker-compose environment), # but only if it looks like a real key (not a placeholder) -SECRETS_KEY=$(sed -n 's/.*cred\s*=\s*"\([^"]*\)".*/\1/p' .streamlit/secrets.toml 2>/dev/null || echo "") -if [ -n "$SECRETS_KEY" ]; then - VITE_SKYVERN_API_KEY="$SECRETS_KEY" - echo "Using VITE_SKYVERN_API_KEY from .streamlit/secrets.toml" -elif [ -n "$VITE_SKYVERN_API_KEY" ] && [ "$VITE_SKYVERN_API_KEY" != "YOUR_API_KEY" ]; then +# 2. Generated credentials file from the backend first-run setup +SKYVERN_CREDENTIALS_FILE="${SKYVERN_CREDENTIALS_FILE:-/app/.skyvern/credentials.toml}" +GENERATED_KEY=$(sed -n 's/.*cred\s*=\s*"\([^"]*\)".*/\1/p' "$SKYVERN_CREDENTIALS_FILE" 2>/dev/null || echo "") +if [ -n "$VITE_SKYVERN_API_KEY" ] && [ "$VITE_SKYVERN_API_KEY" != "YOUR_API_KEY" ]; then VITE_SKYVERN_API_KEY=$(echo "$VITE_SKYVERN_API_KEY" | xargs) echo "Using VITE_SKYVERN_API_KEY from environment variable" +elif [ -n "$GENERATED_KEY" ]; then + VITE_SKYVERN_API_KEY="$GENERATED_KEY" + echo "Using VITE_SKYVERN_API_KEY from $SKYVERN_CREDENTIALS_FILE" else - echo "WARNING: No VITE_SKYVERN_API_KEY found in .streamlit/secrets.toml or environment" + echo "WARNING: No VITE_SKYVERN_API_KEY found in environment or $SKYVERN_CREDENTIALS_FILE" VITE_SKYVERN_API_KEY="" fi @@ -37,4 +38,3 @@ find /app/dist -name "*.js" -exec sed -i \ node localServer.js & node artifactServer.js & wait - diff --git a/kubernetes-deployment/backend/backend-deployment.yaml b/kubernetes-deployment/backend/backend-deployment.yaml index ed55bae10..2c495c79e 100644 --- a/kubernetes-deployment/backend/backend-deployment.yaml +++ b/kubernetes-deployment/backend/backend-deployment.yaml @@ -31,11 +31,12 @@ spec: mountPath: /data/har - name: log mountPath: /data/log - - name: streamlit - mountPath: /app/.streamlit + - name: credentials + mountPath: /app/.skyvern readinessProbe: - exec: - command: ["test", "-f", "/app/.streamlit/secrets.toml"] + httpGet: + path: /api/v1/heartbeat + port: 8000 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 5 @@ -57,7 +58,7 @@ spec: hostPath: path: /data/log type: DirectoryOrCreate - - name: streamlit + - name: credentials hostPath: - path: /app/.streamlit + path: /app/.skyvern type: DirectoryOrCreate diff --git a/kubernetes-deployment/frontend/frontend-deployment.yaml b/kubernetes-deployment/frontend/frontend-deployment.yaml index 58cd8e60c..9abddd3bf 100644 --- a/kubernetes-deployment/frontend/frontend-deployment.yaml +++ b/kubernetes-deployment/frontend/frontend-deployment.yaml @@ -29,8 +29,8 @@ spec: mountPath: /data/videos - name: har mountPath: /data/har - - name: streamlit - mountPath: /app/.streamlit + - name: credentials + mountPath: /app/.skyvern volumes: - name: artifacts hostPath: @@ -44,7 +44,7 @@ spec: hostPath: path: /data/har type: DirectoryOrCreate - - name: streamlit + - name: credentials hostPath: - path: /app/.streamlit + path: /app/.skyvern type: DirectoryOrCreate diff --git a/skyvern/cli/doctor.py b/skyvern/cli/doctor.py index ce04ddacb..f074968ac 100644 --- a/skyvern/cli/doctor.py +++ b/skyvern/cli/doctor.py @@ -7,6 +7,7 @@ import importlib.util import json import os import platform +import re import shutil import socket import subprocess @@ -25,6 +26,9 @@ from skyvern.cli.console import console doctor_app = typer.Typer(help="Check Skyvern installation health.") +GENERATED_CREDENTIALS_FILE = Path(".skyvern/credentials.toml") +LEGACY_STREAMLIT_CREDENTIALS_FILE = Path(".streamlit/secrets.toml") + @dataclass class CheckResult: @@ -460,9 +464,27 @@ def _check_llm_config() -> CheckResult: ) +def _read_credential_file(path: Path) -> str: + if not path.exists(): + return "" + m = re.search(r'(? str: + return _read_credential_file(GENERATED_CREDENTIALS_FILE) + + +def _read_legacy_streamlit_credential() -> str: + return _read_credential_file(LEGACY_STREAMLIT_CREDENTIALS_FILE) + + def _check_api_key_consistency() -> CheckResult: - """Check that API keys are consistent across backend .env, frontend .env, and secrets.toml.""" - import re + """Check that local API keys are consistent. + + Generated Docker credentials are only a fallback for compose startup; backend .env + remains the preferred source when present. + """ from dotenv import dotenv_values @@ -470,35 +492,34 @@ def _check_api_key_consistency() -> CheckResult: backend_env = resolve_backend_env_path() frontend_env = Path("skyvern-frontend/.env") - secrets_toml = Path(".streamlit/secrets.toml") backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else "" frontend_raw = dotenv_values(frontend_env).get("VITE_SKYVERN_API_KEY", "") if frontend_env.exists() else "" frontend_key = "" if frontend_raw in ("", "YOUR_API_KEY") else frontend_raw + generated_key = _read_generated_credential() + legacy_key = _read_legacy_streamlit_credential() - secrets_key = "" - if secrets_toml.exists(): - m = re.search(r'cred\s*=\s*"([^"]*)"', secrets_toml.read_text()) - if m: - secrets_key = m.group(1) - - canonical = secrets_key or backend_key + # Docker Compose can inject the generated credentials file into the UI + # without requiring VITE_SKYVERN_API_KEY in skyvern-frontend/.env. + # Legacy installs may only have the API key in the old compatibility file; + # treat it as a migration source. + canonical = backend_key or generated_key or legacy_key if not canonical: return CheckResult( name="API Key Consistency", status="warn", - detail="No API key found in backend .env or .streamlit/secrets.toml", + detail="No API key found in backend .env or generated credentials", hint="Run `skyvern init` or `skyvern quickstart` to generate an API key", ) mismatches: list[str] = [] - if backend_key and backend_key != canonical: - mismatches.append("backend .env differs from secrets.toml") + if not backend_key and not generated_key: + mismatches.append("SKYVERN_API_KEY not set in backend .env") if not frontend_env.exists(): mismatches.append("skyvern-frontend/.env missing") - elif not frontend_key: + elif not frontend_key and not generated_key: mismatches.append("VITE_SKYVERN_API_KEY not set in frontend .env") - elif frontend_key != canonical: + elif frontend_key and frontend_key != canonical: mismatches.append("frontend .env differs from backend") if mismatches: @@ -509,7 +530,51 @@ def _check_api_key_consistency() -> CheckResult: hint="Run `skyvern doctor --fix` to sync API keys", ) - return CheckResult(name="API Key Consistency", status="ok", detail="Keys consistent across all config files") + return CheckResult(name="API Key Consistency", status="ok", detail="Backend and frontend API keys are consistent") + + +def _check_legacy_streamlit_secrets() -> CheckResult: + if not LEGACY_STREAMLIT_CREDENTIALS_FILE.exists(): + return CheckResult(name="Legacy Streamlit Secrets", status="ok", detail="not present") + + from dotenv import dotenv_values + + from skyvern.utils.env_paths import resolve_backend_env_path + + backend_env = resolve_backend_env_path() + backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else "" + legacy_key = _read_legacy_streamlit_credential() + + if not legacy_key: + return CheckResult( + name="Legacy Streamlit Secrets", + status="warn", + detail=".streamlit/secrets.toml exists but no cred value was found", + hint="Remove the file, or run `skyvern doctor --fix` to remove the deprecated file", + ) + + if not backend_key: + return CheckResult( + name="Legacy Streamlit Secrets", + status="warn", + detail=".streamlit/secrets.toml has a legacy API key but backend .env is missing SKYVERN_API_KEY", + hint="Run `skyvern doctor --fix` to migrate the key into .env and skyvern-frontend/.env", + ) + + if legacy_key != backend_key: + return CheckResult( + name="Legacy Streamlit Secrets", + status="warn", + detail=".streamlit/secrets.toml is deprecated and differs from backend .env", + hint="Run `skyvern doctor --fix` to remove the deprecated file", + ) + + return CheckResult( + name="Legacy Streamlit Secrets", + status="warn", + detail=".streamlit/secrets.toml matches backend .env; deprecated compatibility file only", + hint="Run `skyvern doctor --fix` to remove the deprecated file", + ) def _redact_password(db_string: str) -> str: @@ -576,6 +641,7 @@ _CHECKS = [ _check_docker, _check_llm_config, _check_api_key_consistency, + _check_legacy_streamlit_secrets, _check_playwright_browser, _check_port_8000, _check_api_connectivity, @@ -603,6 +669,8 @@ def _try_fix(result: CheckResult) -> bool: return _fix_install_playwright() if result.name == "API Key Consistency" and result.status == "error": return _fix_api_key_consistency() + if result.name == "Legacy Streamlit Secrets" and result.status == "warn": + return _fix_legacy_streamlit_secrets() if result.name == "Docker" and "not running" in result.detail: console.print(" [yellow]→ Please start Docker Desktop manually[/yellow]") return False @@ -671,8 +739,6 @@ def _fix_start_postgres() -> bool: def _fix_api_key_consistency() -> bool: - import re - from dotenv import dotenv_values, set_key from skyvern.utils.env_paths import resolve_backend_env_path @@ -680,23 +746,22 @@ def _fix_api_key_consistency() -> bool: backend_env = resolve_backend_env_path() frontend_env = Path("skyvern-frontend/.env") frontend_example = Path("skyvern-frontend/.env.example") - secrets_toml = Path(".streamlit/secrets.toml") backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else "" - secrets_key = "" - if secrets_toml.exists(): - m = re.search(r'cred\s*=\s*"([^"]*)"', secrets_toml.read_text()) - if m: - secrets_key = m.group(1) + generated_key = _read_generated_credential() + legacy_key = _read_legacy_streamlit_credential() - canonical = secrets_key or backend_key + canonical = backend_key or generated_key or legacy_key if not canonical: console.print(" [yellow]→ No source API key found to sync from[/yellow]") return False - if not frontend_env.exists() and frontend_example.exists(): - import shutil + if not backend_env.exists(): + backend_env.touch() + set_key(str(backend_env), "SKYVERN_API_KEY", canonical) + + if not frontend_env.exists() and frontend_example.exists(): shutil.copy(frontend_example, frontend_env) console.print(" [cyan]Created skyvern-frontend/.env from .env.example[/cyan]") @@ -705,8 +770,51 @@ def _fix_api_key_consistency() -> bool: return False set_key(str(frontend_env), "VITE_SKYVERN_API_KEY", canonical) - source = "secrets.toml" if secrets_key else "backend .env" - console.print(f" [green]✅ Synced VITE_SKYVERN_API_KEY in skyvern-frontend/.env (from {source})[/green]") + source = ( + "backend .env" + if backend_key + else ".skyvern/credentials.toml" + if generated_key + else "legacy .streamlit/secrets.toml" + ) + console.print(f" [green]✅ Synced local API key across backend and frontend (from {source})[/green]") + return True + + +def _fix_legacy_streamlit_secrets() -> bool: + """Best-effort migration for old .streamlit/secrets.toml installs.""" + from dotenv import dotenv_values, set_key + + from skyvern.utils.env_paths import resolve_backend_env_path + + backend_env = resolve_backend_env_path() + frontend_env = Path("skyvern-frontend/.env") + frontend_example = Path("skyvern-frontend/.env.example") + backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else "" + legacy_key = _read_legacy_streamlit_credential() + + if not LEGACY_STREAMLIT_CREDENTIALS_FILE.exists(): + return False + + if not legacy_key: + console.print( + " [yellow]→ Legacy .streamlit/secrets.toml has no cred value; leaving it for inspection[/yellow]" + ) + return False + + if not backend_key and legacy_key: + if not backend_env.exists(): + backend_env.touch() + set_key(str(backend_env), "SKYVERN_API_KEY", legacy_key) + if not frontend_env.exists() and frontend_example.exists(): + shutil.copy(frontend_example, frontend_env) + if frontend_env.exists(): + set_key(str(frontend_env), "VITE_SKYVERN_API_KEY", legacy_key) + console.print(" [green]✅ Migrated legacy .streamlit API key into .env files[/green]") + + LEGACY_STREAMLIT_CREDENTIALS_FILE.unlink() + console.print(" [green]✅ Removed deprecated .streamlit/secrets.toml[/green]") + return True diff --git a/skyvern/config.py b/skyvern/config.py index 4d2c2e490..8f11297b1 100644 --- a/skyvern/config.py +++ b/skyvern/config.py @@ -711,7 +711,7 @@ class Settings(BaseSettings): def execute_all_steps(self) -> bool: """ - This provides the functionality to execute steps one by one through the Streamlit UI. + This provides the functionality to execute steps one by one through the local UI. ***Value is always True if ENV is not local.*** :return: True if env is not local, else the value of EXECUTE_ALL_STEPS diff --git a/tests/unit/test_cli_doctor.py b/tests/unit/test_cli_doctor.py new file mode 100644 index 000000000..029e5c2fb --- /dev/null +++ b/tests/unit/test_cli_doctor.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from skyvern.cli import doctor + + +def _prepare_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + monkeypatch.chdir(tmp_path) + frontend = tmp_path / "skyvern-frontend" + frontend.mkdir() + (frontend / ".env.example").write_text("VITE_SKYVERN_API_KEY=YOUR_API_KEY\n") + return tmp_path + + +def _write_legacy_secret(tmp_path: Path, body: str) -> Path: + legacy = tmp_path / ".streamlit" / "secrets.toml" + legacy.parent.mkdir() + legacy.write_text(body) + return legacy + + +def test_legacy_streamlit_check_is_ok_when_file_missing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _prepare_workspace(tmp_path, monkeypatch) + + result = doctor._check_legacy_streamlit_secrets() + + assert result.status == "ok" + assert result.detail == "not present" + + +def test_legacy_streamlit_fix_preserves_unparseable_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _prepare_workspace(tmp_path, monkeypatch) + legacy = _write_legacy_secret(tmp_path, '[general]\nnot_cred = "keep-me"\n') + + result = doctor._check_legacy_streamlit_secrets() + + assert result.status == "warn" + assert "no cred value" in result.detail + assert doctor._fix_legacy_streamlit_secrets() is False + assert legacy.exists() + + +def test_legacy_streamlit_fix_migrates_only_parseable_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _prepare_workspace(tmp_path, monkeypatch) + legacy = _write_legacy_secret(tmp_path, '[general]\ncred = "legacy-key"\n') + + result = doctor._check_legacy_streamlit_secrets() + + assert result.status == "warn" + assert "backend .env is missing" in result.detail + assert doctor._fix_legacy_streamlit_secrets() is True + assert not legacy.exists() + assert "SKYVERN_API_KEY='legacy-key'" in (tmp_path / ".env").read_text() + assert "VITE_SKYVERN_API_KEY='legacy-key'" in (tmp_path / "skyvern-frontend" / ".env").read_text() + + +def test_legacy_streamlit_fix_removes_matching_deprecated_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _prepare_workspace(tmp_path, monkeypatch) + legacy = _write_legacy_secret(tmp_path, '[general]\ncred = "same-key"\n') + (tmp_path / ".env").write_text('SKYVERN_API_KEY="same-key"\n') + + result = doctor._check_legacy_streamlit_secrets() + + assert result.status == "warn" + assert "deprecated compatibility file" in result.detail + assert doctor._fix_legacy_streamlit_secrets() is True + assert not legacy.exists()