Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b4c1aa7
test(browser): require forced-close failure teardown evidence
seonghobae Aug 13, 2026
fbada6a
Merge current forced-close teardown prerequisite
seonghobae Aug 13, 2026
e0737d6
fix(browser): retain forced-close failure teardown proof
seonghobae Aug 13, 2026
36f6d6f
chore(stack): absorb corrected late-failure prerequisite
seonghobae Aug 18, 2026
171f365
test(browser): prevent state diagnostic reflection
seonghobae Aug 20, 2026
830be53
fix(mv3): redact agent task state failures
seonghobae Aug 20, 2026
371ce49
test(mv3): expose wedged driver teardown escape
seonghobae Aug 21, 2026
7f7a4ec
fix(mv3): bound forced-close driver teardown failures
seonghobae Aug 21, 2026
7e4a867
test(mv3): require bounded driver teardown evidence
seonghobae Aug 21, 2026
1731883
test(browser): require kill fallback evidence
seonghobae Aug 21, 2026
c3291dc
fix(browser): retain kill fallback evidence
seonghobae Aug 21, 2026
ae0789b
test(browser): preserve kill fallback evidence
seonghobae Aug 21, 2026
cb2b887
test(browser): expose forced-close session cleanup loss
seonghobae Aug 21, 2026
dab7248
fix(browser): retain forced-close session cleanup failures
seonghobae Aug 21, 2026
5887d66
test(browser): decouple teardown ordering contract from condition for…
seonghobae Aug 21, 2026
0cafe07
test(browser): accept multiline bounded session cleanup call
seonghobae Aug 21, 2026
27a3761
test(stack): restack forced-close failure contracts on live teardown …
seonghobae Aug 23, 2026
7a7923e
fix(browser): restore typed forced-close cleanup envelope
seonghobae Aug 23, 2026
52bd685
fix(browser): preserve parent teardown contracts after restack
seonghobae Aug 23, 2026
152ca36
test(mv3): reject untyped cleanup suppression
seonghobae Aug 23, 2026
b0aabe0
fix(mv3): preserve typed cleanup failures
seonghobae Aug 23, 2026
a1ed848
test(mv3): preserve primary failure precedence contract
seonghobae Aug 23, 2026
618fee1
test(mv3): retain WebDriver HTTP cleanup failures
seonghobae Aug 23, 2026
d4cf106
fix(mv3): classify WebDriver HTTP cleanup failures
seonghobae Aug 23, 2026
4749131
test(mv3): retry transient startup HTTP protocol failure
seonghobae Aug 23, 2026
fb5a9c3
fix(mv3): retry malformed startup status line
seonghobae Aug 23, 2026
3f77c75
test(browser): fail closed on terminal startup errors
seonghobae Aug 23, 2026
5bc6044
fix(browser): keep terminal startup errors fail closed
seonghobae Aug 23, 2026
34ab98e
docs(browser): restore generic WebDriver path contract
seonghobae Aug 23, 2026
f96e55f
test(browser): reproduce truncated startup response
seonghobae Aug 23, 2026
86b174b
fix(browser): retry truncated startup responses
seonghobae Aug 23, 2026
a3df28d
test(browser): reject WebDriver response detail leakage
seonghobae Aug 23, 2026
48435b9
fix(browser): redact WebDriver response diagnostics
seonghobae Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 279 additions & 60 deletions scripts/ci/run_mv3_compatibility.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Dead no-such-window HTTP branch after redaction

_is_no_such_window_runtime_error still parses a WebDriver HTTP 404: <json> message, but _json_request no longer emits that shape: no-such-window now raises the redacted WebDriver error: string and other HTTP errors raise WebDriver HTTP {status} with no detail. The HTTP-prefix branch is now unreachable; detection still works via the direct-prefix branch.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def test_browser_pass_retains_failure_process_termination_evidence(self) -> None
for expected in (
"browser_failure_type: str | None = None",
"browser_failure_type = type(exc).__name__",
'"failure_type": browser_failure_type',
"primary_failure_type = browser_failure_type",
'"failure_type": primary_failure_type',
'"browser_process_terminated": browser_process_terminated',
):
with self.subTest(expected=expected):
Expand Down
227 changes: 219 additions & 8 deletions tests/test_agent_task_forced_close_process_termination_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pathlib
import runpy
import subprocess
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
Expand All @@ -25,8 +26,11 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self)
"_read_linux_proc_stat_process_identity",
"_snapshot_linux_process_evidence",
"_read_linux_process_identity_set",
"_terminate_owned_process_bounded",
"_wait_for_linux_process_identity_exit",
"_wait_for_linux_process_identity_set_exit",
'"driver_process_terminated"',
'"driver_kill_fallback_used"',
'"browser_process_terminated"',
'"chromium_process_set_terminated"',
):
Expand All @@ -51,6 +55,8 @@ def fake_browser_pass(
"browser_version": namespace["PINNED_CHROME_VERSION"],
"forced_close_detected": True,
"session_survived": True,
"driver_process_terminated": True,
"driver_kill_fallback_used": False,
"browser_process_terminated": False,
"chromium_process_set_terminated": False,
}
Expand All @@ -62,22 +68,227 @@ def fake_browser_pass(
"http://127.0.0.1/fixture",
1,
)
self.assertIs(result["driver_process_terminated"], True)
self.assertIs(result["driver_kill_fallback_used"], False)
self.assertIs(result["browser_process_terminated"], False)
self.assertIs(result["chromium_process_set_terminated"], False)

