flowproof 0.23.4__tar.gz → 0.23.6__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 (167) hide show
  1. {flowproof-0.23.4 → flowproof-0.23.6}/Cargo.lock +7 -7
  2. {flowproof-0.23.4 → flowproof-0.23.6}/Cargo.toml +1 -1
  3. {flowproof-0.23.4 → flowproof-0.23.6}/PKG-INFO +1 -1
  4. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/web.rs +238 -60
  5. flowproof-0.23.6/crates/flowproof-adapters/tests/visibility_detach_e2e.rs +77 -0
  6. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/author.rs +24 -6
  7. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/Cargo.toml +4 -2
  8. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/lib.rs +21 -0
  9. flowproof-0.23.6/crates/flowproof-cli/tests/agent_url_record_e2e.rs +478 -0
  10. flowproof-0.23.6/crates/flowproof-cli/tests/sigterm_shared_browser_e2e.rs +153 -0
  11. {flowproof-0.23.4 → flowproof-0.23.6}/flowproof/__init__.py +1 -1
  12. {flowproof-0.23.4 → flowproof-0.23.6}/pyproject.toml +1 -1
  13. flowproof-0.23.4/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -216
  14. {flowproof-0.23.4 → flowproof-0.23.6}/README.md +0 -0
  15. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/Cargo.toml +0 -0
  16. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  17. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  18. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress.rs +0 -0
  19. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
  20. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  21. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  22. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
  23. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  24. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  25. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  26. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  27. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  28. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  29. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  30. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  31. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/fiori.rs +0 -0
  32. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  33. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/lib.rs +0 -0
  34. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  35. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  36. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  37. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  38. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/src/vision.rs +0 -0
  39. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/a11y_capture.rs +0 -0
  40. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
  41. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  42. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/grid_scene_e2e.rs +0 -0
  43. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/network_idle_e2e.rs +0 -0
  44. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/shared_browser_shutdown_e2e.rs +0 -0
  45. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/side_effect_http_e2e.rs +0 -0
  46. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/static_area_e2e.rs +0 -0
  47. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
  48. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/Cargo.toml +0 -0
  49. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  50. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/clarify.rs +0 -0
  51. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/doc_author.rs +0 -0
  52. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/doc_formats/hp_alm.rs +0 -0
  53. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/doc_formats.rs +0 -0
  54. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/draft_assembly.rs +0 -0
  55. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/heal.rs +0 -0
  56. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/lib.rs +0 -0
  57. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/llm.rs +0 -0
  58. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/recorder.rs +0 -0
  59. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/repair.rs +0 -0
  60. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/rules.rs +0 -0
  61. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/src/spec.rs +0 -0
  62. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
  63. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/skills/flowproof-config/SKILL.md +0 -0
  64. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/agent_flow.rs +0 -0
  65. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/capture.rs +0 -0
  66. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/config.rs +0 -0
  67. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/doctor.rs +0 -0
  68. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/main.rs +0 -0
  69. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/suite.rs +0 -0
  70. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/suite_checkpoint.rs +0 -0
  71. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/suite_inputs.rs +0 -0
  72. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/suite_journal.rs +0 -0
  73. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/src/update_check.rs +0 -0
  74. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/a11y_selector_e2e.rs +0 -0
  75. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/agent_conversation_anthropic_e2e.rs +0 -0
  76. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/agent_conversation_streaming_e2e.rs +0 -0
  77. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  78. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  79. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  80. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  81. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  82. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/config_cli_e2e.rs +0 -0
  83. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/config_seed_e2e.rs +0 -0
  84. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/config_skill_e2e.rs +0 -0
  85. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  86. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/doctor_ai_e2e.rs +0 -0
  87. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  88. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/documented_flags.rs +0 -0
  89. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  90. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
  91. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  92. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  93. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  94. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  95. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  96. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  97. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  98. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  99. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/falsifiability_side_effect.rs +0 -0
  100. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/fiori_field_commit_e2e.rs +0 -0
  101. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  102. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  103. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
  104. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  105. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  106. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  107. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  108. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  109. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  110. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  111. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  112. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  113. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  114. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  115. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  116. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/suite_checkpoint.rs +0 -0
  117. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  118. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  119. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  120. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/suite_policy.rs +0 -0
  121. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  122. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  123. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  124. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  125. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/Cargo.toml +0 -0
  126. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/app.rs +0 -0
  127. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/backend.rs +0 -0
  128. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/gdi.rs +0 -0
  129. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/lib.rs +0 -0
  130. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/mock.rs +0 -0
  131. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/oob.rs +0 -0
  132. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/recording.rs +0 -0
  133. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/redact.rs +0 -0
  134. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/surface.rs +0 -0
  135. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/visual.rs +0 -0
  136. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-driver/src/window.rs +0 -0
  137. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-python/Cargo.toml +0 -0
  138. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-python/src/lib.rs +0 -0
  139. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-replay/Cargo.toml +0 -0
  140. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-replay/src/lib.rs +0 -0
  141. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-replay/src/report.rs +0 -0
  142. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-replay/src/runrecord.rs +0 -0
  143. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
  144. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/Cargo.toml +0 -0
  145. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/schema/cassette-v1.schema.json +0 -0
  146. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/schema/side-effect-v1.schema.json +0 -0
  147. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
  148. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/captures.rs +0 -0
  149. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/cassette.rs +0 -0
  150. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  151. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/egress.rs +0 -0
  152. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/format.rs +0 -0
  153. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/lib.rs +0 -0
  154. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/secret.rs +0 -0
  155. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  156. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/side_effect.rs +0 -0
  157. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/substitution.rs +0 -0
  158. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  159. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/tests/cassette_conformance.rs +0 -0
  160. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/tests/fixtures/multi.trace.jsonl +0 -0
  161. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  162. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
  163. {flowproof-0.23.4 → flowproof-0.23.6}/crates/flowproof-trace/tests/side_effect_conformance.rs +0 -0
  164. {flowproof-0.23.4 → flowproof-0.23.6}/flowproof/cli.py +0 -0
  165. {flowproof-0.23.4 → flowproof-0.23.6}/flowproof/flow.py +0 -0
  166. {flowproof-0.23.4 → flowproof-0.23.6}/flowproof/mcp_server.py +0 -0
  167. {flowproof-0.23.4 → flowproof-0.23.6}/flowproof/py.typed +0 -0
