|
31 | 31 |
|
32 | 32 |
|
33 | 33 | class _FakeResult: |
34 | | - def __init__(self, exit_code: int = 0, stdout: bytes = b"", stderr: bytes = b"") -> None: |
| 34 | + def __init__(self, exit_code: int = 0, stdout: bytes = b"", stderr: bytes = b"", timed_out: bool = False) -> None: |
35 | 35 | self.exit_code = exit_code |
36 | 36 | self.stdout = stdout |
37 | 37 | self.stderr = stderr |
| 38 | + self.timed_out = timed_out |
38 | 39 |
|
39 | 40 | @property |
40 | 41 | def stdout_text(self) -> str: |
@@ -388,6 +389,26 @@ def test_execute_command_returns_error_as_text() -> None: |
388 | 389 | assert box.execute_command("echo hi") == "Error: boom" |
389 | 390 |
|
390 | 391 |
|
| 392 | +@pytest.mark.parametrize("exit_code", [0, 143]) |
| 393 | +@pytest.mark.parametrize("stdout,stderr", [(b"partial output", b"diagnostic"), (b"", b"")]) |
| 394 | +def test_execute_command_reports_timeout_and_preserves_output(monkeypatch, exit_code, stdout, stderr) -> None: |
| 395 | + fake = _FakeSandbox() |
| 396 | + monkeypatch.setattr(fake, "_run_script", lambda script: _FakeResult(exit_code=exit_code, stdout=stdout, stderr=stderr, timed_out=True)) |
| 397 | + invalidated = [] |
| 398 | + box = TenkiSandbox("sb", fake, on_terminal_failure=lambda *args: invalidated.append(args)) |
| 399 | + |
| 400 | + output = box.execute_command("sleep 10", timeout=1) |
| 401 | + |
| 402 | + assert "Error: command timed out" in output |
| 403 | + assert output.endswith("Exit Code: 124") |
| 404 | + assert stdout.decode() in output |
| 405 | + assert stderr.decode() in output |
| 406 | + assert len(fake.exec_calls) == 1 |
| 407 | + assert fake.exec_calls[0]["timeout"] == 1 |
| 408 | + assert not invalidated |
| 409 | + assert not box.is_closed |
| 410 | + |
| 411 | + |
391 | 412 | def test_execute_command_closed_returns_error() -> None: |
392 | 413 | box = TenkiSandbox("sb", _FakeSandbox()) |
393 | 414 | box.close() |
@@ -845,6 +866,127 @@ def test_bootstrap_is_non_interactive_and_time_bounded(monkeypatch): |
845 | 866 | provider.shutdown() |
846 | 867 |
|
847 | 868 |
|
| 869 | +def test_timed_out_bootstrap_warns_even_with_success_marker(monkeypatch, caplog): |
| 870 | + class _TimedOutBootstrap(_FakeSandbox): |
| 871 | + def _run_script(self, script: str) -> _FakeResult: |
| 872 | + if "BOOTSTRAP_OK" in script: |
| 873 | + return _FakeResult(stdout=b"BOOTSTRAP_OK\n", timed_out=True) |
| 874 | + return super()._run_script(script) |
| 875 | + |
| 876 | + provider = _install(monkeypatch, client=_FakeClient(sandbox_factory=_TimedOutBootstrap)) |
| 877 | + try: |
| 878 | + with caplog.at_level("WARNING"): |
| 879 | + sid = provider.acquire("thread-1", user_id="u1") |
| 880 | + assert provider.get(sid) is not None |
| 881 | + assert any("bootstrap" in r.message and "timed_out=True" in r.message for r in caplog.records) |
| 882 | + finally: |
| 883 | + provider.shutdown() |
| 884 | + |
| 885 | + |
| 886 | +@pytest.mark.parametrize("timed_out,exit_code", [(True, 0), (False, 1)]) |
| 887 | +def test_warm_pool_rejects_unsuccessful_probe_with_ok_output(monkeypatch, caplog, timed_out, exit_code): |
| 888 | + client = _FakeClient() |
| 889 | + provider = _install(monkeypatch, client=client) |
| 890 | + try: |
| 891 | + sid = provider.acquire("thread-1", user_id="u1") |
| 892 | + first = client.last_sandbox |
| 893 | + provider.release(sid) |
| 894 | + monkeypatch.setattr( |
| 895 | + first, |
| 896 | + "_run_script", |
| 897 | + lambda script: _FakeResult(stdout=b"ok\n", exit_code=exit_code, timed_out=timed_out), |
| 898 | + ) |
| 899 | + |
| 900 | + with caplog.at_level(logging.WARNING): |
| 901 | + assert provider.acquire("thread-1", user_id="u1") == sid |
| 902 | + assert client.last_sandbox is not first |
| 903 | + assert first.closed |
| 904 | + failure = next(record.message for record in caplog.records if "health check failed" in record.message) |
| 905 | + assert sid in failure |
| 906 | + assert "ok\n" in failure |
| 907 | + assert f"Exit Code: {124 if timed_out else exit_code}" in failure |
| 908 | + finally: |
| 909 | + provider.shutdown() |
| 910 | + |
| 911 | + |
| 912 | +@pytest.mark.parametrize( |
| 913 | + "stdout,stderr", |
| 914 | + [ |
| 915 | + (b"ok\n", b"sh: warning: setlocale: LC_ALL: cannot change locale\n"), |
| 916 | + (b"profile loaded\nok\n", b""), |
| 917 | + ( |
| 918 | + b"profile loaded\nok\n", |
| 919 | + b"sh: warning: setlocale: LC_ALL: cannot change locale\n", |
| 920 | + ), |
| 921 | + ], |
| 922 | +) |
| 923 | +def test_warm_pool_reclaims_healthy_probe_with_shell_noise(monkeypatch, caplog, stdout, stderr): |
| 924 | + client = _FakeClient() |
| 925 | + provider = _install(monkeypatch, client=client) |
| 926 | + try: |
| 927 | + sid = provider.acquire("thread-1", user_id="u1") |
| 928 | + first = client.last_sandbox |
| 929 | + provider.release(sid) |
| 930 | + monkeypatch.setattr( |
| 931 | + first, |
| 932 | + "_run_script", |
| 933 | + lambda script: _FakeResult(stdout=stdout, stderr=stderr), |
| 934 | + ) |
| 935 | + calls_before = len(first.exec_calls) |
| 936 | + |
| 937 | + with caplog.at_level(logging.WARNING): |
| 938 | + assert provider.acquire("thread-1", user_id="u1") == sid |
| 939 | + assert client.create_count == 1 |
| 940 | + assert client.last_sandbox is first |
| 941 | + assert not first.closed |
| 942 | + assert len(first.exec_calls) == calls_before + 1 |
| 943 | + assert first.exec_calls[-1]["argv"] == ("sh", "-lc", "echo ok") |
| 944 | + assert first.exec_calls[-1]["timeout"] == 10 |
| 945 | + assert not any("health check" in record.message for record in caplog.records) |
| 946 | + finally: |
| 947 | + provider.shutdown() |
| 948 | + |
| 949 | + |
| 950 | +@pytest.mark.parametrize("stdout", [b"not ok\n", b"ok-ish\n", b""]) |
| 951 | +def test_warm_pool_logs_probe_without_exact_success_line(monkeypatch, caplog, stdout): |
| 952 | + client = _FakeClient() |
| 953 | + provider = _install(monkeypatch, client=client) |
| 954 | + try: |
| 955 | + sid = provider.acquire("thread-1", user_id="u1") |
| 956 | + first = client.last_sandbox |
| 957 | + provider.release(sid) |
| 958 | + monkeypatch.setattr(first, "_run_script", lambda script: _FakeResult(stdout=stdout)) |
| 959 | + |
| 960 | + with caplog.at_level(logging.WARNING): |
| 961 | + assert provider.acquire("thread-1", user_id="u1") == sid |
| 962 | + assert client.create_count == 2 |
| 963 | + assert client.last_sandbox is not first |
| 964 | + assert first.closed |
| 965 | + output = stdout.decode() if stdout else "(no output)" |
| 966 | + assert any(f"Tenki warm-pool sandbox {sid} health check failed: {output}" == record.message for record in caplog.records) |
| 967 | + finally: |
| 968 | + provider.shutdown() |
| 969 | + |
| 970 | + |
| 971 | +def test_warm_pool_rejects_probe_error_containing_success_line(monkeypatch, caplog): |
| 972 | + client = _FakeClient() |
| 973 | + provider = _install(monkeypatch, client=client) |
| 974 | + try: |
| 975 | + sid = provider.acquire("thread-1", user_id="u1") |
| 976 | + first = client.last_sandbox |
| 977 | + provider.release(sid) |
| 978 | + first.exec_error = RuntimeError("probe transport failed\nok") |
| 979 | + |
| 980 | + with caplog.at_level(logging.WARNING): |
| 981 | + assert provider.acquire("thread-1", user_id="u1") == sid |
| 982 | + assert client.create_count == 2 |
| 983 | + assert client.last_sandbox is not first |
| 984 | + assert first.closed |
| 985 | + assert any(f"Tenki warm-pool sandbox {sid} health check failed: Error: probe transport failed\nok" == record.message for record in caplog.records) |
| 986 | + finally: |
| 987 | + provider.shutdown() |
| 988 | + |
| 989 | + |
848 | 990 | def test_release_parks_in_warm_pool(monkeypatch): |
849 | 991 | client = _FakeClient() |
850 | 992 | provider = _install(monkeypatch, client=client) |
|
0 commit comments