diff --git a/README.md b/README.md index 429cdce360..2c9f562949 100644 --- a/README.md +++ b/README.md @@ -471,6 +471,7 @@ such a checkout, use `bash ./scripts/.sh ...`. make setup-sandbox ``` Reads the configured sandbox image from UTF-8 `config.yaml`, with or without a leading BOM, using LF or CRLF line endings. + On macOS, a successful Apple Container pull completes this step even when Docker is not installed. If Docker is available, its image is also pulled. 4. **Start services**: ```bash diff --git a/backend/tests/test_setup_sandbox.py b/backend/tests/test_setup_sandbox.py index caab50078f..5ba7ea84ab 100644 --- a/backend/tests/test_setup_sandbox.py +++ b/backend/tests/test_setup_sandbox.py @@ -1,6 +1,7 @@ """Regression coverage for sandbox image selection before pulling.""" import os +import shutil import subprocess from pathlib import Path @@ -43,3 +44,59 @@ def test_setup_sandbox_image_selection(tmp_path, encoding, configured, newline): else: assert args[1] != image assert f"Using default image: {args[1]}" in result.stdout + + +@pytest.mark.skipif(os.name == "nt", reason="isolated POSIX PATH simulates macOS runtime discovery") +@pytest.mark.parametrize( + ("apple_exit", "docker_available", "configured", "expected_exit", "expected_engines"), + [ + pytest.param(0, False, True, 0, ["container"], id="apple-only-success"), + pytest.param(0, False, False, 0, ["container"], id="apple-only-default-image-note"), + pytest.param(1, True, True, 0, ["container", "docker"], id="apple-failure-docker-fallback"), + pytest.param(None, False, True, 1, [], id="no-engines"), + pytest.param(0, True, True, 0, ["container", "docker"], id="both-engines"), + ], +) +def test_setup_sandbox_runtime_selection(tmp_path, apple_exit, docker_available, configured, expected_exit, expected_engines): + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + # Do not inherit the host PATH: an installed Docker must not mask the bug + # or be invoked by a test. All engine commands below are local stubs. + (bin_dir / "bash").symlink_to(BASH) + for tool in ("sed", "grep", "awk", "head"): + executable = shutil.which(tool) + if executable is None: + pytest.skip(f"shell-script test requires {tool}") + (bin_dir / tool).symlink_to(executable) + scripts = {"uname": "#!/usr/bin/env bash\nprintf 'Darwin\\n'\n"} + if apple_exit is not None: + scripts["container"] = f'#!/usr/bin/env bash\nprintf "%s\\n" container "$@" >> "$CAPTURE_ENGINE_ARGS"\nexit {apple_exit}\n' + if docker_available: + scripts["docker"] = '#!/usr/bin/env bash\nprintf "%s\\n" docker "$@" >> "$CAPTURE_ENGINE_ARGS"\n' + for name, script in scripts.items(): + path = bin_dir / name + path.write_text(script, encoding="utf-8") + path.chmod(0o755) + + image = "example.invalid/custom-sandbox:review" + if configured: + (tmp_path / "config.yaml").write_text(f"sandbox:\n image: {image}\n", encoding="utf-8") + else: + image = "enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:1.11.0" + capture = tmp_path / "engine-args.txt" + env = {**os.environ, "PATH": str(bin_dir), "CAPTURE_ENGINE_ARGS": str(capture)} + result = subprocess.run([BASH, str(REPO_ROOT / "scripts/setup-sandbox.sh")], cwd=tmp_path, env=env, capture_output=True, text=True, check=False) + + assert result.returncode == expected_exit, result.stdout + result.stderr + args = capture.read_text(encoding="utf-8").splitlines() if capture.exists() else [] + expected_args = [] + for engine in expected_engines: + expected_args.extend([engine, "image", "pull", image] if engine == "container" else [engine, "pull", image]) + assert args == expected_args + if expected_exit == 0: + assert "Sandbox image pulled successfully" in result.stdout + assert "Neither Docker nor Apple Container is available" not in result.stdout + else: + assert "Neither Docker nor Apple Container is available" in result.stdout + if not configured: + assert "NOTE: pulling this image does not make the sandbox use it." in result.stdout diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index f4f20c6d7f..122447bb4b 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -8,6 +8,10 @@ because it runs before dependency synchronization. Read UTF-8 config files with or without a leading BOM so the first section remains detectable. `setup-sandbox.sh` also strips the leading BOM and normalizes CRLF before selecting the image; keep its shell filter compatible with GNU and BSD sed. +An Apple Container pull that succeeds on macOS must not fail the setup step +just because Docker is absent. Keep the Docker pull when Docker is available, +including after an Apple Container failure, and retain the final image-config +note rather than exiting early on Apple Container success. The root `PORT` value configures Docker's published nginx ingress only; local orchestration pins Next.js to `3000`. Runtime commands launch from the already diff --git a/scripts/setup-sandbox.sh b/scripts/setup-sandbox.sh index 9c13813936..a664760dfc 100755 --- a/scripts/setup-sandbox.sh +++ b/scripts/setup-sandbox.sh @@ -31,9 +31,15 @@ fi echo "" +APPLE_PULL_SUCCEEDED=0 if command -v container >/dev/null 2>&1 && [ "$(uname)" = "Darwin" ]; then echo "Detected Apple Container on macOS, pulling image..." - container image pull "$IMAGE" || echo "⚠ Apple Container pull failed, will try Docker" + if container image pull "$IMAGE"; then + APPLE_PULL_SUCCEEDED=1 + echo "✓ Sandbox image pulled successfully using Apple Container" + else + echo "⚠ Apple Container pull failed, will try Docker" + fi fi if command -v docker >/dev/null 2>&1; then @@ -45,7 +51,7 @@ if command -v docker >/dev/null 2>&1; then echo "" echo "⚠ Failed to pull sandbox image (this is OK for local sandbox mode)" fi -else +elif [ "$APPLE_PULL_SUCCEEDED" -eq 0 ]; then echo "✗ Neither Docker nor Apple Container is available" echo " Please install Docker: https://docs.docker.com/get-docker/" exit 1