agentenv-framework-protocol 0.1.439__tar.gz → 0.1.441__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/PKG-INFO +5 -3
  2. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/README.md +4 -2
  3. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/checks.py +15 -8
  5. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/website_browser.py +21 -2
  6. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_conformance.py +34 -3
  7. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_conformance_checks.py +1 -1
  8. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_website_browser.py +36 -1
  9. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/.gitignore +0 -0
  10. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/LICENSE +0 -0
  11. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/NOTICE +0 -0
  12. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/THIRD_PARTY_NOTICES.md +0 -0
  13. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/__init__.py +0 -0
  14. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/advanced_agent.py +0 -0
  15. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/basic_agent.py +0 -0
  16. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/custom_extensions_agent.py +0 -0
  17. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/multimodal_agent.py +0 -0
  18. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/reference_model.py +0 -0
  19. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/examples/streaming_agent.py +0 -0
  20. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/__init__.py +0 -0
  21. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  22. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  23. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  24. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  25. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  26. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  27. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  28. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  29. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  30. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  31. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  32. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/agent_env_environment.py +0 -0
  33. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/client.py +0 -0
  34. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/__init__.py +0 -0
  35. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/_declare.py +0 -0
  36. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/_linux.py +0 -0
  37. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/_load.py +0 -0
  38. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  39. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/app.py +0 -0
  40. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/app_data.py +0 -0
  41. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/app_runtime.py +0 -0
  42. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/apps.py +0 -0
  43. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/card.py +0 -0
  44. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/checks.py +0 -0
  45. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  46. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  47. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  48. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  49. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  50. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  51. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/dataplane.py +0 -0
  52. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/driver.py +0 -0
  53. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/errors.py +0 -0
  54. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/extensions.py +0 -0
  55. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/frames.py +0 -0
  56. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/keys.py +0 -0
  57. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/launch.py +0 -0
  58. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/manifest.py +0 -0
  59. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/operations.py +0 -0
  60. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/platform.py +0 -0
  61. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/queue.py +0 -0
  62. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/recording.py +0 -0
  63. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/results.py +0 -0
  64. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  65. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/runtime.py +0 -0
  66. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/serve.py +0 -0
  67. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/serve_app.py +0 -0
  68. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/session.py +0 -0
  69. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/settings.py +0 -0
  70. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/skill.py +0 -0
  71. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/spaces.py +0 -0
  72. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/computer/tools.py +0 -0
  73. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/errors.py +0 -0
  74. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/manifest.py +0 -0
  75. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/preflight.py +0 -0
  76. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/tool_access.py +0 -0
  77. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/tool_client.py +0 -0
  78. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/tool_context.py +0 -0
  79. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/transfers.py +0 -0
  80. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/src/agentenv_protocol/types.py +0 -0
  81. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/computer_fake.py +0 -0
  82. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/probe_fake.py +0 -0
  83. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_agent.py +0 -0
  84. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_agent_examples.py +0 -0
  85. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_cdp_config.py +0 -0
  86. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_object_transfers.py +0 -0
  87. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_peer_agents.py +0 -0
  88. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_proxy.py +0 -0
  89. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_staging.py +0 -0
  90. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_a2a_trajectory_log.py +0 -0
  91. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_agent_env_environment.py +0 -0
  92. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_client.py +0 -0
  93. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_app_data.py +0 -0
  94. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_app_server.py +0 -0
  95. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_conformance_recording.py +0 -0
  96. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_dataplane.py +0 -0
  97. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_declare.py +0 -0
  98. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_driver.py +0 -0
  99. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_extensions.py +0 -0
  100. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_frames.py +0 -0
  101. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_queue.py +0 -0
  102. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_recording.py +0 -0
  103. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_serve.py +0 -0
  104. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_computer_spaces.py +0 -0
  105. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_control_token.py +0 -0
  106. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_data_objects.py +0 -0
  107. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_errors.py +0 -0
  108. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_extension_methods.py +0 -0
  109. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_intake.py +0 -0
  110. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_manifest.py +0 -0
  111. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_preflight.py +0 -0
  112. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_tool_access.py +0 -0
  113. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_tool_annotations.py +0 -0
  114. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_tool_client.py +0 -0
  115. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_tool_context.py +0 -0
  116. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_tool_context_fastmcp.py +0 -0
  117. {agentenv_framework_protocol-0.1.439 → agentenv_framework_protocol-0.1.441}/tests/test_tool_context_injection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.439
