diff --git a/tests/test_addon_metadata.py b/tests/test_addon_metadata.py index 090fce7..9776b15 100644 --- a/tests/test_addon_metadata.py +++ b/tests/test_addon_metadata.py @@ -100,3 +100,27 @@ def test_dockerfile_patches_parent_cloakserve_instead_of_copying_a_binary() -> N def test_runtime_accepts_docker_port_unpublished_status() -> None: runtime_test = (ROOT / "tests/test_runtime.sh").read_text(encoding="utf-8") assert 'docker port "$container" 9222/tcp 2>/dev/null || true' in runtime_test + + +def test_runtime_requires_process_baseline_after_cdp_close_and_zero_stopped_pid() -> None: + runtime_test = (ROOT / "tests/test_runtime.sh").read_text(encoding="utf-8") + assert 'docker top "$container" -eo pid,args' in runtime_test + for prefix in ("first", "second"): + container = f"${prefix}_container" + baseline = ( + f'{prefix}_baseline="$(capture_process_baseline "{container}")"' + ) + ready_index = runtime_test.index( + f'wait_ready "{container}" "${prefix}_port"' + ) + baseline_index = runtime_test.index(baseline) + close_index = runtime_test.index( + f'probe_and_close_cdp "{container}"', baseline_index + ) + return_index = runtime_test.index( + f'assert_processes_return_to_baseline "{container}" "${prefix}_baseline"', + close_index, + ) + assert ready_index < baseline_index < close_index < return_index + assert "{{.State.Pid}}" in runtime_test + assert '[ "$state" = "exited 0 0" ]' in runtime_test diff --git a/tests/test_runtime.sh b/tests/test_runtime.sh index daa905c..19486f9 100755 --- a/tests/test_runtime.sh +++ b/tests/test_runtime.sh @@ -108,6 +108,76 @@ wait_ready() { fail "$container did not become ready within 90 seconds" } +normalized_process_commands() { + local container="$1" + local top_file="${tmp_dir}/${container}-processes.txt" + docker top "$container" -eo pid,args > "$top_file" + python3 - "$top_file" <<'PY' +import pathlib +import sys + +commands = [] +for raw_line in pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").splitlines()[1:]: + fields = raw_line.split(maxsplit=1) + command = " ".join(fields[1].split()) if len(fields) == 2 else "" + lowered = command.lower() + if "/usr/local/bin/addon-supervisor" in command: + commands.append("addon-supervisor") + elif "/usr/local/bin/cloakserve" in command: + commands.append("cloakserve") + elif "/usr/local/bin/stelloauth" in command: + commands.append("stelloauth") + elif ( + ("/run/rosetta/rosetta" in lowered or "qemu-" in lowered) + and "chrome" not in lowered + and "chromium" not in lowered + and "headless_shell" not in lowered + ): + continue + elif command: + commands.append(f"unexpected:{command}") + +print("\n".join(sorted(commands))) +PY +} + +capture_process_baseline() { + local container="$1" + local expected current + local deadline=$((SECONDS + 10)) + expected=$'addon-supervisor\ncloakserve\nstelloauth' + while (( SECONDS < deadline )); do + current="$(normalized_process_commands "$container")" + if [ "$current" = "$expected" ]; then + printf '%s\n' "$current" + return + fi + sleep 0.25 + done + printf 'expected startup process baseline:\n%s\ncurrent process commands:\n%s\n' \ + "$expected" "$current" >&2 + docker top "$container" >&2 + fail "$container did not reach the expected startup process baseline" +} + +assert_processes_return_to_baseline() { + local container="$1" + local baseline="$2" + local current + local deadline=$((SECONDS + 10)) + while (( SECONDS < deadline )); do + current="$(normalized_process_commands "$container")" + if [ "$current" = "$baseline" ]; then + return + fi + sleep 0.25 + done + printf 'expected process baseline after CDP close:\n%s\ncurrent process commands:\n%s\n' \ + "$baseline" "$current" >&2 + docker top "$container" >&2 + fail "$container retained browser or profile processes after CDP close" +} + assert_loopback_cdp_listener() { local container="$1" local artifact="$2" @@ -243,8 +313,8 @@ import sys if float(sys.argv[1]) > 10.0: raise SystemExit(f"container stop exceeded 10 seconds: {sys.argv[1]}") PY - state="$(docker inspect --format '{{.State.Status}} {{.State.ExitCode}}' "$container")" - [ "$state" = "exited 0" ] || fail "$container state is $state, want exited 0" + state="$(docker inspect --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Pid}}' "$container")" + [ "$state" = "exited 0 0" ] || fail "$container state is $state, want exited 0 with PID 0" } mkdir -p "$artifacts_dir" @@ -262,9 +332,11 @@ fi start_container "$first_container" first_port="$(host_port "$first_container")" wait_ready "$first_container" "$first_port" +first_baseline="$(capture_process_baseline "$first_container")" assert_no_9222_mapping "$first_container" assert_loopback_cdp_listener "$first_container" "${artifacts_dir}/runtime-proc-net-tcp.txt" probe_and_close_cdp "$first_container" +assert_processes_return_to_baseline "$first_container" "$first_baseline" post_invalid_worker "$first_port" "${artifacts_dir}/runtime-invalid-worker-response.json" scan_logs "$first_container" stop_and_assert "$first_container" "${artifacts_dir}/runtime-first-stop.txt" @@ -273,9 +345,11 @@ scan_logs "$first_container" start_container "$second_container" second_port="$(host_port "$second_container")" wait_ready "$second_container" "$second_port" +second_baseline="$(capture_process_baseline "$second_container")" assert_no_9222_mapping "$second_container" assert_loopback_cdp_listener "$second_container" "${artifacts_dir}/runtime-restart-proc-net-tcp.txt" probe_and_close_cdp "$second_container" +assert_processes_return_to_baseline "$second_container" "$second_baseline" sleep 5 docker stats --no-stream "$second_container" > "${artifacts_dir}/runtime-docker-stats.txt"