Files
2026-07-13 13:13:07 +08:00

843 lines
35 KiB
Bash
Executable File

#!/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"