3
+ Version: 0.1.441
4
4
  Summary: Open data-plane protocol and server SDK for agent environments.
5
5
  Project-URL: Homepage, https://www.agentenvframework.com
6
6
  Project-URL: Documentation, https://www.agentenvframework.com/docs
@@ -846,8 +846,10 @@ are the profile's template. The base class adds:
846
846
  the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
847
847
  `devtools_answers` asks `Browser.getVersion` over the pipe.
848
848
  - **Its control connection**, `self.control`, a `BrowserControl` over the pipe. At each launch it attaches
849
- to every page with flattened sessions and keeps a record, `since` its server started: open tabs in
850
- opened order, the one whose page has focus `active`; `closed_tabs`, each with `closed_at` and
849
+ to every page with flattened sessions, telling each page it attached to as it opened, a popup say, to run, and keeps a
850
+ record, `since` its server started: open tabs in opened order, each with its URL and title as the browser has them
851
+ when the record is read (Chrome sends no event for a title), the one whose page has focus `active`; `closed_tabs`,
852
+ each as of its page's last load, with `closed_at` and
851
853
  `closed_by` (`agentenv`, over this connection; `unknown`, anyone else; `exit` or `stop`, when the browser
852
854
  ended on its own or its server stopped it); `downloads`, into `~/Downloads`, each with its state
853
855
  and, completed, its size and sha256; and `dialogs`, each with its type, message, answer and who gave it.
@@ -790,8 +790,10 @@ are the profile's template. The base class adds:
790
790
  the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
791
791
  `devtools_answers` asks `Browser.getVersion` over the pipe.
792
792
  - **Its control connection**, `self.control`, a `BrowserControl` over the pipe. At each launch it attaches
793
- to every page with flattened sessions and keeps a record, `since` its server started: open tabs in
794
- opened order, the one whose page has focus `active`; `closed_tabs`, each with `closed_at` and
793
+ to every page with flattened sessions, telling each page it attached to as it opened, a popup say, to run, and keeps a
794
+ record, `since` its server started: open tabs in opened order, each with its URL and title as the browser has them
795
+ when the record is read (Chrome sends no event for a title), the one whose page has focus `active`; `closed_tabs`,
796
+ each as of its page's last load, with `closed_at` and
795
797
  `closed_by` (`agentenv`, over this connection; `unknown`, anyone else; `exit` or `stop`, when the browser
796
798
  ended on its own or its server stopped it); `downloads`, into `~/Downloads`, each with its state
797
799
  and, completed, its size and sha256; and `dialogs`, each with its type, message, answer and who gave it.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.439"
3
+ version = "0.1.441"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -13,6 +13,7 @@ import uuid
13
13
  from collections.abc import Awaitable, Callable
14
14
  from datetime import datetime
15
15
  from importlib import resources
16
+ from typing import Any
16
17
 
17
18
  from mcp import ClientSession
18
19
  from mcp.types import CallToolResult
@@ -152,6 +153,10 @@ class Probe:
152
153
  raise AssertionError(f"computer_screenshot failed: {result.content[0].text}")
153
154
  return result, image(result)
154
155
 
156
+ async def pixel(self, x: int, y: int) -> tuple[int, int, int]:
157
+ """The pixel at frame pixel (x, y) of a new frame."""
158
+ return (await self.shot())[1].getpixel((x, y))
159
+
155
160
  async def act(self, name: str, args: dict) -> None:
156
161
  result = await self.tool(name, args)
157
162
  if result.isError:
@@ -161,13 +166,15 @@ class Probe:
161
166
  out = await self.driver.run("cat", f"{STAGE}/state/state.json")
162
167
  return json.loads(out["stdout"])
163
168
 
