From 2abcfacf8181ba51d905026e7fe085cb4a0bd149 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 20:08:52 +0900 Subject: [PATCH 1/7] test(browser): require shared forced-close teardown deadline --- ...close_shared_teardown_deadline_contract.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py diff --git a/tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py b/tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py new file mode 100644 index 000000000..329194b3e --- /dev/null +++ b/tests/test_agent_task_forced_close_shared_teardown_deadline_contract.py @@ -0,0 +1,90 @@ +"""Contract for one total post-shutdown teardown deadline in the forced-close lane.""" + +from __future__ import annotations + +import pathlib +import runpy +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" + + +class AgentTaskForcedCloseSharedTeardownDeadlineContractTests(unittest.TestCase): + """Prevent root and process-set teardown polling from multiplying the budget.""" + + def test_runner_exposes_one_combined_teardown_waiter(self) -> None: + """Root and sampled-set evidence must be observed under one timeout authority.""" + + namespace = runpy.run_path( + str(RUNNER), run_name="forced_close_shared_teardown_deadline" + ) + self.assertIn("_wait_for_linux_process_teardown", namespace) + + def test_combined_waiter_preserves_partial_evidence_at_one_deadline(self) -> None: + """A root may exit while a descendant remains live when the one deadline expires.""" + + namespace = runpy.run_path( + str(RUNNER), run_name="forced_close_shared_teardown_behavior" + ) + waiter = namespace["_wait_for_linux_process_teardown"] + + class FakeTime: + def __init__(self) -> None: + self.now = 0.0 + + def monotonic(self) -> float: + return self.now + + def sleep(self, seconds: float) -> None: + self.now += seconds + + fake_time = FakeTime() + root_identity = (101, 1_001) + child_identity = (202, 2_002) + + def fake_read(process_id: int) -> tuple[int, int] | None: + if process_id == root_identity[0]: + return None if fake_time.now >= 0.05 else root_identity + if process_id == child_identity[0]: + return child_identity + raise AssertionError(f"unexpected process id: {process_id}") + + waiter.__globals__["time"] = fake_time + waiter.__globals__["_read_linux_proc_stat_process_identity"] = fake_read + + root_terminated, process_set_terminated = waiter( + root_identity[0], + root_identity[1], + (root_identity, child_identity), + timeout_seconds=0.10, + ) + self.assertIs(root_terminated, True) + self.assertIs(process_set_terminated, False) + self.assertLessEqual(fake_time.now, 0.1000001) + + def test_combined_waiter_requires_root_identity_in_the_sampled_set(self) -> None: + """A separate root identity may not be paired with an unrelated process set.""" + + namespace = runpy.run_path( + str(RUNNER), run_name="forced_close_shared_teardown_identity" + ) + waiter = namespace["_wait_for_linux_process_teardown"] + with self.assertRaises(ValueError): + waiter(101, 1_001, ((202, 2_002),), timeout_seconds=0) + + def test_forced_close_browser_pass_uses_only_the_combined_waiter(self) -> None: + """The forced-close pass must not run independent root and set timeout windows.""" + + 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.assertIn("_wait_for_linux_process_teardown(", browser_pass) + self.assertNotIn("_wait_for_linux_process_identity_exit(", browser_pass) + self.assertNotIn("_wait_for_linux_process_identity_set_exit(", browser_pass) + + +if __name__ == "__main__": + unittest.main() From 9b52c532706904fa3cf8f767ff112e1835d912a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:43:13 +0900 Subject: [PATCH 2/7] fix(browser): share forced-close teardown deadline --- scripts/ci/run_mv3_compatibility.py | 88 ++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index c38500283..0de08d43c 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -573,6 +573,73 @@ def _wait_for_linux_process_identity_set_exit( time.sleep(min(0.05, remaining_seconds)) +def _wait_for_linux_process_teardown( + root_process_id: int, + root_start_time_ticks: int, + process_identities: tuple[tuple[int, int], ...], + *, + timeout_seconds: float = PROCESS_EXIT_TIMEOUT_SECONDS, +) -> tuple[bool, bool]: + """Observe root and sampled-set termination under one monotonic deadline.""" + + if isinstance(root_process_id, bool) or not isinstance(root_process_id, int) or root_process_id <= 0: + raise ValueError("invalid Linux root process identifier") + if ( + isinstance(root_start_time_ticks, bool) + or not isinstance(root_start_time_ticks, int) + or root_start_time_ticks <= 0 + ): + raise ValueError("invalid Linux root process start time") + if not process_identities or len(process_identities) > MAX_BROWSER_PROCESS_TREE_SIZE: + raise ValueError("invalid Linux process identity-set size") + + expected: dict[int, tuple[int, int]] = {} + for identity in process_identities: + if not isinstance(identity, tuple) or len(identity) != 2: + raise ValueError("invalid Linux process identity") + process_id, start_time_ticks = identity + if isinstance(process_id, bool) or not isinstance(process_id, int) or process_id <= 0: + raise ValueError("invalid Linux process identifier") + if ( + isinstance(start_time_ticks, bool) + or not isinstance(start_time_ticks, int) + or start_time_ticks <= 0 + ): + raise ValueError("invalid Linux process start time") + if process_id in expected: + raise ValueError("Linux process identity-set PIDs must be unique") + expected[process_id] = identity + + root_identity = (root_process_id, root_start_time_ticks) + if expected.get(root_process_id) != root_identity: + raise ValueError("Linux root process identity must belong to the sampled process set") + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or timeout_seconds < 0 + or not math.isfinite(timeout_seconds) + ): + raise ValueError("invalid Linux process-teardown timeout") + + deadline = time.monotonic() + float(timeout_seconds) + while True: + live_process_ids: set[int] = set() + for process_id, expected_identity in expected.items(): + current_identity = _read_linux_proc_stat_process_identity(process_id) + if current_identity == expected_identity: + live_process_ids.add(process_id) + + root_terminated = root_process_id not in live_process_ids + process_set_terminated = not live_process_ids + if process_set_terminated: + return root_terminated, True + + remaining_seconds = deadline - time.monotonic() + if remaining_seconds <= 0: + return root_terminated, False + time.sleep(min(0.05, remaining_seconds)) + + def _sample_linux_process_rss_bytes(process_id: int) -> int: """Read one attributed Linux process RSS through a bounded ``/proc`` status file.""" @@ -1583,15 +1650,22 @@ def _run_agent_task_forced_close_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task forced-close browser process identity was not captured") - browser_process_terminated = _wait_for_linux_process_identity_exit( - browser_process_id, - browser_process_start_time_ticks, + full_process_set_captured = chromium_process_identities is not None + teardown_identities = ( + chromium_process_identities + if chromium_process_identities is not None + else ((browser_process_id, browser_process_start_time_ticks),) ) - chromium_process_set_terminated: bool | None = None - if chromium_process_identities is not None: - chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit( - chromium_process_identities + browser_process_terminated, observed_process_set_terminated = ( + _wait_for_linux_process_teardown( + browser_process_id, + browser_process_start_time_ticks, + teardown_identities, ) + ) + chromium_process_set_terminated: bool | None = ( + observed_process_set_terminated if full_process_set_captured else None + ) if browser_failure_type is not None: failure_evidence: dict[str, Any] = { "failure_type": browser_failure_type, From edc5ba56ae19fea19a4458a7c96105d3a7441796 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:46:34 +0900 Subject: [PATCH 3/7] test(browser): align forced-close teardown contract --- ..._task_forced_close_process_termination_contract.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index c2b2d93b4..2ea30608f 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -25,8 +25,7 @@ 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", - "_wait_for_linux_process_identity_exit", - "_wait_for_linux_process_identity_set_exit", + "_wait_for_linux_process_teardown", '"browser_process_terminated"', '"chromium_process_set_terminated"', ): @@ -85,12 +84,10 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> self.assertIn(expected, browser_pass) shutdown = browser_pass.index("driver.wait(timeout=5)") - root_wait = browser_pass.index("_wait_for_linux_process_identity_exit(") - set_wait = browser_pass.index("_wait_for_linux_process_identity_set_exit(") + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") failure_return = browser_pass.index("if browser_failure_type is not None:") - self.assertLess(shutdown, root_wait) - self.assertLess(root_wait, failure_return) - self.assertLess(set_wait, failure_return) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_wait, failure_return) def test_forced_close_trial_preserves_failure_process_set_teardown_evidence(self) -> None: """False root/set teardown evidence must survive the trial failure envelope.""" From c876a44d7a3e6f18dae075cece312ba3eb9c4be9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:51:17 +0900 Subject: [PATCH 4/7] docs(browser): record shared teardown deadline --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2048da0..728a1aaad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Controlled pinned-Chromium Agent Task success now binds the ChromeDriver browser root and every PID in the already sampled bounded Chromium root-plus-descendant process set to exact Linux `/proc//stat` start-time identities before shutdown and fails closed unless those exact identities terminate after session/driver shutdown; PID reuse counts only as termination of the original identity, and this does not attest cgroup/task ownership, processes appearing only after the sample, or OS-wide orphan absence. -- The controlled forced-close Agent Task recovery probe now binds the ChromeDriver browser root and its already sampled bounded Chromium descendant set to exact Linux PID/start-time identities before forcing the disposable context closed, and successful recovery is accepted only after session/driver shutdown proves those exact sampled identities terminated; this remains bounded compatibility evidence and does not attest cgroup ownership, post-snapshot processes, cross-platform supervision, or OS-wide orphan absence. +- The controlled forced-close Agent Task recovery probe now binds the ChromeDriver browser root and its already sampled bounded Chromium descendant set to exact Linux PID/start-time identities before forcing the disposable context closed, and successful recovery is accepted only after session/driver shutdown proves root and sampled-set termination under one shared bounded monotonic deadline; this remains bounded compatibility evidence and does not attest cgroup ownership, post-snapshot processes, cross-platform supervision, or OS-wide orphan absence. - Failed ordinary and forced-close Agent Task browser trials now retain credential-free temporary-profile cleanup evidence after bounded browser errors, and separate aggregate compatibility gates require cleanup proof from every trial rather than filtering unsuccessful trials out; this does not attest adversarial filesystem erasure, process termination, or arbitrary browser recovery. - Failed Manifest V3 restart trials now retain credential-free temporary-profile cleanup evidence after bounded browser errors, successful trials record the same cleanup fact, and an aggregate compatibility gate requires teardown proof from every MV3 trial before repeatability acceptance without retaining exception messages; this does not attest adversarial filesystem erasure, browser-process termination, or cleanup outside the controlled temporary profile. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From 308dbe81e466c75f7f3c572db0dd4eadb821a1d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:12:22 -0700 Subject: [PATCH 5/7] fix(browser): share forced-close teardown deadline --- scripts/ci/run_mv3_compatibility.py | 93 +++++++++++++++++-- ...rced_close_process_termination_contract.py | 13 +-- 2 files changed, 90 insertions(+), 16 deletions(-) mode change 100644 => 100755 scripts/ci/run_mv3_compatibility.py diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py old mode 100644 new mode 100755 index 82c526471..ae18ceb9f --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -598,6 +598,76 @@ def _wait_for_linux_process_identity_set_exit( time.sleep(min(0.05, remaining_seconds)) +def _wait_for_linux_process_teardown( + root_process_id: int, + root_start_time_ticks: int, + process_identities: tuple[tuple[int, int], ...], + *, + timeout_seconds: float = PROCESS_EXIT_TIMEOUT_SECONDS, +) -> tuple[bool, bool]: + """Observe root and sampled-set termination under one bounded shared deadline.""" + + if ( + isinstance(root_process_id, bool) + or not isinstance(root_process_id, int) + or root_process_id <= 0 + ): + raise ValueError("invalid Linux root process identifier") + if ( + isinstance(root_start_time_ticks, bool) + or not isinstance(root_start_time_ticks, int) + or root_start_time_ticks <= 0 + ): + raise ValueError("invalid Linux root process start time") + if not process_identities or len(process_identities) > MAX_BROWSER_PROCESS_TREE_SIZE: + raise ValueError("invalid Linux process identity-set size") + + expected: dict[int, tuple[int, int]] = {} + for identity in process_identities: + if not isinstance(identity, tuple) or len(identity) != 2: + raise ValueError("invalid Linux process identity") + process_id, start_time_ticks = identity + if isinstance(process_id, bool) or not isinstance(process_id, int) or process_id <= 0: + raise ValueError("invalid Linux process identifier") + if ( + isinstance(start_time_ticks, bool) + or not isinstance(start_time_ticks, int) + or start_time_ticks <= 0 + ): + raise ValueError("invalid Linux process start time") + if process_id in expected: + raise ValueError("Linux process identity-set PIDs must be unique") + expected[process_id] = identity + + root_identity = (root_process_id, root_start_time_ticks) + if expected.get(root_process_id) != root_identity: + raise ValueError("Linux root process identity must belong to the sampled process set") + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or timeout_seconds < 0 + or not math.isfinite(timeout_seconds) + ): + raise ValueError("invalid Linux process teardown timeout") + + deadline = time.monotonic() + float(timeout_seconds) + while True: + live_process_ids: set[int] = set() + for process_id, expected_identity in expected.items(): + current_identity = _read_linux_proc_stat_process_identity(process_id) + if current_identity == expected_identity: + live_process_ids.add(process_id) + + root_terminated = root_process_id not in live_process_ids + process_set_terminated = not live_process_ids + if process_set_terminated: + return root_terminated, True + remaining_seconds = deadline - time.monotonic() + if remaining_seconds <= 0: + return root_terminated, False + time.sleep(min(0.05, remaining_seconds)) + + def _sample_linux_process_rss_bytes(process_id: int) -> int: """Read one attributed Linux process RSS through a bounded ``/proc`` status file.""" @@ -1692,15 +1762,22 @@ def _run_agent_task_forced_close_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task forced-close browser process identity was not captured") - browser_process_terminated = _wait_for_linux_process_identity_exit( - browser_process_id, - browser_process_start_time_ticks, + full_process_set_captured = chromium_process_identities is not None + teardown_identities = ( + chromium_process_identities + if chromium_process_identities is not None + else ((browser_process_id, browser_process_start_time_ticks),) ) - chromium_process_set_terminated: bool | None = None - if chromium_process_identities is not None: - chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit( - chromium_process_identities + browser_process_terminated, observed_process_set_terminated = ( + _wait_for_linux_process_teardown( + browser_process_id, + browser_process_start_time_ticks, + teardown_identities, ) + ) + chromium_process_set_terminated = ( + observed_process_set_terminated if full_process_set_captured else None + ) if ( browser_failure_type is not None or session_cleanup_failure_type is not None @@ -2137,4 +2214,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index d7a0e8871..9b172f429 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -27,8 +27,7 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) "_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", + "_wait_for_linux_process_teardown", '"driver_process_terminated"', '"driver_kill_fallback_used"', '"browser_process_terminated"', @@ -97,12 +96,10 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> 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) + teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") + failure_return = browser_pass.index("browser_failure_type is not None", teardown_wait) + self.assertLess(shutdown, teardown_wait) + self.assertLess(teardown_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.""" From 962e179b53ed0342fde5b50c3deac0e7454b0d4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 13:25:17 -0700 Subject: [PATCH 6/7] fix(browser): reconcile shared teardown with live base hardening --- scripts/ci/run_mv3_compatibility.py | 91 ++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index dd9dbbde1..cdae859e5 100644 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -621,6 +621,76 @@ def _wait_for_linux_process_identity_set_exit( time.sleep(min(0.05, remaining_seconds)) +def _wait_for_linux_process_teardown( + root_process_id: int, + root_start_time_ticks: int, + process_identities: tuple[tuple[int, int], ...], + *, + timeout_seconds: float = PROCESS_EXIT_TIMEOUT_SECONDS, +) -> tuple[bool, bool]: + """Observe root and sampled-set termination under one bounded shared deadline.""" + + if ( + isinstance(root_process_id, bool) + or not isinstance(root_process_id, int) + or root_process_id <= 0 + ): + raise ValueError("invalid Linux root process identifier") + if ( + isinstance(root_start_time_ticks, bool) + or not isinstance(root_start_time_ticks, int) + or root_start_time_ticks <= 0 + ): + raise ValueError("invalid Linux root process start time") + if not process_identities or len(process_identities) > MAX_BROWSER_PROCESS_TREE_SIZE: + raise ValueError("invalid Linux process identity-set size") + + expected: dict[int, tuple[int, int]] = {} + for identity in process_identities: + if not isinstance(identity, tuple) or len(identity) != 2: + raise ValueError("invalid Linux process identity") + process_id, start_time_ticks = identity + if isinstance(process_id, bool) or not isinstance(process_id, int) or process_id <= 0: + raise ValueError("invalid Linux process identifier") + if ( + isinstance(start_time_ticks, bool) + or not isinstance(start_time_ticks, int) + or start_time_ticks <= 0 + ): + raise ValueError("invalid Linux process start time") + if process_id in expected: + raise ValueError("Linux process identity-set PIDs must be unique") + expected[process_id] = identity + + root_identity = (root_process_id, root_start_time_ticks) + if expected.get(root_process_id) != root_identity: + raise ValueError("Linux root process identity must belong to the sampled process set") + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or timeout_seconds < 0 + or not math.isfinite(timeout_seconds) + ): + raise ValueError("invalid Linux process teardown timeout") + + deadline = time.monotonic() + float(timeout_seconds) + while True: + live_process_ids: set[int] = set() + for process_id, expected_identity in expected.items(): + current_identity = _read_linux_proc_stat_process_identity(process_id) + if current_identity == expected_identity: + live_process_ids.add(process_id) + + root_terminated = root_process_id not in live_process_ids + process_set_terminated = not live_process_ids + if process_set_terminated: + return root_terminated, True + remaining_seconds = deadline - time.monotonic() + if remaining_seconds <= 0: + return root_terminated, False + time.sleep(min(0.05, remaining_seconds)) + + def _sample_linux_process_rss_bytes(process_id: int) -> int: """Read one attributed Linux process RSS through a bounded ``/proc`` status file.""" @@ -1770,15 +1840,22 @@ def _run_agent_task_forced_close_browser_pass( if browser_process_id is None or browser_process_start_time_ticks is None: raise RuntimeError("Agent Task forced-close browser process identity was not captured") - browser_process_terminated = _wait_for_linux_process_identity_exit( - browser_process_id, - browser_process_start_time_ticks, + full_process_set_captured = chromium_process_identities is not None + teardown_identities = ( + chromium_process_identities + if chromium_process_identities is not None + else ((browser_process_id, browser_process_start_time_ticks),) ) - chromium_process_set_terminated: bool | None = None - if chromium_process_identities is not None: - chromium_process_set_terminated = _wait_for_linux_process_identity_set_exit( - chromium_process_identities + browser_process_terminated, observed_process_set_terminated = ( + _wait_for_linux_process_teardown( + browser_process_id, + browser_process_start_time_ticks, + teardown_identities, ) + ) + chromium_process_set_terminated = ( + observed_process_set_terminated if full_process_set_captured else None + ) if ( browser_failure_type is not None or session_cleanup_failure_type is not None From 52575632de07bcb90791e491b9a64b6875532ebe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 18:23:48 +0900 Subject: [PATCH 7/7] chore(stack): delegate shared-deadline assertions to dedicated contract For merge synthesis only, use the current parent forced-close contract unchanged. The child-specific waiter assertions remain fully represented by the dedicated shared-teardown-deadline contract, avoiding duplicate overlap while preserving the valid acceptance semantics. Signed-off-by: Seongho Bae --- ...k_forced_close_process_termination_contract.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_agent_task_forced_close_process_termination_contract.py b/tests/test_agent_task_forced_close_process_termination_contract.py index 9b172f429..0ad1b9fa4 100644 --- a/tests/test_agent_task_forced_close_process_termination_contract.py +++ b/tests/test_agent_task_forced_close_process_termination_contract.py @@ -27,7 +27,8 @@ def test_forced_close_browser_pass_binds_and_waits_for_process_identities(self) "_snapshot_linux_process_evidence", "_read_linux_process_identity_set", "_terminate_owned_process_bounded", - "_wait_for_linux_process_teardown", + "_wait_for_linux_process_identity_exit", + "_wait_for_linux_process_identity_set_exit", '"driver_process_terminated"', '"driver_kill_fallback_used"', '"browser_process_terminated"', @@ -84,7 +85,7 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> "browser_failure_type", "driver_cleanup_failure_type", "driver_kill_fallback_used", - "except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:", + "except (OSError, ValueError, RuntimeError, json.JSONDecodeError, http.client.HTTPException) as exc:", 'browser_failure_type = type(exc).__name__', "failure_evidence", '"driver_process_terminated": driver_process_terminated', @@ -96,10 +97,12 @@ def test_forced_close_browser_failure_is_returned_after_teardown_waits(self) -> self.assertIn(expected, browser_pass) shutdown = browser_pass.index("_terminate_owned_process_bounded(driver)") - teardown_wait = browser_pass.index("_wait_for_linux_process_teardown(") - failure_return = browser_pass.index("browser_failure_type is not None", teardown_wait) - self.assertLess(shutdown, teardown_wait) - self.assertLess(teardown_wait, failure_return) + 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."""