def test_main_forced_close_gate_requires_process_termination(self) -> None:
"""Compatibility success must reject a live forced-close browser identity."""
def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> None:
"""A reviewed browser failure after identity capture must not skip teardown proof."""

runner = RUNNER.read_text(encoding="utf-8")
start = runner.index("forced_close_surfaces_complete = all(")
end = runner.index("\n\n evidence = {", start)
gate = runner[start:end]
start = runner.index("def _run_agent_task_forced_close_browser_pass(")
end = runner.index("\ndef _run_agent_task_forced_close_trial(", start)
browser_pass = runner[start:end]

for expected in (
'trial.get("browser_process_terminated") is True',
'trial.get("chromium_process_set_terminated") is True',
"browser_failure_type",
"driver_cleanup_failure_type",
"driver_kill_fallback_used",
"except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:",
'browser_failure_type = type(exc).__name__',
"failure_evidence",
'"driver_process_terminated": driver_process_terminated',
'"driver_kill_fallback_used": driver_kill_fallback_used',
'"browser_process_terminated": browser_process_terminated',
'failure_evidence["chromium_process_set_terminated"]',
):
with self.subTest(expected=expected):
self.assertIn(expected, gate)
self.assertIn(expected, browser_pass)

shutdown = browser_pass.index("_terminate_owned_process_bounded(driver)")
root_wait = browser_pass.index("_wait_for_linux_process_identity_exit(")
set_wait = browser_pass.index("_wait_for_linux_process_identity_set_exit(")
failure_return = browser_pass.index("browser_failure_type is not None", set_wait)
self.assertLess(shutdown, root_wait)
self.assertLess(root_wait, failure_return)
self.assertLess(set_wait, failure_return)

def test_forced_close_driver_shutdown_timeout_is_bounded_and_typed(self) -> None:
"""A wedged ChromeDriver after SIGKILL must become failure evidence, not escape."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_driver_shutdown_timeout_contract"
)
shutdown = namespace["_terminate_owned_process_bounded"]
timeout_seconds = namespace["PROCESS_EXIT_TIMEOUT_SECONDS"]

class WedgedProcess:
def __init__(self) -> None:
self.terminated = False
self.killed = False
self.wait_timeouts: list[float] = []

def terminate(self) -> None:
self.terminated = True

def kill(self) -> None:
self.killed = True

def wait(self, timeout: float) -> int:
self.wait_timeouts.append(timeout)
raise subprocess.TimeoutExpired("chromedriver", timeout)

process = WedgedProcess()
terminated, failure_type, kill_fallback_used = shutdown(process)

self.assertIs(process.terminated, True)
self.assertIs(process.killed, True)
self.assertEqual(process.wait_timeouts, [timeout_seconds, timeout_seconds])
self.assertIs(terminated, False)
self.assertEqual(failure_type, "TimeoutExpired")
self.assertIs(kill_fallback_used, True)

def test_forced_close_driver_shutdown_records_successful_kill_fallback(self) -> None:
"""A successful SIGKILL fallback must remain explicit in cleanup evidence."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_driver_kill_fallback_contract"
)
shutdown = namespace["_terminate_owned_process_bounded"]
timeout_seconds = namespace["PROCESS_EXIT_TIMEOUT_SECONDS"]

class KillRecoversProcess:
def __init__(self) -> None:
self.terminated = False
self.killed = False
self.wait_timeouts: list[float] = []

def terminate(self) -> None:
self.terminated = True

def kill(self) -> None:
self.killed = True