164
- async def until(self, holds: Callable[[dict], bool]) -> tuple[bool, dict]:
165
- """Whether the fixture's state comes to satisfy ``holds`` within SETTLE_S, and the state it last had."""
169
+ async def until(self, holds: Callable[[Any], bool],
170
+ read: Callable[[], Awaitable[Any]] | None = None) -> tuple[bool, Any]:
171
+ """Whether what ``read`` gives, the fixture's state unless it's given, comes to satisfy ``holds`` within
172
+ SETTLE_S, and what it last gave."""
166
173
  deadline = time.monotonic() + SETTLE_S
167
174
  while True:
168
- state = await self.state()
169
- if holds(state) or time.monotonic() > deadline:
170
- return holds(state), state
175
+ seen = await (read or self.state)()
176
+ if holds(seen) or time.monotonic() > deadline:
177
+ return holds(seen), seen
171
178
  await asyncio.sleep(POLL_S)
172
179
 
173
180
  async def settled(self, holds: Callable[[dict], bool], show: Callable[[dict], object]) -> str | None:
@@ -588,8 +595,8 @@ class Probe:
588
595
  problem = await self.settled(lambda s: s["cells"].get("2,3") == 1, lambda s: s["cells"])
589
596
  if problem:
590
597
  return problem
591
- _, img = await self.shot()
592
- return None if img.getpixel((x, y)) == (255, 0, 0) else f"the next frame shows {img.getpixel((x, y))}"
598
+ shows, pixel = await self.until(lambda p: p == (255, 0, 0), lambda: self.pixel(x, y))
599
+ return None if shows else f"no frame within {SETTLE_S:g} s shows it: the last shows {pixel}"
593
600
 
594
601
  async def norm_space() -> str | None:
595
602
  x, y = cell(5, 5)
@@ -636,7 +643,7 @@ class Probe:
636
643
  return await self.settled(lambda s: s["saved_count"] == 1 and s["subscribed"],
637
644
  lambda s: (s["saved_count"], s["subscribed"]))
638
645
 
639
- await self.check("P4", "a click in frame space hits its cell, and the next frame shows it", frame_space)
646
+ await self.check("P4", "a click in frame space hits its cell, and a frame shows it", frame_space)
640
647
  if self.driver.declares(COMPUTER_SPACES):
641
648
  await self.check("P4", "a click in norm space hits its cell", norm_space, COMPUTER_SPACES)
642
649
  if "right" in buttons:
@@ -260,13 +260,26 @@ class BrowserControl:
260
260
  return visible
261
261
 
262
262
  async def record(self) -> dict:
263
- """The record ``since`` it started: open ``tabs``, each with whether it's ``active``, ``closed_tabs``,
264
- ``downloads`` and ``dialogs``."""
263
+ """The record ``since`` it started: open ``tabs``, each with its URL and title now and whether it's ``active``,
264
+ ``closed_tabs``, ``downloads`` and ``dialogs``."""
265
+ await self._refresh()
265
266
  active = await self.active_tab()
266
267
  return {"since": self.since, "tabs": [{**tab, "active": tab["id"] == active} for tab in self.tabs.values()],
267
268
  "closed_tabs": list(self.closed_tabs), "downloads": list(self.downloads.values()),
268
269
  "dialogs": list(self.dialog_log)}
269
270
 
271
+ async def _refresh(self, target: Optional[str] = None) -> None:
272
+ """The URL and title of the tab ``target``, or of every open tab, as the browser has them now: it sends no
273
+ event when a page's title changes."""
274
+ try:
275
+ infos = (await self.devtools.call("Target.getTargets", {"filter": PAGES},
276
+ timeout_s=ACTIVE_CHECK_TIMEOUT_S))["targetInfos"]
277
+ except (ConnectionError, DevToolsError, asyncio.TimeoutError, KeyError):
278
+ return
279
+ for info in infos:
280
+ if target in (None, info["targetId"]) and (tab := self.tabs.get(info["targetId"])) is not None:
281
+ tab.update(url=info.get("url", tab["url"]), title=info.get("title", tab["title"]))
282
+
270
283
  def _event(self, message: dict) -> None:
271
284
  handler = getattr(self, "_on_" + message.get("method", "").replace(".", "_"), None)
272
285
  if handler is not None:
@@ -295,10 +308,16 @@ class BrowserControl:
295
308
  if params["targetInfo"].get("type") == "page":
296
309
  self._sessions[params["sessionId"]] = params["targetInfo"]["targetId"]
