#!/usr/bin/env bash # # Bare VM Tests (lifecycle, exec, shell, file I/O, observability) # # Part of the smolvm test suite. Run with: ./tests/test_machine_bare.sh # source "$(dirname "$0")/common.sh" init_smolvm log_info "Pre-flight cleanup: killing orphan processes..." kill_orphan_smolvm_processes trap cleanup_machine EXIT echo "" echo "==========================================" echo " Bare VM Tests (lifecycle, exec, shell, file I/O, observability)" echo "==========================================" echo "" test_machine_start() { cleanup_machine $SMOLVM machine start 2>&1 } test_machine_stop() { ensure_machine_running $SMOLVM machine stop 2>&1 } test_machine_status_running() { ensure_machine_running local status status=$($SMOLVM machine status 2>&1) [[ "$status" == *"running"* ]] } test_machine_status_stopped() { cleanup_machine local status exit_code=0 status=$($SMOLVM machine status 2>&1) || exit_code=$? # When stopped, status command either: # - Returns non-zero exit code, OR # - Returns status containing "not running" or "stopped" [[ $exit_code -ne 0 ]] || [[ "$status" == *"not running"* ]] || [[ "$status" == *"stopped"* ]] } test_machine_start_stop_cycle() { cleanup_machine # Start $SMOLVM machine start 2>&1 || return 1 # Verify running local status exit_code=0 status=$($SMOLVM machine status 2>&1) || exit_code=$? if [[ $exit_code -ne 0 ]] || [[ "$status" != *"running"* ]]; then return 1 fi # Stop $SMOLVM machine stop 2>&1 || return 1 # Verify stopped - either non-zero exit or status message indicates stopped exit_code=0 status=$($SMOLVM machine status 2>&1) || exit_code=$? [[ $exit_code -ne 0 ]] || [[ "$status" == *"not running"* ]] || [[ "$status" == *"stopped"* ]] } test_machine_exec() { ensure_machine_running local output output=$($SMOLVM machine exec -- cat /etc/os-release 2>&1) [[ "$output" == *"Alpine"* ]] } test_machine_exec_echo() { ensure_machine_running local output output=$($SMOLVM machine exec -- echo "test-marker-xyz" 2>&1) [[ "$output" == *"test-marker-xyz"* ]] } test_machine_exec_binary_output_preserved() { ensure_machine_running # Fetch first 4 bytes of /bin/busybox — the ELF magic. # Pipe through xxd to render as hex so we're comparing ASCII strings # (bash can't easily compare binary blobs, but the agent→client→CLI # path is what we're exercising; the xxd happens host-side after the # bytes are already through the protocol). local hex hex=$($SMOLVM machine exec -- head -c 4 /bin/busybox 2>&1 | xxd -p | tr -d '\n') # ELF magic: 7f 45 4c 46 (.ELF) # If the 0x7f byte was dropped/replaced, we'd see "454c46" or "efbfbd454c46". [[ "$hex" == "7f454c46" ]] || { echo "expected ELF magic '7f454c46', got '$hex' — binary output corrupted" return 1 } } test_machine_exec_exit_code() { ensure_machine_running # Test exit 0 $SMOLVM machine exec -- sh -c "exit 0" 2>&1 || return 1 # Test exit 1 local exit_code=0 $SMOLVM machine exec -- sh -c "exit 1" 2>&1 || exit_code=$? [[ $exit_code -eq 1 ]] } test_machine_exec_failed_does_not_kill_vm() { ensure_machine_running # Nonexistent binary — should fail but VM stays alive local exit_code=0 $SMOLVM machine exec -- /nonexistent_binary_xyz 2>&1 || exit_code=$? [[ $exit_code -ne 0 ]] || { echo "expected failure for nonexistent binary"; return 1; } # VM must still be running local status status=$($SMOLVM machine status 2>&1) [[ "$status" == *"running"* ]] || { echo "VM died after failed exec: $status"; return 1; } # Next exec must succeed local output output=$($SMOLVM machine exec -- echo "survived-failed-exec" 2>&1) [[ "$output" == *"survived-failed-exec"* ]] || { echo "exec after failure returned: $output"; return 1; } # Empty string command — should fail but VM stays alive exit_code=0 $SMOLVM machine exec -- "" 2>&1 || exit_code=$? [[ $exit_code -ne 0 ]] || { echo "expected failure for empty command"; return 1; } status=$($SMOLVM machine status 2>&1) [[ "$status" == *"running"* ]] || { echo "VM died after empty command exec: $status"; return 1; } # Final verification output=$($SMOLVM machine exec -- echo "still-alive" 2>&1) [[ "$output" == *"still-alive"* ]] } test_sigterm_during_exec_does_not_stall_vm() { local name="bug12-sigterm-$$" $SMOLVM machine stop --name "$name" 2>/dev/null || true $SMOLVM machine delete --name "$name" -f 2>/dev/null || true $SMOLVM machine create --name "$name" 2>&1 | tail -1 || return 1 $SMOLVM machine start --name "$name" 2>&1 | tail -1 || { $SMOLVM machine delete --name "$name" -f 2>/dev/null; return 1 } wait_vm_ready --name "$name" || { $SMOLVM machine delete --name "$name" -f 2>/dev/null; return 1 } # Start a long-running exec, then SIGTERM the client mid-flight. $SMOLVM machine exec --name "$name" -- sh -c 'sleep 30' & local client_pid=$! # Wait long enough for the exec to have registered with the agent (vsock round-trip). sleep 1 kill -TERM "$client_pid" 2>/dev/null wait "$client_pid" 2>/dev/null # Give the agent's 10ms poll loop a moment to detect the disconnect sleep 1 # VM must still be reachable. Before the fix, state would be "unreachable" # and the next exec would take ~30s. Time the exec to detect the stall. local t_start t_end elapsed t_start=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s) local result result=$($SMOLVM machine exec --name "$name" -- echo "survived" 2>&1) t_end=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s) elapsed=$(python3 -c "print(f'{$t_end - $t_start:.1f}')" 2>/dev/null || echo "?") echo " Next exec after SIGTERM: ${elapsed}s" [[ "$result" == *"survived"* ]] || { echo "FAIL: exec after SIGTERM failed: $result" $SMOLVM machine stop --name "$name" 2>/dev/null $SMOLVM machine delete --name "$name" -f 2>/dev/null return 1 } # Must be fast — the old path took ~30s while the orphan sleep finished. local over_threshold over_threshold=$(python3 -c "print('yes' if $t_end - $t_start > 5 else 'no')" 2>/dev/null || echo "no") if [[ "$over_threshold" == "yes" ]]; then echo "FAIL: next exec took ${elapsed}s (>5s) — agent stalled on orphan child?" $SMOLVM machine stop --name "$name" 2>/dev/null $SMOLVM machine delete --name "$name" -f 2>/dev/null return 1 fi $SMOLVM machine stop --name "$name" 2>/dev/null || true $SMOLVM machine delete --name "$name" -f 2>/dev/null || true } test_exec_timeout_does_not_stall_vm() { local name="bug20-timeout-$$" $SMOLVM machine stop --name "$name" 2>/dev/null || true $SMOLVM machine delete --name "$name" -f 2>/dev/null || true $SMOLVM machine create --name "$name" 2>&1 | tail -1 || return 1 $SMOLVM machine start --name "$name" 2>&1 | tail -1 || { $SMOLVM machine delete --name "$name" -f 2>/dev/null; return 1 } wait_vm_ready --name "$name" || { $SMOLVM machine delete --name "$name" -f 2>/dev/null; return 1 } # Short timeout, long-running command with sub-processes — timeout should # kill the whole tree without stalling the agent. local result result=$($SMOLVM machine exec --name "$name" --timeout 2s -- sh -c 'sleep 30' 2>&1) # Exit code 124 = timeout, expected behavior (don't assert — just note) # Next exec must be fast. If the agent stalled, this will take ~30s. local t_start t_end elapsed t_start=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s) result=$($SMOLVM machine exec --name "$name" -- echo "alive" 2>&1) t_end=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s) elapsed=$(python3 -c "print(f'{$t_end - $t_start:.1f}')" 2>/dev/null || echo "?") echo " Next exec after timeout: ${elapsed}s" [[ "$result" == *"alive"* ]] || { echo "FAIL: exec after timeout failed: $result" $SMOLVM machine stop --name "$name" 2>/dev/null $SMOLVM machine delete --name "$name" -f 2>/dev/null return 1 } local over_threshold over_threshold=$(python3 -c "print('yes' if $t_end - $t_start > 5 else 'no')" 2>/dev/null || echo "no") if [[ "$over_threshold" == "yes" ]]; then echo "FAIL: next exec took ${elapsed}s (>5s) — agent stalled after timeout?" $SMOLVM machine stop --name "$name" 2>/dev/null $SMOLVM machine delete --name "$name" -f 2>/dev/null return 1 fi $SMOLVM machine stop --name "$name" 2>/dev/null || true $SMOLVM machine delete --name "$name" -f 2>/dev/null || true } test_machine_named_vm() { local vm_name="test-vm-named" # Clean up any existing $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true # Create the named VM first $SMOLVM machine create --name "$vm_name" 2>&1 || return 1 # Start $SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; } # Check status local status status=$($SMOLVM machine status --name "$vm_name" 2>&1) if [[ "$status" != *"running"* ]]; then $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 fi # Stop and delete $SMOLVM machine stop --name "$vm_name" 2>&1 $SMOLVM machine delete --name "$vm_name" -f 2>&1 ensure_data_dir_deleted "$vm_name" } test_machine_create_prints_named_start_hint() { local vm_name="create-hint-test-$$" local output $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true output=$($SMOLVM machine create --name "$vm_name" 2>&1) || return 1 [[ "$output" == *"Use 'smolvm machine start --name $vm_name' to start the machine"* ]] || { echo "$output" $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } $SMOLVM machine delete --name "$vm_name" -f 2>&1 ensure_data_dir_deleted "$vm_name" } test_machine_exec_when_stopped() { cleanup_machine local exit_code=0 $SMOLVM machine exec -- echo "should-fail" 2>&1 || exit_code=$? # Should fail with non-zero exit code (don't check specific message) [[ $exit_code -ne 0 ]] } test_bare_vm_workspace() { ensure_machine_running local output output=$($SMOLVM machine exec -- ls -d /workspace 2>&1) [[ "$output" == *"/workspace"* ]] || { echo "FAIL: /workspace missing on bare VM"; return 1; } # Write and read back $SMOLVM machine exec -- sh -c 'echo ws-bare > /workspace/bare.txt' 2>&1 || return 1 output=$($SMOLVM machine exec -- cat /workspace/bare.txt 2>&1) [[ "$output" == *"ws-bare"* ]] } test_file_upload_download() { local vm_name="cp-test-$$" # Create and start a machine $SMOLVM machine create --name "$vm_name" 2>&1 || return 1 run_with_timeout 30 $SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1 } # Upload a file local upload_content="hello from host $(date +%s)" echo "$upload_content" > /tmp/smolvm-cp-test.txt $SMOLVM machine cp /tmp/smolvm-cp-test.txt "$vm_name":/tmp/uploaded.txt 2>&1 || { echo "Upload failed" $SMOLVM machine stop --name "$vm_name" 2>/dev/null $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null rm -f /tmp/smolvm-cp-test.txt return 1 } # Verify upload via exec local exec_result exec_result=$($SMOLVM machine exec --name "$vm_name" -- cat /tmp/uploaded.txt 2>&1) || { echo "Exec after upload failed" $SMOLVM machine stop --name "$vm_name" 2>/dev/null $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null rm -f /tmp/smolvm-cp-test.txt return 1 } [[ "$exec_result" == *"$upload_content"* ]] || { echo "Upload content mismatch: expected '$upload_content', got '$exec_result'" $SMOLVM machine stop --name "$vm_name" 2>/dev/null $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null rm -f /tmp/smolvm-cp-test.txt return 1 } # Create file in VM and download $SMOLVM machine exec --name "$vm_name" -- sh -c "echo 'hello from VM' > /tmp/to-download.txt" 2>&1 $SMOLVM machine cp "$vm_name":/tmp/to-download.txt /tmp/smolvm-downloaded.txt 2>&1 || { echo "Download failed" $SMOLVM machine stop --name "$vm_name" 2>/dev/null $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null rm -f /tmp/smolvm-cp-test.txt /tmp/smolvm-downloaded.txt return 1 } local downloaded downloaded=$(cat /tmp/smolvm-downloaded.txt) [[ "$downloaded" == *"hello from VM"* ]] || { echo "Download content mismatch: '$downloaded'" $SMOLVM machine stop --name "$vm_name" 2>/dev/null $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null rm -f /tmp/smolvm-cp-test.txt /tmp/smolvm-downloaded.txt return 1 } # Cleanup $SMOLVM machine stop --name "$vm_name" 2>/dev/null $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null rm -f /tmp/smolvm-cp-test.txt /tmp/smolvm-downloaded.txt } test_streaming_exec() { # Start a machine, run a command with --stream, verify output arrives $SMOLVM machine stop 2>/dev/null || true $SMOLVM machine create --name stream-test-$$ 2>&1 || return 1 run_with_timeout 30 $SMOLVM machine start --name stream-test-$$ 2>&1 || { $SMOLVM machine delete --name stream-test-$$ -f 2>/dev/null; return 1 } # Streaming exec — output should contain the echoed text local result result=$(run_with_timeout 15 $SMOLVM machine exec --stream --name stream-test-$$ -- sh -c "echo 'stream-line-1' && echo 'stream-line-2' && echo 'done'" 2>&1) [[ $? -eq 124 ]] && { echo "TIMEOUT"; $SMOLVM machine stop --name stream-test-$$ 2>/dev/null; $SMOLVM machine delete --name stream-test-$$ -f 2>/dev/null; return 1; } [[ "$result" == *"stream-line-1"* ]] && [[ "$result" == *"stream-line-2"* ]] && [[ "$result" == *"done"* ]] || { echo "Missing streaming output: $result" $SMOLVM machine stop --name stream-test-$$ 2>/dev/null $SMOLVM machine delete --name stream-test-$$ -f 2>/dev/null return 1 } # Cleanup $SMOLVM machine stop --name stream-test-$$ 2>/dev/null $SMOLVM machine delete --name stream-test-$$ -f 2>/dev/null } test_agent_json_logs() { local vm_name="observability-test-$$" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true $SMOLVM machine create --name "$vm_name" 2>&1 || return 1 $SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; } # Run a command to generate agent log entries $SMOLVM machine exec --name "$vm_name" -- echo "observability-test" 2>&1 || true # Find the console log using the platform-aware vm_data_dir helper local data_dir data_dir=$(vm_data_dir "$vm_name") local console_log="${data_dir}/agent-console.log" # Copy the log before stopping (stop/delete may remove the data dir) local saved_log saved_log=$(mktemp) cp "$console_log" "$saved_log" 2>/dev/null || true $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true if [[ ! -s "$saved_log" ]]; then echo "Console log not found or empty at $console_log" rm -f "$saved_log" return 1 fi # Agent should write JSON — verify at least one line parses as JSON local json_lines json_lines=$(grep -c '^{' "$saved_log" 2>/dev/null || echo "0") if [[ "$json_lines" -eq 0 ]]; then echo "No JSON lines in console log ($console_log)" rm -f "$saved_log" return 1 fi # Verify a tracing-formatted JSON line has expected structured fields. # Skip early boot_log lines (target=smolvm_agent::boot) which use a # simpler format without timestamps — they run before tracing is initialized. local first_json first_json=$(grep '^{' "$saved_log" | grep '"timestamp"' | head -1) rm -f "$saved_log" if [[ -z "$first_json" ]]; then echo "No tracing JSON lines with timestamp found in console log" return 1 fi echo "$first_json" | python3 -c " import sys, json line = json.load(sys.stdin) assert 'timestamp' in line, 'missing timestamp' assert 'level' in line, 'missing level' " 2>&1 || { echo "JSON log missing structured fields: $first_json"; return 1; } } test_machine_shell() { ensure_machine_running # shell opens an interactive PTY. Pipe "echo X; exit" through it. # Use a subshell with a watchdog kill to avoid hanging if the PTY blocks. local tmpout tmpout=$(mktemp) (echo "echo shell-test-ok; exit" | $SMOLVM machine shell > "$tmpout" 2>&1) & local pid=$! sleep 5 kill $pid 2>/dev/null; wait $pid 2>/dev/null local output output=$(cat "$tmpout") rm -f "$tmpout" [[ "$output" == *"shell-test-ok"* ]] || { echo "FAIL: expected shell-test-ok, got: $output" return 1 } } test_exec_large_stdout_does_not_crash_vm() { ensure_machine_running "true" # Generate 128KB of output — well above the ~64KB pipe buffer local output output=$(run_with_timeout 30 $SMOLVM machine exec -- sh -c 'dd if=/dev/urandom bs=1024 count=128 2>/dev/null | base64' 2>&1) local exit_code=$? [[ $exit_code -eq 124 ]] && { echo "FAIL: timed out (pipe deadlock?)"; return 1; } local output_size=${#output} [[ $output_size -gt 100000 ]] || { echo "FAIL: expected >100KB output, got ${output_size} bytes" return 1 } # VM must still be responsive after large output local check check=$(run_with_timeout 10 $SMOLVM machine exec -- echo "still-alive" 2>&1) || { echo "FAIL: VM unreachable after large stdout" return 1 } echo "$check" | grep -q "still-alive" || { echo "FAIL: expected 'still-alive', got: $check" return 1 } } _docker_in_vm_start_dockerd() { local vm_name="$1" $SMOLVM machine exec --name "$vm_name" -- sh -c ' mkdir -p /storage/docker /var/lib/docker mount --bind /storage/docker /var/lib/docker rm -f /var/run/docker.pid dockerd --storage-driver=overlay2 >/tmp/dockerd.log 2>&1 & for i in $(seq 1 40); do docker info >/dev/null 2>&1 && echo "dockerd-ready" && exit 0 sleep 1 done echo "FAIL: dockerd did not become ready" tail -5 /tmp/dockerd.log exit 1 ' 2>&1 } test_docker_in_vm() { skip_if_slow && return 0 local vm_name="docker-in-vm-$$" local smolfile="$PROJECT_ROOT/examples/docker-in-vm/docker.smolfile" echo "phase: pre-cleanup" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true # ── Create ──────────────────────────────────────────────────────────────── echo "phase: create" local output output=$($SMOLVM machine create --name "$vm_name" \ -s "$smolfile" --net-backend virtio-net 2>&1) || { echo "FAIL: machine create --name failed" echo "$output" return 1 } [[ "$output" == *"Init commands: 4"* ]] || { echo "FAIL: expected 4 init commands, got: $output" $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── First start (runs init: apk add docker + bind-mount) ───────────────── echo "phase: first-start (apk add docker — expect 30-90s)" output=$($SMOLVM machine start --name "$vm_name" 2>&1) || { echo "FAIL: first machine start failed" echo "$output" $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"Running 4 init command"* ]] || { echo "FAIL: expected 4 init commands to run on first start" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Start dockerd ───────────────────────────────────────────────────────── echo "phase: start-dockerd" output=$(_docker_in_vm_start_dockerd "$vm_name") || { echo "FAIL: dockerd failed to start" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"dockerd-ready"* ]] || { echo "FAIL: dockerd did not report ready" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Assert: overlay2 storage driver on ext4 ─────────────────────────────── echo "phase: assert-overlay2-on-ext4" output=$($SMOLVM machine exec --name "$vm_name" -- sh -c ' driver=$(docker info 2>/dev/null | grep "Storage Driver:" | awk "{print \$3}") [ "$driver" = "overlay2" ] || { echo "FAIL: storage driver=$driver"; exit 1; } echo "storage-driver-ok" mount | grep -q "/dev/vda on /var/lib/docker type ext4" || { echo "FAIL: /var/lib/docker not on ext4" mount | grep "var/lib/docker" || true exit 1 } echo "bind-mount-ok" ' 2>&1) || { echo "FAIL: storage driver or bind-mount check failed" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"storage-driver-ok"* ]] || { echo "FAIL: $output"; $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true; $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true; return 1; } [[ "$output" == *"bind-mount-ok"* ]] || { echo "FAIL: $output"; $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true; $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true; return 1; } # ── Assert: docker run executes a container ─────────────────────────────── echo "phase: docker-run" output=$($SMOLVM machine exec --name "$vm_name" -- \ docker run --rm alpine echo "docker-in-vm-ok" 2>&1) || { echo "FAIL: docker run failed" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"docker-in-vm-ok"* ]] || { echo "FAIL: expected docker-in-vm-ok, got: $output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Assert: bridge networking reaches the internet ──────────────────────── echo "phase: docker-bridge-networking" output=$($SMOLVM machine exec --name "$vm_name" -- \ docker run --rm alpine wget -qO- https://httpbin.org/get 2>&1) || { echo "FAIL: docker bridge networking failed" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *'"url"'* ]] || { echo "FAIL: expected JSON response with url field, got: $output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Assert: docker build produces a runnable image ──────────────────────── echo "phase: docker-build" output=$($SMOLVM machine exec --name "$vm_name" -- sh -c ' printf "FROM alpine\nRUN echo build-layer-ok" > /tmp/Dockerfile docker build -t smolvm-test-build /tmp 2>&1 | tail -3 docker run --rm smolvm-test-build echo "built-image-run-ok" ' 2>&1) || { echo "FAIL: docker build or run of built image failed" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"built-image-run-ok"* ]] || { echo "FAIL: expected built-image-run-ok, got: $output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Stop → restart cycle ────────────────────────────────────────────────── echo "phase: stop" $SMOLVM machine stop --name "$vm_name" 2>&1 || { echo "FAIL: machine stop failed" $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } echo "phase: restart" output=$($SMOLVM machine start --name "$vm_name" 2>&1) || { echo "FAIL: machine restart failed" echo "$output" $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"Init already completed"* ]] || { echo "FAIL: init should be skipped on restart, got: $output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Re-start dockerd after restart ──────────────────────────────────────── echo "phase: start-dockerd-after-restart" output=$(_docker_in_vm_start_dockerd "$vm_name") || { echo "FAIL: dockerd failed to start after VM restart" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"dockerd-ready"* ]] || { echo "FAIL: dockerd not ready after VM restart" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } # ── Assert: images persist across restart ───────────────────────────────── echo "phase: assert-persistence" output=$($SMOLVM machine exec --name "$vm_name" -- sh -c ' docker images | grep -q "^alpine " || { echo "FAIL: alpine image missing after restart"; docker images; exit 1; } echo "alpine-persists" docker images | grep -q "smolvm-test-build" || { echo "FAIL: built image missing after restart"; docker images; exit 1; } echo "built-image-persists" docker run --rm alpine echo "post-restart-run-ok" ' 2>&1) || { echo "FAIL: post-restart image persistence check failed" echo "$output" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true return 1 } [[ "$output" == *"alpine-persists"* ]] || { echo "FAIL: $output"; $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true; $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true; return 1; } [[ "$output" == *"built-image-persists"* ]] || { echo "FAIL: $output"; $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true; $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true; return 1; } [[ "$output" == *"post-restart-run-ok"* ]] || { echo "FAIL: $output"; $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true; $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true; return 1; } # ── Cleanup ─────────────────────────────────────────────────────────────── echo "phase: cleanup" $SMOLVM machine stop --name "$vm_name" 2>/dev/null || true $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true } run_test "Machine start" test_machine_start || true run_test "Machine stop" test_machine_stop || true run_test "Machine status (running)" test_machine_status_running || true run_test "Machine status (stopped)" test_machine_status_stopped || true run_test "Machine start/stop cycle" test_machine_start_stop_cycle || true run_test "Machine exec" test_machine_exec || true run_test "Machine exec: echo" test_machine_exec_echo || true run_test "Machine exec: binary output preserved (BUG-23)" test_machine_exec_binary_output_preserved || true run_test "Machine exec exit code" test_machine_exec_exit_code || true run_test "Failed exec does not kill VM" test_machine_exec_failed_does_not_kill_vm || true run_test "SIGTERM during exec does not stall VM" test_sigterm_during_exec_does_not_stall_vm || true run_test "Exec timeout does not stall VM" test_exec_timeout_does_not_stall_vm || true run_test "Named machine" test_machine_named_vm || true run_test "Create prints named start hint" test_machine_create_prints_named_start_hint || true run_test "Exec when stopped fails" test_machine_exec_when_stopped || true run_test "Bare VM: /workspace exists" test_bare_vm_workspace || true run_test "File upload and download" test_file_upload_download || true run_test "Streaming exec" test_streaming_exec || true run_test "Agent: structured JSON logs" test_agent_json_logs || true run_test "Shell: machine shell opens interactive shell" test_machine_shell || true run_test "Exec: large stdout does not crash VM (bare)" test_exec_large_stdout_does_not_crash_vm || true run_test "Docker-in-VM: overlay2 + bridge networking + build + restart persistence" test_docker_in_vm || true # ============================================================================= # Exec stdin null — stdin-blocking commands exit cleanly # ============================================================================= _EXEC_STDIN_MACHINE="exec-stdin-$$" test_exec_cat_no_interactive() { "$SMOLVM" machine stop --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || true "$SMOLVM" machine delete --name "$_EXEC_STDIN_MACHINE" -f 2>/dev/null || true "$SMOLVM" machine create --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || return 1 "$SMOLVM" machine start --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || return 1 local out exit_code=0 out=$(run_with_timeout 10 "$SMOLVM" machine exec --name "$_EXEC_STDIN_MACHINE" -- cat 2>&1) \ || exit_code=$? "$SMOLVM" machine stop --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || true "$SMOLVM" machine delete --name "$_EXEC_STDIN_MACHINE" -f 2>/dev/null || true if echo "$out" | grep -qi "connection closed"; then echo "FAIL: got 'connection closed'" return 1 fi [[ $exit_code -eq 0 ]] || { echo "FAIL: exit $exit_code (expected 0)"; return 1; } } run_test "Exec: 'exec -- cat' exits cleanly with null stdin" test_exec_cat_no_interactive || true test_exec_tty_piped_stdin_terminates() { # `--tty` runs the child on a PTY. A PTY cannot have one direction # closed, so when the feeding pipe (`echo`) closes, end-of-input must # still reach the child. Without EOF propagation a stdin reader (cat) # never terminates and the exec session hangs. "$SMOLVM" machine stop --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || true "$SMOLVM" machine delete --name "$_EXEC_STDIN_MACHINE" -f 2>/dev/null || true "$SMOLVM" machine create --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || return 1 "$SMOLVM" machine start --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || return 1 # Newline-terminated input: the PTY line buffer is empty when EOF # arrives, so a single VEOF would already yield the zero-length read. local exit_code=0 run_with_timeout 15 sh -c \ "echo tty-input | '$SMOLVM' machine exec --name '$_EXEC_STDIN_MACHINE' --tty -i -- cat" \ >/dev/null 2>&1 || exit_code=$? # Unterminated input (no trailing newline): the first VEOF only flushes # the partial line as data, so a second VEOF is required to deliver the # zero-length read. This is the case a single VEOF fails to terminate. local exit_code_partial=0 run_with_timeout 15 sh -c \ "printf no-newline | '$SMOLVM' machine exec --name '$_EXEC_STDIN_MACHINE' --tty -i -- cat" \ >/dev/null 2>&1 || exit_code_partial=$? "$SMOLVM" machine stop --name "$_EXEC_STDIN_MACHINE" 2>/dev/null || true "$SMOLVM" machine delete --name "$_EXEC_STDIN_MACHINE" -f 2>/dev/null || true [[ $exit_code -ne 124 ]] || { echo "FAIL: 'exec --tty -i' with piped stdin timed out (PTY EOF not propagated)"; return 1; } [[ $exit_code_partial -ne 124 ]] || { echo "FAIL: 'exec --tty -i' with unterminated stdin timed out (PTY EOF not propagated)"; return 1; } } run_test "Exec: 'exec --tty -i' with piped stdin terminates (PTY EOF)" test_exec_tty_piped_stdin_terminates || true # ============================================================================= # Named machine survives observer Drop # ============================================================================= _DROP_MACHINE="drop-safe-$$" test_machine_survives_rapid_exec() { "$SMOLVM" machine stop --name "$_DROP_MACHINE" 2>/dev/null || true "$SMOLVM" machine delete --name "$_DROP_MACHINE" -f 2>/dev/null || true "$SMOLVM" machine create --name "$_DROP_MACHINE" 2>/dev/null || return 1 "$SMOLVM" machine start --name "$_DROP_MACHINE" 2>/dev/null || return 1 local i for i in 1 2 3 4 5; do local out exit_code=0 out=$(run_with_timeout 15 "$SMOLVM" machine exec --name "$_DROP_MACHINE" -- echo "alive-$i" 2>/dev/null) \ || exit_code=$? [[ $exit_code -eq 0 ]] || { "$SMOLVM" machine delete --name "$_DROP_MACHINE" -f 2>/dev/null || true echo "FAIL: exec #$i failed (exit $exit_code)"; return 1 } done "$SMOLVM" machine stop --name "$_DROP_MACHINE" 2>/dev/null || true "$SMOLVM" machine delete --name "$_DROP_MACHINE" -f 2>/dev/null || true } run_test "Drop-safety: machine survives 5 rapid execs" test_machine_survives_rapid_exec || true print_summary "Bare VM Tests"