def wait(self, timeout: float) -> int:
self.wait_timeouts.append(timeout)
if len(self.wait_timeouts) == 1:
raise subprocess.TimeoutExpired("chromedriver", timeout)
return 0

process = KillRecoversProcess()
terminated, failure_type, kill_fallback_used = shutdown(process)

self.assertIs(process.terminated, True)
self.assertIs(process.killed, True)
self.assertEqual(process.wait_timeouts, [timeout_seconds, timeout_seconds])
self.assertIs(terminated, True)
self.assertIsNone(failure_type)
self.assertIs(kill_fallback_used, True)

def test_forced_close_driver_shutdown_graceful_path_records_no_fallback(self) -> None:
"""Graceful ChromeDriver shutdown must not report hard-kill fallback use."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_driver_graceful_shutdown_contract"
)
shutdown = namespace["_terminate_owned_process_bounded"]
timeout_seconds = namespace["PROCESS_EXIT_TIMEOUT_SECONDS"]

class GracefulProcess:
def __init__(self) -> None:
self.terminated = False
self.killed = False
self.wait_timeouts: list[float] = []

def terminate(self) -> None:
self.terminated = True

def kill(self) -> None:
self.killed = True

def wait(self, timeout: float) -> int:
self.wait_timeouts.append(timeout)
return 0

process = GracefulProcess()
terminated, failure_type, kill_fallback_used = shutdown(process)

self.assertIs(process.terminated, True)
self.assertIs(process.killed, False)
self.assertEqual(process.wait_timeouts, [timeout_seconds])
self.assertIs(terminated, True)
self.assertIsNone(failure_type)
self.assertIs(kill_fallback_used, False)

def test_forced_close_trial_preserves_driver_cleanup_failure_separately(self) -> None:
"""Browser and driver-cleanup failure evidence must remain separately attributable."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_driver_cleanup_trial_contract"
)
trial = namespace["_run_agent_task_forced_close_trial"]

def fake_browser_pass(
_chrome_bin: pathlib.Path,
_chromedriver_bin: pathlib.Path,
_fixture_url: str,
_profile_dir: str,
) -> dict[str, object]:
return {
"failure_type": "RuntimeError",
"cleanup_failure_type": "TimeoutExpired",
"driver_process_terminated": False,
"driver_kill_fallback_used": True,
"browser_process_terminated": True,
"chromium_process_set_terminated": True,
}

trial.__globals__["_run_agent_task_forced_close_browser_pass"] = fake_browser_pass
result = trial(
pathlib.Path("/unused/chrome"),
pathlib.Path("/unused/chromedriver"),
"http://127.0.0.1/fixture",
2,
)

self.assertIs(result["passed"], False)
self.assertEqual(result["failure_type"], "RuntimeError")
self.assertEqual(result["cleanup_failure_type"], "TimeoutExpired")
self.assertIs(result["driver_process_terminated"], False)
self.assertIs(result["driver_kill_fallback_used"], True)
self.assertIs(result["browser_process_terminated"], True)
self.assertIs(result["chromium_process_set_terminated"], True)
self.assertIs(result["profile_cleaned"], True)

def test_forced_close_trial_preserves_successful_kill_fallback_evidence(self) -> None:
"""Successful forced-close trials must still say when ChromeDriver needed SIGKILL."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_driver_kill_fallback_trial_contract"
)
trial = namespace["_run_agent_task_forced_close_trial"]

def fake_browser_pass(
_chrome_bin: pathlib.Path,
_chromedriver_bin: pathlib.Path,
_fixture_url: str,
_profile_dir: str,
) -> dict[str, object]:
return {
"browser_version": namespace["PINNED_CHROME_VERSION"],
"forced_close_detected": True,
"session_survived": True,
"driver_process_terminated": True,
"driver_kill_fallback_used": True,
"browser_process_terminated": True,
"chromium_process_set_terminated": True,
}

trial.__globals__["_run_agent_task_forced_close_browser_pass"] = fake_browser_pass
result = trial(
pathlib.Path("/unused/chrome"),
pathlib.Path("/unused/chromedriver"),
"http://127.0.0.1/fixture",
3,
)

self.assertIs(result["passed"], True)
self.assertIs(result["driver_process_terminated"], True)
self.assertIs(result["driver_kill_fallback_used"], True)
self.assertIs(result["browser_process_terminated"], True)
self.assertIs(result["chromium_process_set_terminated"], True)
self.assertIs(result["profile_cleaned"], True)


if __name__ == "__main__":
Expand Down
120 changes: 120 additions & 0 deletions tests/test_agent_task_forced_close_session_cleanup_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Contract for truthful WebDriver session cleanup in the forced-close Agent Task lane."""