297
310
  self._spawn(self.devtools.call("Page.enable", session_id=params["sessionId"]))
311
+ # A page attached as it opens, a popup say, waits for this before it navigates.
312
+ self._spawn(self.devtools.call("Runtime.runIfWaitingForDebugger", session_id=params["sessionId"]))
298
313
 
299
314
  def _on_Target_detachedFromTarget(self, params: dict, session: Optional[str]) -> None:
300
315
  self._sessions.pop(params["sessionId"], None)
301
316
 
317
+ def _on_Page_loadEventFired(self, params: dict, session: Optional[str]) -> None:
318
+ if (target := self._sessions.get(session)) is not None:
319
+ self._spawn(self._refresh(target))
320
+
302
321
  def _on_Page_javascriptDialogOpening(self, params: dict, session: Optional[str]) -> None:
303
322
  dialog = {"tab": self._sessions.get(session), "type": params.get("type"), "message": params.get("message", ""),
304
323
  "url": params.get("url", ""), "opened_at": _now(), "answer": None, "by": None}
@@ -85,12 +85,43 @@ async def test_a_driver_given_a_control_token_passes_with_it_and_refuses_calls_w
85
85
  assert not unauthorized.passed and "401 unauthorized" in _failed(unauthorized)[0]
86
86
 
87
87
 
88
+ class StaleFrame(FixtureDriver):
89
+ """Its first ``stale`` captures after a click are from before the click, as when an app records a click before it
90
+ repaints."""
91
+
92
+ def __init__(self, stale: int = 1, **kwargs) -> None:
93
+ super().__init__(**kwargs)
94
+ self.stale, self.left, self.before_click = stale, 0, None
95
+
96
+ @click
97
+ async def click_at(self, x, y, button, count, modifiers):
98
+ self.before_click, self.left = self.screen.image.copy(), self.stale
99
+ return await super().click_at(x, y, button, count, modifiers)
100
+
101
+ @capture
102
+ async def grab(self):
103
+ if not self.left:
104
+ return self.screen.image.copy()
105
+ self.left -= 1
106
+ return self.before_click.copy()
107
+
108
+
88
109
  @pytest.mark.asyncio
89
- async def test_a_driver_whose_default_space_is_norm_passes_at_full():
90
- report = await _probe(FixtureDriver(settings=ComputerSettings(default_space="norm")))
110
+ @pytest.mark.parametrize("driver", [lambda: FixtureDriver(settings=ComputerSettings(default_space="norm")), StaleFrame],
111
+ ids=["default-space-norm", "stale-first-frame"])
112
+ async def test_a_driver_whose_default_space_is_norm_or_whose_first_frame_after_a_click_is_stale_passes_at_full(driver):
113
+ report = await _probe(driver())
91
114
  assert report.passed, _failed(report)
92
115
 
93
116
 
117
+ @pytest.mark.asyncio
118
+ async def test_frames_that_never_show_a_click_the_fixture_saw_fail_p4_with_the_last_pixel(monkeypatch):
119
+ monkeypatch.setattr(checks, "SETTLE_S", 0.5)
120
+ report = await _probe(StaleFrame(stale=10**6))
121
+ failed = next(c for c in report.checks if c.name == "a click in frame space hits its cell, and a frame shows it")
122
+ assert (failed.status, failed.detail) == ("failed", "no frame within 0.5 s shows it: the last shows (255, 255, 255)")
123
+
124
+
94
125
  @pytest.mark.asyncio
95
126
  async def test_a_tool_already_hidden_from_the_probes_role_stays_hidden():
96
127
  with served(FixtureDriver()) as url:
@@ -195,7 +226,7 @@ class ExitZero(FixtureDriver):
195
226
  @pytest.mark.asyncio