@@ -930,7 +930,7 @@ dependencies = [
930
930
 
931
931
  [[package]]
932
932
  name = "flowproof-adapters"
933
- version = "0.23.4"
933
+ version = "0.23.6"
934
934
  dependencies = [
935
935
  "anyhow",
936
936
  "flowproof-driver",
@@ -949,7 +949,7 @@ dependencies = [
949
949
 
950
950
  [[package]]
951
951
  name = "flowproof-agent"
952
- version = "0.23.4"
952
+ version = "0.23.6"
953
953
  dependencies = [
954
954
  "chrono",
955
955
  "flowproof-driver",
@@ -967,7 +967,7 @@ dependencies = [
967
967
 
968
968
  [[package]]
969
969
  name = "flowproof-cli"
970
- version = "0.23.4"
970
+ version = "0.23.6"
971
971
  dependencies = [
972
972
  "ab_glyph",
973
973
  "chrono",
@@ -992,7 +992,7 @@ dependencies = [
992
992
 
993
993
  [[package]]
994
994
  name = "flowproof-driver"
995
- version = "0.23.4"
995
+ version = "0.23.6"
996
996
  dependencies = [
997
997
  "calamine",
998
998
  "flowproof-trace",
@@ -1008,7 +1008,7 @@ dependencies = [
1008
1008
 
1009
1009
  [[package]]
1010
1010
  name = "flowproof-python"
1011
- version = "0.23.4"
1011
+ version = "0.23.6"
1012
1012
  dependencies = [
1013
1013
  "flowproof-agent",
1014
1014
  "flowproof-cli",
@@ -1020,7 +1020,7 @@ dependencies = [
1020
1020
 
1021
1021
  [[package]]
1022
1022
  name = "flowproof-replay"
1023
- version = "0.23.4"
1023
+ version = "0.23.6"
1024
1024
  dependencies = [
1025
1025
  "chrono",
1026
1026
  "flowproof-agent",
@@ -1034,7 +1034,7 @@ dependencies = [
1034
1034
 
1035
1035
  [[package]]
1036
1036
  name = "flowproof-trace"
1037
- version = "0.23.4"
1037
+ version = "0.23.6"
1038
1038
  dependencies = [
1039
1039
  "jsonschema",
1040
1040
  "regex",
@@ -22,7 +22,7 @@ members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproo
22
22
  exclude = ["spike/windows-containment"]
23
23
 
24
24
  [workspace.package]
25
- version = "0.23.4"
25
+ version = "0.23.6"
26
26
  edition = "2021"
27
27
  license = "Apache-2.0"
28
28
  repository = "https://github.com/automators-com/flowproof"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowproof
3
- Version: 0.23.4
3
+ Version: 0.23.6
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -459,10 +459,23 @@ const FRAME_ACT: &str = r#"function(FRAME, CSS, ID, TEXT, OP, ARG){
459
459
  + '.urMnu, .lsPOMNContainer, [id^="ARIA_"], [id*="SAPGUI"]'
460
460
  );
461
461
  }
462
+ // The classic value-help ("F4") popup's own filter input is a document-
463
+ // level match for isSapWebGui() too (it renders inside the same WebGUI
464
+ // frame), but it does NOT commit on blur like an ordinary screen field -
465
+ // it commits on an explicit Go/search button click, which the recorded
466
+ // flow already does as its own separate step. Confirmed live: typing a
467
+ // value then tabbing out of one of these (the sap_webgui path's blur+
468
+ // verify dance) left the field empty rather than reformatted, failing a
469
+ // spec that never asked for a Tab here at all. `NSH`-prefixed ids and
470
+ // `SHLP`/search-help markers are SAP's own standard naming for this popup
471
+ // across systems, not something specific to one client's screens.
472
+ function isValueHelpPopupField(){
473
+ return !!el.closest('[id^="NSH"], [id*="SHLP"], [id*="F4Help"], [class*="shlp"]');
474
+ }
462
475
  if (OP === 'kind'){
463
476
  var nativeInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
464
477
  return (nativeInput ? 'native_input' : 'other') + ':'
465
- + (isSapWebGui() ? 'sap_webgui' : 'generic');
478
+ + (isSapWebGui() && !isValueHelpPopupField() ? 'sap_webgui' : 'generic');
466
479
  }
467
480
  if (OP === 'value'){
468
481
  var value = ('value' in el)
@@ -2078,27 +2091,38 @@ impl WebAppDriver {
2078
2091
  ))
2079
2092
  }
2080
2093
 
2081
- /// Run an element operation with ONE retry on a CDP transport fault:
2082
- /// re-resolve the element (its object id may be gone with the dead
2083
- /// connection) and try again before failing the step.
2094
+ // These probes run before dispatching any input. A control without a
2095
+ // box during a redraw is not ready; let the bounded actionability wait
2096
+ // re-resolve it, without retrying a click or masking transport errors.
2097
+ fn composed_readiness(&mut self, target: &UiaSelector) -> Result<Option<String>, DriverError> {
2098
+ match flowproof_driver::composed_actionability_gate(self, target) {
2099
+ Err(DriverError::Browser(message))
2100
+ if message.contains("box model of [")
2101
+ && message.contains("Could not compute box model") =>
2102
+ {
2103
+ Ok(Some(
2104
+ "not rendered (target has no box during redraw)".into(),
2105
+ ))
2106
+ }
2107
+ result => result,
2108
+ }
2109
+ }
2110
+
2111
+ /// Run an element operation with the shared transport-fault retry
2112
+ /// policy (see [`retry_on_transport_fault`]): re-resolve the element
2113
+ /// (its object id may be gone with the dead connection) and try again
2114
+ /// before failing the step.
2084
2115
  fn with_element<T>(
2085
2116
  &self,
2086
2117
  locator: &WebLocator,
2087
2118
  context: &str,
2088
2119
  op: impl Fn(&headless_chrome::Element<'_>) -> Result<T, anyhow::Error>,
2089
2120
  ) -> Result<T, DriverError> {
2090
- let mut retried = false;
2091
- loop {
2092
- let element = self.find(locator)?;
2093
- match op(&element) {
2094
- Ok(value) => return Ok(value),
2095
- Err(e) if !retried && is_transport_fault(&e.to_string()) => {
2096
- retried = true;
2097
- std::thread::sleep(Duration::from_millis(300));
2098
- }
2099
- Err(e) => return Err(web_err(context, e)),
2100
- }
2101
- }
2121
+ retry_on_transport_fault(|| {
2122
+ let element = self.find(locator).map_err(|e| e.to_string())?;
2123
+ op(&element).map_err(|e| e.to_string())
2124
+ })
2125
+ .map_err(|e| web_err(context, e))
2102
2126
  }
2103
2127
  }
2104
2128
 
@@ -2120,30 +2144,63 @@ fn is_transport_fault(message: &str) -> bool {
2120
2144
  || m.contains("unable to make method calls")
2121
2145
  }
2122
2146
 
2123
- /// Retry a fallible operation exactly once if its first failure looks like
2124
- /// a transient CDP transport fault - the same one-shot recovery
2125
- /// `with_element` already gives element-scoped calls (re-finding the
2126
- /// element and retrying its own op inline), generalized here for a call
2127
- /// that talks to the tab directly and has nothing to re-resolve. Pure
2128
- /// control flow, no CDP calls of its own, so it is unit-testable with a
2129
- /// mock closure rather than only provable against a real, flaky browser.
2130
- fn retry_once_on_transport_fault<T>(
2147
+ /// Attempts allowed beyond the first for a transport-faulting operation.
2148
+ /// Was 1 (a single fixed ~300ms retry) until issue #608: a clean live
2149
+ /// re-verification of `medium-02-change-info-record-readonly.flow.yaml`
2150
+ /// still hit "underlying connection is closed" 3 of 6 runs, including
2151
+ /// `frame_act` failing *twice in a row* even though it already had that one
2152
+ /// retry - proof a single ~300ms retry does not reliably bridge a real CDP
2153
+ /// outage.
2154
+ const TRANSPORT_FAULT_RETRIES: u32 = 3;
2155
+
2156
+ /// Backoff base: the wait before retry N is `N * TRANSPORT_FAULT_BACKOFF`
2157
+ /// (300ms, 600ms, 900ms), so the delay grows instead of hammering a
2158
+ /// connection that needs a moment to recover.
2159
+ const TRANSPORT_FAULT_BACKOFF: Duration = Duration::from_millis(300);
2160
+
2161
+ /// Retry a fallible operation on a transient CDP transport fault, backing
2162
+ /// off a little more between each attempt - the same recovery
2163
+ /// `with_element` gives element-scoped calls (re-finding the element and
2164
+ /// retrying its own op), generalized here for a call that talks to the tab
2165
+ /// directly and has nothing to re-resolve. Pure control flow, no CDP calls
2166
+ /// of its own, so it is unit-testable with a mock closure rather than only
2167
+ /// provable against a real, flaky browser.
2168
+ fn retry_on_transport_fault<T>(
2131
2169
  mut attempt: impl FnMut() -> Result<T, String>,
2132
2170
  ) -> Result<T, String> {
2133
- match attempt() {
2134
- Ok(value) => Ok(value),
2135
- Err(e) if is_transport_fault(&e) => {
2136
- std::thread::sleep(Duration::from_millis(300));
2137
- attempt()
2171
+ retry_on_transport_fault_with(TRANSPORT_FAULT_RETRIES, &mut attempt, |backoff| {
2172
+ std::thread::sleep(backoff)
2173
+ })
2174
+ }
2175
+
2176
+ /// The retry loop itself, with the attempt budget and the sleep both
2177
+ /// injectable: production always calls it via [`retry_on_transport_fault`]
2178
+ /// with the real constants and a real sleep; tests inject a smaller budget
2179
+ /// and a no-op sleep so the backoff schedule is provable without paying
2180
+ /// seconds of real wall-clock time per test.
2181
+ fn retry_on_transport_fault_with<T>(
2182
+ max_retries: u32,
2183
+ attempt: &mut impl FnMut() -> Result<T, String>,
2184
+ mut sleep: impl FnMut(Duration),
2185
+ ) -> Result<T, String> {
2186
+ let mut retries = 0;
2187
+ loop {
2188
+ match attempt() {
2189
+ Ok(value) => return Ok(value),
2190
+ Err(e) if retries < max_retries && is_transport_fault(&e) => {
2191
+ retries += 1;
2192
+ sleep(TRANSPORT_FAULT_BACKOFF * retries);
2193
+ }
2194
+ Err(e) => return Err(e),
2138
2195
  }
2139
- Err(e) => Err(e),
2140
2196
  }
2141
2197
  }
2142
2198
 
2143
2199
  #[cfg(test)]
2144
2200
  mod transport_retry_tests {
2145
- use super::retry_once_on_transport_fault;
2146
- use std::cell::Cell;
2201
+ use super::{retry_on_transport_fault, retry_on_transport_fault_with};
2202
+ use std::cell::{Cell, RefCell};
2203
+ use std::time::Duration;
2147
2204
 
2148
2205
  /// docs/fiori-reliability/FINDINGS.md:
2149
2206
  /// medium-02-change-info-record-readonly.flow.yaml failed live with
@@ -2154,7 +2211,7 @@ mod transport_retry_tests {
2154
2211
  #[test]
2155
2212
  fn a_transport_fault_recovers_on_the_second_attempt() {
2156
2213
  let calls = Cell::new(0);
2157
- let result = retry_once_on_transport_fault(|| {
2214
+ let result = retry_on_transport_fault(|| {
2158
2215
  calls.set(calls.get() + 1);
2159
2216
  if calls.get() == 1 {
2160
2217
  Err("Unable to make method calls because underlying connection is closed".into())
@@ -2163,21 +2220,51 @@ mod transport_retry_tests {
2163
2220
  }
2164
2221
  });
2165
2222
  assert_eq!(result, Ok("recovered"));
2166
- assert_eq!(calls.get(), 2, "must retry exactly once, not loop");
2223
+ assert_eq!(calls.get(), 2);
2167
2224
  }
2168
2225
 
2226
+ /// issue #608: a live run saw `frame_act` fail twice in a row on the
2227
+ /// same connection-closed fault even though it already had one retry -
2228
+ /// this proves the policy now survives that exact shape of failure by
2229
+ /// recovering on the third attempt (second retry).
2169
2230
  #[test]
2170
- fn a_transport_fault_that_never_clears_still_fails_after_one_retry() {
2231
+ fn a_transport_fault_recovers_on_the_third_attempt() {
2171
2232
  let calls = Cell::new(0);
2172
- let result: Result<(), String> = retry_once_on_transport_fault(|| {
2173
- calls.set(calls.get() + 1);
2174
- Err("the event waited for never came".into())
2175
- });
2233
+ let result = retry_on_transport_fault_with(
2234
+ 3,
2235
+ &mut || {
2236
+ calls.set(calls.get() + 1);
2237
+ if calls.get() < 3 {
2238
+ Err(
2239
+ "Unable to make method calls because underlying connection is closed"
2240
+ .into(),
2241
+ )
2242
+ } else {
2243
+ Ok("recovered")
2244
+ }
2245
+ },
2246
+ |_| {},
2247
+ );
2248
+ assert_eq!(result, Ok("recovered"));
2249
+ assert_eq!(calls.get(), 3, "must survive two consecutive faults");
2250
+ }
2251
+
2252
+ #[test]
2253
+ fn a_transport_fault_that_never_clears_still_fails_after_all_retries() {
2254
+ let calls = Cell::new(0);
2255
+ let result: Result<(), String> = retry_on_transport_fault_with(
2256
+ 3,
2257
+ &mut || {
2258
+ calls.set(calls.get() + 1);
2259
+ Err("the event waited for never came".into())
2260
+ },
2261
+ |_| {},
2262
+ );
2176
2263
  assert_eq!(result, Err("the event waited for never came".to_string()));
2177
2264
  assert_eq!(
2178
2265
  calls.get(),
2179
- 2,
2180
- "one retry, then give up - not an infinite loop"
2266
+ 4,
2267
+ "one first attempt plus three retries, then give up - not an infinite loop"
2181
2268
  );
2182
2269
  }
2183
2270
 
@@ -2187,13 +2274,34 @@ mod transport_retry_tests {
2187
2274
  #[test]
2188
2275
  fn a_non_transport_error_is_never_retried() {
2189
2276
  let calls = Cell::new(0);
2190
- let result: Result<(), String> = retry_once_on_transport_fault(|| {
2277
+ let result: Result<(), String> = retry_on_transport_fault(|| {
2191
2278
  calls.set(calls.get() + 1);
2192
2279
  Err("element not found".into())
2193
2280
  });
2194
2281
  assert_eq!(result, Err("element not found".to_string()));
2195
2282
  assert_eq!(calls.get(), 1, "a non-transport error must not be retried");
2196
2283
  }
2284
+
2285
+ /// The wait between attempts grows (300ms, 600ms, 900ms) instead of
2286
+ /// staying fixed - issue #608's "and/or backoff that scales" direction.
2287
+ #[test]
2288
+ fn backoff_grows_with_each_retry() {
2289
+ let waits = RefCell::new(Vec::new());
2290
+ let result: Result<(), String> = retry_on_transport_fault_with(
2291
+ 3,
2292
+ &mut || Err("connection is closed".into()),
2293
+ |backoff| waits.borrow_mut().push(backoff),
2294
+ );
2295
+ assert!(result.is_err());
2296
+ assert_eq!(
2297
+ waits.into_inner(),
2298
+ vec![
2299
+ Duration::from_millis(300),
2300
+ Duration::from_millis(600),
2301
+ Duration::from_millis(900),
2302
+ ]
2303
+ );
2304
+ }
2197
2305
  }
2198
2306
 
2199
2307
  /// How a [`UiaSelector`] resolves on a page: a CSS selector or a text
@@ -2473,8 +2581,10 @@ impl WebAppDriver {
2473
2581
  // this talks to the tab directly, with no element to re-resolve, so
2474
2582
  // it never went through with_element's retry either - one transient
2475
2583
  // CDP hiccup here (this is the exact call `type_text`'s framed path
2476
- // uses for every field inside an iframe) killed the whole flow.
2477
- let status = retry_once_on_transport_fault(|| {
2584
+ // uses for every field inside an iframe) killed the whole flow. A
2585
+ // single retry was also not enough on its own (issue #608) - this
2586
+ // is the call site that failed twice in a row live.
2587
+ let status = retry_on_transport_fault(|| {
2478
2588
  self.tab()
2479
2589
  .map_err(|e| e.to_string())?
2480
2590
  .evaluate(&call, false)
@@ -3366,7 +3476,7 @@ impl AppDriver for WebAppDriver {
3366
3476
  })
3367
3477
  .flatten();
3368
3478
  let Some(resolver) = resolver else {
3369
- return flowproof_driver::composed_actionability_gate(self, target);
3479
+ return self.composed_readiness(target);
3370
3480
  };
3371
3481
  let gate_js = format!(
3372
3482
  "(async el => {{
@@ -3402,7 +3512,7 @@ impl AppDriver for WebAppDriver {
3402
3512
  .map_err(|e| web_err(&format!("actionability of [{target}]"), e))?;
3403
3513
  let verdict = value.value.and_then(|v| v.as_str().map(str::to_string));
3404
3514
  Ok(match verdict.as_deref() {
3405
- Some("sap_grid") => return flowproof_driver::composed_actionability_gate(self, target),
3515
+ Some("sap_grid") => return self.composed_readiness(target),
3406
3516
  Some("ok") => None,
3407
3517
  Some("disabled") => Some("disabled".into()),
3408
3518
  Some("unstable") => Some("unstable (still moving/animating)".into()),
@@ -3454,6 +3564,36 @@ impl AppDriver for WebAppDriver {
3454
3564
 
3455
3565
  fn element_visible(&mut self, selector: &UiaSelector) -> Result<Option<bool>, DriverError> {
3456
3566
  let locator = Self::locator(selector)?;
3567
+ // A dialog can disappear between element_exists and this probe.
3568
+ // Resolve and read in one page operation: absence is a visibility
3569
+ // answer, while browser/transport failures must still propagate.
3570
+ if selector.frame.is_none() {
3571
+ if let Some(resolver) = Self::js_resolver(&locator) {
3572
+ let expression = format!(
3573
+ "((element) => {{
3574
+ if (!element) return false;
3575
+ if (typeof element.checkVisibility === 'function' &&
3576
+ !element.checkVisibility({{
3577
+ contentVisibilityAuto: true,
3578
+ opacityProperty: true,
3579
+ visibilityProperty: true
3580
+ }})) return false;
3581
+ return element.getClientRects().length > 0;
3582
+ }})({resolver})"
3583
+ );
3584
+ let value = self
3585
+ .tab()?
3586
+ .evaluate(&expression, false)
3587
+ .map_err(|e| web_err(&format!("reading visibility of [{selector}]"), e))?;
3588
+ return value
3589
+ .value
3590
+ .and_then(|v| v.as_bool())
3591
+ .map(Some)
3592
+ .ok_or_else(|| {
3593
+ DriverError::Browser("visibility probe did not return a boolean".into())
3594
+ });
3595
+ }
3596
+ }
3457
3597
  let value = self.with_element(
3458
3598
  &locator,
3459
3599
  &format!("reading visibility of [{selector}]"),
@@ -4165,14 +4305,14 @@ impl AppDriver for WebAppDriver {
4165
4305
  id = js(&query.inner_id),
4166
4306
  text = js(&query.inner_text),
4167
4307
  );
4168
- // ONE retry on a transport fault, same recovery `with_element` gives
4308
+ // Retry on a transport fault, same recovery `with_element` gives
4169
4309
  // element-scoped calls (see medium-02-change-info-record-readonly's
4170
4310
  // real "probing an iframe: ... underlying connection is closed"
4171
4311
  // failure in docs/fiori-reliability/FINDINGS.md): this call talks to
4172
4312
  // the tab directly, with no element to re-resolve, so it never went
4173
4313
  // through that retry at all - any transient CDP hiccup here killed
4174
4314
  // the whole flow on the first blip, with zero chance to recover.
4175
- let status = retry_once_on_transport_fault(|| {
4315
+ let status = retry_on_transport_fault(|| {
4176
4316
  self.tab()
4177
4317
  .map_err(|e| e.to_string())?
4178
4318
  .evaluate(&call, false)
@@ -4287,15 +4427,38 @@ impl AppDriver for WebAppDriver {
4287
4427
  // back the accepted value before passing.
4288
4428
  self.press_key("A", &[KeyMod::Ctrl])?;
4289
4429
  self.press_key("Backspace", &[])?;
4290
- self.tab()?
4291
- .type_str(text)
4292
- .map_err(|e| web_err("typing into the framed SAP field", e))?;
4430
+ // Talks to the tab directly, same gap frame_act had before
4431
+ // its own retry: confirmed live, a dropped CDP event here
4432
+ // failed the whole flow on a field that would have typed
4433
+ // fine a moment later.
4434
+ retry_on_transport_fault(|| {
4435
+ self.tab()
4436
+ .map_err(|e| e.to_string())?
4437
+ .type_str(text)
4438
+ .map_err(|e| e.to_string())
4439
+ })
4440
+ .map_err(|e| web_err("typing into the framed SAP field", e))?;
4293
4441
  self.press_key("Tab", &[])?;
4294
- std::thread::sleep(Duration::from_millis(300));
4295
- let accepted = self.frame_value(&query)?;
4442
+ // Some SAP WebGUI fields (a material number checked
4443
+ // against the current plant/org, for instance) validate
4444
+ // against the backend on commit, and read back empty for
4445
+ // a moment while that round trip is in flight - not
4446
+ // reverted, just not settled yet. A single fixed sleep
4447
+ // caught that transitional empty state and failed a
4448
+ // field that would have read back correctly moments
4449
+ // later (confirmed live). Poll instead of sampling once.
4450
+ let deadline = std::time::Instant::now() + Duration::from_secs(2);
4451
+ let accepted = loop {
4452
+ let value = self.frame_value(&query)?;
4453
+ if value == text || std::time::Instant::now() >= deadline {
4454
+ break value;
4455
+ }
4456
+ std::thread::sleep(Duration::from_millis(200));
4457
+ };
4296
4458
  if accepted != text {
4297
4459
  return Err(DriverError::Browser(format!(
4298
- "the field inside iframe '{}' accepted a different value after commit",
4460
+ "the field inside iframe '{}' accepted a different value after \
4461
+ commit: typed {text:?}, field now reads {accepted:?}",
4299
4462
  query.frame
4300
4463
  )));
4301
4464
  }
@@ -4305,9 +4468,13 @@ impl AppDriver for WebAppDriver {
4305
4468
  // it - the same click -> select -> keystrokes contract the
4306
4469
  // top-level path below uses, not an append.
4307
4470
  self.frame_act(&query, "select_all", serde_json::Value::Null)?;
4308
- self.tab()?
4309
- .type_str(text)
4310
- .map_err(|e| web_err("typing into the framed field", e))?;
4471
+ retry_on_transport_fault(|| {
4472
+ self.tab()
4473
+ .map_err(|e| e.to_string())?
4474
+ .type_str(text)
4475
+ .map_err(|e| e.to_string())
4476
+ })
4477
+ .map_err(|e| web_err("typing into the framed field", e))?;
4311
4478
  // An Escape here was tried to dismiss the value-help popup
4312
4479
  // real keystrokes can trigger on classic SAP GUI screens
4313
4480
  // (confirmed live), but Escape on these fields REVERTS to
@@ -4697,9 +4864,20 @@ impl AppDriver for WebAppDriver {
4697
4864
  "Space" | "Spacebar" => " ",
4698
4865
  _ => key,
4699
4866
  };
4700
- self.tab()?
4701
- .press_key_with_modifiers(browser_key, (!mods.is_empty()).then_some(mods.as_slice()))
4702
- .map_err(|e| web_err(&format!("pressing key '{key}'"), e))?;
4867
+ // Same direct-tab gap type_text's framed path had: no element to
4868
+ // re-resolve, so with_element's retry never covered it either -
4869
+ // confirmed live, a dropped CDP event on a bare keypress (Tab to
4870
+ // commit a field, Ctrl+A to select) failed the whole flow.
4871
+ retry_on_transport_fault(|| {
4872
+ self.tab()
4873
+ .map_err(|e| e.to_string())?
4874
+ .press_key_with_modifiers(
4875
+ browser_key,
4876
+ (!mods.is_empty()).then_some(mods.as_slice()),
4877
+ )
4878
+ .map_err(|e| e.to_string())
4879
+ })
4880
+ .map_err(|e| web_err(&format!("pressing key '{key}'"), e))?;
4703
4881
  Ok(())
4704
4882
  }
4705
4883
 
@@ -0,0 +1,77 @@
1
+ //! Visibility must tolerate a dialog disappearing after the existence probe.
2
+ use flowproof_driver::{AppDriver, ScopeQuery, UiaSelector};
3
+
4
+ #[test]
5
+ fn removed_element_is_not_visible_without_swallowing_browser_errors() {
6
+ if std::env::var("FLOWPROOF_E2E").as_deref() != Ok("1") {
7
+ return;
8
+ }
9
+ let _guard = flowproof_adapters::SharedBrowserGuard::new();
10
+ let html = "<html><body><div id='dialog'>Receipt checked</div><div id='hidden' style='display:none'>Hidden</div><button id='close' onclick=\"document.getElementById('dialog').remove()\">Close</button><div id='scope'>Scope<button id='payment' style='display:none'>Payment</button></div><button id='reveal' onclick=\"document.getElementById('payment').style.display='block'\">Reveal</button></body></html>";
11
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener");
12
+ let port = listener.local_addr().expect("fixture address").port();
13
+ std::thread::spawn(move || {
14
+ use std::io::{Read, Write};
15
+ for mut stream in listener.incoming().flatten() {
16
+ let mut request = [0; 2048];
17
+ let _ = stream.read(&mut request);
18
+ let _ = write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", html.len(), html);
19
+ }
20
+ });
21
+ let mut driver = flowproof_adapters::WebAppDriver::new().expect("Chrome launches");
22
+ driver
23
+ .launch(
24
+ &format!("http://127.0.0.1:{port}"),
25
+ "",
26
+ std::time::Duration::from_secs(30),
27
+ )
28
+ .expect("fixture loads");
29
+ let scoped = UiaSelector {
30
+ scope: Some(ScopeQuery {
31
+ container: "css:#scope".into(),
32
+ anchor: "Scope".into(),
33
+ inner_css: Some("#payment".into()),
34
+ ..Default::default()
35
+ }),
36
+ ..Default::default()
37
+ };
38
+ assert!(driver
39
+ .actionability_gate(&scoped)
40
+ .expect("hidden target waits")
41
+ .is_some());
42
+ driver
43
+ .invoke(&UiaSelector::css("#reveal"))
44
+ .expect("reveal target");
45
+ assert!(driver
46
+ .actionability_gate(&scoped)
47
+ .expect("visible target ready")
48
+ .is_none());
49
+ let dialog = UiaSelector::css("#dialog");
50
+ assert!(driver.element_exists(&dialog).expect("dialog existence"));
51
+ assert_eq!(
52
+ driver.element_visible(&dialog).expect("visible dialog"),
53
+ Some(true)
54
+ );
55
+ assert_eq!(
56
+ driver
57
+ .element_visible(&UiaSelector::css("#hidden"))
58
+ .expect("hidden element visibility"),
59
+ Some(false)
60
+ );
61
+ driver
62
+ .invoke(&UiaSelector::css("#close"))
63
+ .expect("close dialog");
64
+ assert_eq!(
65
+ driver
66
+ .element_visible(&dialog)
67
+ .expect("removed element visibility"),
68
+ Some(false)
69
+ );
70
+ assert_eq!(
71
+ driver
72
+ .element_visible(&UiaSelector::css("#never-present"))
73
+ .expect("absent element visibility"),
74
+ Some(false)
75
+ );
76
+ assert!(driver.element_visible(&UiaSelector::css("[")).is_err());
77
+ }
@@ -65,7 +65,7 @@ when it already holds a value the step does not contradict.
65
65
  submit button behind mandatory fields - include the actions that supply it, then the \
66
66
  action that reaches the goal.
67
67
  - Respond with ONLY JSON, no prose, no code fences.
68
- - The JSON action is one of: \"click\", \"click_at\", \"drag\", \"type_text\", \
68
+ - The JSON action is one of: \"click\", \"double_click\", \"click_at\", \"drag\", \"type_text\", \
69
69
  \"assert_text\", \"capture_text\", \"capture_count\", \"type_captured\", \
70
70
  \"select_option\", \"select_options\", \"scroll\", \"scroll_into_view\", \"press_key\", \"rule_step\", or \
71
71
  \"capture_ambiguity\".
@@ -745,14 +745,23 @@ fn ground_one(
745
745
  )));
746
746
  };
747
747
  match authored.action.as_str() {
748
- "click" => {
748
+ "click" | "double_click" => {
749
749
  if !scene_token_is_actionable(scene, token) {
750
750
  return Err("click target is readable but not actionable".into());
751
751
  }
752
- Ok(vec![ResolvedAction::Press {
753
- target,
754
- label: scene_label(scene, token).unwrap_or_default(),
755
- dialog: None,
752
+ let label = scene_label(scene, token).unwrap_or_default();
753
+ Ok(vec![if authored.action == "double_click" {
754
+ ResolvedAction::DoubleClick {
755
+ target,
756
+ label,
757
+ dialog: None,
758
+ }
759
+ } else {
760
+ ResolvedAction::Press {
761
+ target,
762
+ label,
763
+ dialog: None,
764
+ }
756
765
  }])
757
766
  }
758
767
  "click_at" => {
@@ -2248,6 +2257,15 @@ mod tests {
2248
2257
  #[test]
2249
2258
  fn human_language_primitives_ground_without_rules_in_the_input() {
2250
2259
  let cases = [
2260
+ (
2261
+ "Double-click task 1 to activate its editor",
2262
+ r#"{"action":"double_click","target":"css:#task-1"}"#,
2263
+ ResolvedAction::DoubleClick {
2264
+ target: Target::css("#task-1"),
2265
+ label: "task 1".into(),
2266
+ dialog: None,
2267
+ },
2268
+ ),
2251
2269
  (
2252
2270
  "Drag task 1 into the todo drop area",
2253
2271
  r#"{"action":"drag","target":"css:#task-1","onto":"css:#todo"}"#,
@@ -20,8 +20,10 @@ chrono = { workspace = true }
20
20
  clap = { workspace = true }
21
21
  # Cross-platform Ctrl-C handling for the long-lived `capture` listener, so a
22
22
  # deliberate interrupt exits 0 cleanly on Windows and Unix alike. Light: no
23
- # async runtime, just a signal handler.
24
- ctrlc = "3"
23
+ # async runtime, just a signal handler. `termination` additionally catches
24
+ # SIGTERM/SIGHUP on Unix (a plain `kill` or a CI timeout, not just Ctrl-C) -
25
+ # see `run_cli`'s shared-browser signal handler.
26
+ ctrlc = { version = "3", features = ["termination"] }
25
27
  # `flowproof config`'s platform config directory (plans/001-credential-config.md,
26
28
  # "Where the file lives"): a single-purpose lookup, no multi-app namespacing
27
29
  # to justify `directories`' extra structure.