from __future__ import annotations

import json
import pathlib
import runpy
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"


class AgentTaskForcedCloseSessionCleanupContractTests(unittest.TestCase):
"""Require reviewed session-delete failures to remain explicit failure evidence."""

def test_session_delete_helper_is_bounded_typed_and_source_free(self) -> None:
"""A reviewed WebDriver cleanup failure must return only its stable exception type."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_session_cleanup_contract"
)
cleanup = namespace["_delete_webdriver_session_bounded"]
original_request = cleanup.__globals__["_json_request"]
calls: list[tuple[int, str, str, dict[str, object]]] = []

def successful_request(
driver_port: int,
method: str,
path: str,
payload: dict[str, object],
) -> dict[str, object]:
calls.append((driver_port, method, path, payload))
return {"value": None}

cleanup.__globals__["_json_request"] = successful_request
try:
self.assertIsNone(cleanup(9515, "session-1"))
finally:
cleanup.__globals__["_json_request"] = original_request
self.assertEqual(calls, [(9515, "DELETE", "/session/session-1", {})])

for exception in (
OSError("raw-io-detail"),
ValueError("raw-value-detail"),
RuntimeError("raw-runtime-detail"),
json.JSONDecodeError("raw-json-detail", "x", 0),
):
with self.subTest(exception_type=type(exception).__name__):
def failing_request(*_args: object, **_kwargs: object) -> dict[str, object]:
raise exception

cleanup.__globals__["_json_request"] = failing_request
try:
failure_type = cleanup(9515, "session-1")
finally:
cleanup.__globals__["_json_request"] = original_request
self.assertEqual(failure_type, type(exception).__name__)
self.assertNotIn("raw-", failure_type)

def test_forced_close_pass_does_not_suppress_session_cleanup_failure(self) -> None:
"""The forced-close failure envelope must consume typed session cleanup evidence."""

runner = RUNNER.read_text(encoding="utf-8")
start = runner.index("def _run_agent_task_forced_close_browser_pass(")
end = runner.index("\ndef _run_agent_task_forced_close_trial(", start)
browser_pass = runner[start:end]

self.assertNotIn("contextlib.suppress(Exception)", browser_pass)
self.assertIn("session_cleanup_failure_type", browser_pass)
cleanup_call = browser_pass.index("_delete_webdriver_session_bounded(")
self.assertIn("driver_port, session_id", browser_pass[cleanup_call:cleanup_call + 160])
self.assertIn('"session_cleanup_failure_type"', browser_pass)
self.assertIn('"WebDriverSessionCleanupError"', browser_pass)

def test_trial_preserves_session_cleanup_failure_separately_from_driver_cleanup(self) -> None:
"""Browser, session-delete, and driver-process failures must remain distinguishable."""

namespace = runpy.run_path(
str(RUNNER), run_name="forced_close_session_cleanup_trial_contract"
)
trial = namespace["_run_agent_task_forced_close_trial"]

def fake_browser_pass(
_chrome_bin: pathlib.Path,
_chromedriver_bin: pathlib.Path,
_fixture_url: str,
_profile_dir: str,
) -> dict[str, object]:
return {
"failure_type": "RuntimeError",
"session_cleanup_failure_type": "OSError",
"cleanup_failure_type": "TimeoutExpired",
"driver_process_terminated": False,
"driver_kill_fallback_used": True,
"browser_process_terminated": True,
"chromium_process_set_terminated": True,
}

trial.__globals__["_run_agent_task_forced_close_browser_pass"] = fake_browser_pass
result = trial(
pathlib.Path("/unused/chrome"),
pathlib.Path("/unused/chromedriver"),
"http://127.0.0.1/fixture",
4,
)

self.assertIs(result["passed"], False)
self.assertEqual(result["failure_type"], "RuntimeError")
self.assertEqual(result["session_cleanup_failure_type"], "OSError")
self.assertEqual(result["cleanup_failure_type"], "TimeoutExpired")
self.assertIs(result["driver_process_terminated"], False)
self.assertIs(result["driver_kill_fallback_used"], True)
self.assertIs(result["browser_process_terminated"], True)
self.assertIs(result["chromium_process_set_terminated"], True)
self.assertIs(result["profile_cleaned"], True)


if __name__ == "__main__":
unittest.main()
Loading
Loading