196
227
  @pytest.mark.parametrize("driver, broken", [
197
228
  (NeverReady, "computer-ready reports ready"),
198
- (ClicksOff, "a click in frame space hits its cell, and the next frame shows it"),
229
+ (ClicksOff, "a click in frame space hits its cell, and a frame shows it"),
199
230
  (ShiftedCapture, "markers at odd and even pixels read back in frame pixels"),
200
231
  (AsciiOnly, "Unicode and mixed-case text arrives whole"),
201
232
  (SwappedScroll, "scrolls up, down, left and right each arrive that many times"),
@@ -229,7 +229,7 @@ async def test_effects_without_spaces_right_or_double_clicks_leave_those_out_and
229
229
  assert "a click in norm space hits its cell" not in got and "a right click arrives as one" not in got
230
230
  assert "a double click arrives as one" not in got and "norm points land on the same pixels" not in got
231
231
  assert got["16 clicks sent at once all land"] == ("failed", "16 of 16 were refused")
232
- assert got["a click in frame space hits its cell, and the next frame shows it"][1].startswith("AssertionError")
232
+ assert got["a click in frame space hits its cell, and a frame shows it"][1].startswith("AssertionError")
233
233
 
234
234
 
235
235
  @pytest.mark.asyncio
@@ -3,7 +3,8 @@ for each; a subclass refused for carrying one of them; the start tabs, the time
3
3
  its launch run with no desktop when alone; a CDP connection over a new debugging pipe at each launch, which its ready
4
4
  check asks, and a browser launched again when its server restarts; its shared memory in files only on a small
5
5
  /dev/shm; an empty cache and the deploy's prompt answers in its profile at each launch; its policies, card params and
6
- skill; and an app that isn't a browser launched and declared as before."""
6
+ skill; its record's titles read from the browser, which sends no event for a page's; and an app that isn't a browser
7
+ launched and declared as before."""
7
8
  from __future__ import annotations
8
9
 
9
10
  import asyncio
@@ -492,6 +493,40 @@ async def test_its_control_records_tabs_in_opened_order_and_who_closed_each_and_
492
493
  assert devtools.calls[2][1] == {"behavior": "default", "eventsEnabled": True}
493
494
 
494
495
 
496
+ @pytest.mark.asyncio
497
+ async def test_each_page_it_attaches_to_is_told_to_run_so_a_popup_it_attached_to_as_it_opened_navigates(tmp_path):
498
+ devtools = StubDevTools()
499
+ control = BrowserControl(devtools, "agent")
500
+ await control.attach()
501
+ devtools.send("Target.attachedToTarget", {"sessionId": "s-popup", "waitingForDebugger": True, **_page("popup")})
502
+ await asyncio.sleep(0)
503
+
504
+ assert ("Page.enable", {}, "s-popup") in devtools.calls
505
+ assert ("Runtime.runIfWaitingForDebugger", {}, "s-popup") in devtools.calls
506
+
507
+
508
+ @pytest.mark.asyncio
509
+ async def test_its_record_reads_each_tabs_title_when_read_and_when_its_page_loads_since_no_event_says(tmp_path):
510
+ now = {"a": "Inbox", "b": "Draft"}
511
+ devtools = StubDevTools({"Target.getTargets": lambda params, session: {"targetInfos": [
512
+ _page(target, f"https://mail.test/{target}", title)["targetInfo"] for target, title in now.items()]}})
513
+ control = BrowserControl(devtools, "agent")
514
+ await control.attach()
515
+ for target in ("a", "b"):
516
+ devtools.send("Target.targetCreated", _page(target, f"https://mail.test/{target}", f"mail.test/{target}"))
517
+ devtools.send("Target.attachedToTarget", {"sessionId": f"s-{target}", **_page(target)})
518
+ devtools.send("Page.loadEventFired", {"timestamp": 1}, session="s-b")
519
+ await asyncio.sleep(0)
520
+ del now["b"]
521
+ devtools.send("Target.targetDestroyed", {"targetId": "b"})
522
+ now["a"] = "Inbox (1)"
523
+ record = await control.record()
524
+
525
+ assert [(tab["id"], tab["title"]) for tab in record["tabs"]] == [("a", "Inbox (1)")]
526
+ assert [(tab["id"], tab["title"]) for tab in record["closed_tabs"]] == [("b", "Draft")]
527
+ assert ("Target.getTargets", {"filter": [{"type": "page"}]}, None) in devtools.calls
528
+
529
+
495
530
  @pytest.mark.asyncio
496
531
  @pytest.mark.parametrize("dialogs, kind, sent, answer, by", [
497
532
  ("accept", "prompt", {"accept": True, "promptText": "yes"}, "accepted", "agentenv"),