# Local CI entry point used by Git hooks. Pass args through to CraftOS tests. ci *args: check-craftos check check-packages npm-build npm-test @just _craftos-test {{args}} @just test-integration # Run all standard tests. Pass `--pretty` for grouped CraftOS output. [positional-arguments] test *args: build npm-test @just _craftos-test {{args}} # Run end-to-end tests that span the MCP bridge and headless CraftOS-PC. e2e: npm-test-integration # Run integration and harness tests that are too broad for unit test targets. test-integration: e2e test-timeout # Run CraftOS-PC headless integration tests. Pass `--pretty` for grouped output. [positional-arguments] _craftos-test *args: #!/usr/bin/env bash set -uo pipefail if [ -f .env.test ]; then set -a; . ./.env.test; set +a; fi pretty=0 has_output=0 timeout_seconds="${TRAP_CCLIBS_TEST_TIMEOUT_SECONDS:-3}" case "$timeout_seconds" in ''|*[!0-9]*) printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be a positive integer' >&2; exit 1 ;; esac if [ "$timeout_seconds" -lt 1 ]; then printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be >= 1' >&2; exit 1; fi runtest_args=("$@") for a in "$@"; do case "$a" in --pretty|--verbose|-v) pretty=1 ;; --output) has_output=1 ;; esac done if [ "$pretty" -eq 1 ] && [ "$has_output" -eq 0 ]; then runtest_args+=(--output /trapos-test-output) fi runtest_args+=(--shutdown) lua_quote() { local value="$1" value="${value//\\/\\\\}" value="${value//\'/\\\'}" printf "'%s'" "$value" } exec_code="shell.run('/programs/runtest.lua'" for arg in "${runtest_args[@]}"; do exec_code+=", $(lua_quote "$arg")" done exec_code+=")" rom_arg=() if [ "$(uname -s)" = "Darwin" ]; then rom_arg=(--rom /Applications/CraftOS-PC.app/Contents/Resources) fi repo='{{justfile_directory()}}' mount_arg=(--mount-ro "/trapos=$repo" --mount-ro "/apis=$repo/apis" --mount-ro "/programs=$repo/programs" --mount-ro "/startup=$repo/startup" --mount-ro "/tests=$repo/tests") tmp="$(mktemp)" data_dir="$(mktemp -d)" output_path="$data_dir/computer/0/trapos-test-output" craftos --directory "$data_dir" --headless "${rom_arg[@]}" "${mount_arg[@]}" --exec "$exec_code" >"$tmp" 2>&1 & pid="$!" ( sleep "$timeout_seconds"; kill -TERM "$pid" >/dev/null 2>&1 ) & watchdog="$!" wait "$pid" >/dev/null 2>&1 status="$?" kill "$watchdog" >/dev/null 2>&1 || true wait "$watchdog" >/dev/null 2>&1 || true if grep -q __TRAPOS_TEST_OK__ "$tmp"; then if [ "$pretty" -eq 1 ] && [ -f "$output_path" ]; then cat "$output_path"; fi rm -f "$tmp" rm -rf "$data_dir" else red=$(printf '\033[31m'); reset=$(printf '\033[0m') if [ "$status" -eq 143 ]; then printf '%s\n' "${red}FAIL${reset} CraftOS integration tests timed out after ${timeout_seconds}s" >&2 else printf '%s\n' "${red}FAIL${reset} CraftOS integration tests did not print __TRAPOS_TEST_OK__" >&2 fi if [ -f "$output_path" ]; then cat "$output_path" >&2; fi cat "$tmp" >&2 rm -f "$tmp" rm -rf "$data_dir" exit 1 fi printf '%s\n' 'OK: CraftOS integration tests passed' # Harness self-test: run a tests/harness fixture and assert which timeout layer # caught it. `expect` is "lua" (libtest cancels the case) or "shell" (the shell # watchdog kills the whole process). Not part of `ci`/`test`: these exercise the # failure paths on purpose. _timeout-fixture script shell_timeout extra_flag expect: check-install #!/usr/bin/env bash set -uo pipefail repo='{{justfile_directory()}}' if [ -f "$repo/.env.test" ]; then set -a; . "$repo/.env.test"; set +a; fi rom_arg="" if [ "$(uname -s)" = "Darwin" ]; then rom_arg="--rom /Applications/CraftOS-PC.app/Contents/Resources" fi mount_arg="--mount-ro /apis=$repo/apis --mount-ro /programs=$repo/programs --mount-ro /tests=$repo/tests" tmp="$(mktemp)" data_dir="$(mktemp -d)" output_path="$data_dir/computer/0/trapos-test-output" exec_code="shell.run('/programs/runtest.lua', '{{script}}', '--verbose', '--output', '/trapos-test-output', {{extra_flag}} '--shutdown')" shell_timeout="{{shell_timeout}}" craftos --directory "$data_dir" --headless $rom_arg $mount_arg --exec "$exec_code" >"$tmp" 2>&1 & pid="$!" ( sleep "$shell_timeout"; kill -TERM "$pid" >/dev/null 2>&1 ) & watchdog="$!" wait "$pid" >/dev/null 2>&1 status="$?" kill "$watchdog" >/dev/null 2>&1 || true wait "$watchdog" >/dev/null 2>&1 || true combined="$(cat "$tmp"; [ -f "$output_path" ] && cat "$output_path")" red=$(printf '\033[31m'); green=$(printf '\033[32m'); reset=$(printf '\033[0m') rc=0 case "{{expect}}" in lua) if printf '%s\n' "$combined" | grep -q 'libtest timeout' && [ "$status" -ne 143 ]; then printf '%s\n' "${green}OK${reset} libtest cancelled the case (shell watchdog not needed)"; \ else printf '%s\n' "${red}FAIL${reset} expected a libtest timeout before the shell watchdog (status=$status)" >&2 rc=1 fi ;; shell) if [ "$status" -eq 143 ]; then printf '%s\n' "${green}OK${reset} shell watchdog killed the run after ${shell_timeout}s (status 143; libtest timeout bypassed)"; \ else printf '%s\n' "${red}FAIL${reset} expected the shell watchdog to kill the run (status=$status)" >&2 rc=1 fi ;; *) printf '%s\n' "${red}FAIL${reset} unknown expectation '{{expect}}'" >&2 rc=1 ;; esac if [ "$rc" -ne 0 ]; then printf '%s\n' "$combined" >&2; fi rm -f "$tmp" rm -rf "$data_dir" exit "$rc" # Prove the libtest (Lua) timeout layer: libtest cancels the slow case quickly, # before the shell watchdog backstop can fire. test-timeout-lua: (_timeout-fixture "/tests/harness/slow-case.lua" "${TRAP_CCLIBS_TEST_TIMEOUT_WATCHDOG_SECONDS:-1}" "'--timeout', '0'," "lua") # Prove the shell watchdog backstop: the slow case runs with the libtest timeout # bypassed (--no-timeout), so the shell watchdog kills the whole process. test-timeout-shell: (_timeout-fixture "/tests/harness/slow-case.lua" "${TRAP_CCLIBS_TEST_TIMEOUT_WATCHDOG_SECONDS:-1}" "'--no-timeout'," "shell") # Fast regression guard for both timeout layers. Wired into `ci`. test-timeout: test-timeout-lua test-timeout-shell