flowproof 0.12.0__tar.gz → 0.12.2__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.
- {flowproof-0.12.0 → flowproof-0.12.2}/Cargo.lock +7 -7
- {flowproof-0.12.0 → flowproof-0.12.2}/Cargo.toml +1 -1
- {flowproof-0.12.0 → flowproof-0.12.2}/PKG-INFO +2 -2
- {flowproof-0.12.0 → flowproof-0.12.2}/README.md +1 -1
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs +17 -2
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/web.rs +282 -17
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +90 -31
- flowproof-0.12.2/crates/flowproof-agent/src/author.rs +1571 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/clarify.rs +10 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/heal.rs +6 -2
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/llm.rs +8 -2
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/recorder.rs +526 -126
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/rules.rs +26 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/spec.rs +103 -3
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/lib.rs +160 -55
- flowproof-0.12.2/crates/flowproof-cli/tests/llm_author_e2e.rs +556 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_author_e2e.rs +5 -5
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/app.rs +5 -3
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-python/src/lib.rs +41 -7
- {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/__init__.py +1 -1
- {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/flow.py +38 -9
- {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/mcp_server.py +17 -9
- {flowproof-0.12.0 → flowproof-0.12.2}/pyproject.toml +1 -1
- flowproof-0.12.0/crates/flowproof-agent/src/author.rs +0 -397
- flowproof-0.12.0/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -179
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/src/lib.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/agent_flow.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/oob.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/src/lib.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/src/runrecord.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/captures.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/format.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/secret_scan.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/cli.py +0 -0
- {flowproof-0.12.0 → flowproof-0.12.2}/flowproof/py.typed +0 -0
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "flowproof-adapters"
|
|
763
|
-
version = "0.12.
|
|
763
|
+
version = "0.12.2"
|
|
764
764
|
dependencies = [
|
|
765
765
|
"anyhow",
|
|
766
766
|
"flowproof-driver",
|
|
@@ -779,7 +779,7 @@ dependencies = [
|
|
|
779
779
|
|
|
780
780
|
[[package]]
|
|
781
781
|
name = "flowproof-agent"
|
|
782
|
-
version = "0.12.
|
|
782
|
+
version = "0.12.2"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"chrono",
|
|
785
785
|
"flowproof-driver",
|
|
@@ -796,7 +796,7 @@ dependencies = [
|
|
|
796
796
|
|
|
797
797
|
[[package]]
|
|
798
798
|
name = "flowproof-cli"
|
|
799
|
-
version = "0.12.
|
|
799
|
+
version = "0.12.2"
|
|
800
800
|
dependencies = [
|
|
801
801
|
"ab_glyph",
|
|
802
802
|
"chrono",
|
|
@@ -817,7 +817,7 @@ dependencies = [
|
|
|
817
817
|
|
|
818
818
|
[[package]]
|
|
819
819
|
name = "flowproof-driver"
|
|
820
|
-
version = "0.12.
|
|
820
|
+
version = "0.12.2"
|
|
821
821
|
dependencies = [
|
|
822
822
|
"image",
|
|
823
823
|
"postgres",
|
|
@@ -831,7 +831,7 @@ dependencies = [
|
|
|
831
831
|
|
|
832
832
|
[[package]]
|
|
833
833
|
name = "flowproof-python"
|
|
834
|
-
version = "0.12.
|
|
834
|
+
version = "0.12.2"
|
|
835
835
|
dependencies = [
|
|
836
836
|
"flowproof-agent",
|
|
837
837
|
"flowproof-cli",
|
|
@@ -843,7 +843,7 @@ dependencies = [
|
|
|
843
843
|
|
|
844
844
|
[[package]]
|
|
845
845
|
name = "flowproof-replay"
|
|
846
|
-
version = "0.12.
|
|
846
|
+
version = "0.12.2"
|
|
847
847
|
dependencies = [
|
|
848
848
|
"chrono",
|
|
849
849
|
"flowproof-agent",
|
|
@@ -857,7 +857,7 @@ dependencies = [
|
|
|
857
857
|
|
|
858
858
|
[[package]]
|
|
859
859
|
name = "flowproof-trace"
|
|
860
|
-
version = "0.12.
|
|
860
|
+
version = "0.12.2"
|
|
861
861
|
dependencies = [
|
|
862
862
|
"jsonschema",
|
|
863
863
|
"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.12.
|
|
25
|
+
version = "0.12.2"
|
|
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.12.
|
|
3
|
+
Version: 0.12.2
|
|
4
4
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -61,7 +61,7 @@ The wheel bundles the Rust engine; no separate install. Targets: `web`
|
|
|
61
61
|
(SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
|
|
62
62
|
(no UI — HTTP/SQL assertion flows, any OS).
|
|
63
63
|
|
|
64
|
-
**Status:
|
|
64
|
+
**Status: pre-alpha, in active development.** Record→replay,
|
|
65
65
|
model-grounded authoring, healing with reviewable diffs, suites, run
|
|
66
66
|
recordings, and the MCP server all work and are tested in CI.
|
|
67
67
|
|
|
@@ -43,7 +43,7 @@ The wheel bundles the Rust engine; no separate install. Targets: `web`
|
|
|
43
43
|
(SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
|
|
44
44
|
(no UI — HTTP/SQL assertion flows, any OS).
|
|
45
45
|
|
|
46
|
-
**Status:
|
|
46
|
+
**Status: pre-alpha, in active development.** Record→replay,
|
|
47
47
|
model-grounded authoring, healing with reviewable diffs, suites, run
|
|
48
48
|
recordings, and the MCP server all work and are tested in CI.
|
|
49
49
|
|
{flowproof-0.12.0 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs
RENAMED
|
@@ -198,11 +198,26 @@ mod tests {
|
|
|
198
198
|
let mut overrides = BTreeMap::new();
|
|
199
199
|
overrides.insert("ZZ_FLOWPROOF_SORT".to_string(), "z".into());
|
|
200
200
|
overrides.insert("AA_FLOWPROOF_SORT".to_string(), "a".into());
|
|
201
|
+
// Windows runner images carry this same prefix shape as
|
|
202
|
+
// COMMONPROGRAMFILES and COMMONPROGRAMFILES(X86). Comparing whole
|
|
203
|
+
// NAME=VALUE entries would put '(' before '=' and falsely call the
|
|
204
|
+
// correctly name-sorted block unsorted.
|
|
205
|
+
overrides.insert("MM_FLOWPROOF_SORT".to_string(), "plain".into());
|
|
206
|
+
overrides.insert("MM_FLOWPROOF_SORT(X86)".to_string(), "qualified".into());
|
|
201
207
|
|
|
202
208
|
let entries = parse(&environment_block(&overrides, &[]));
|
|
203
|
-
let upper: Vec<String> = entries
|
|
209
|
+
let upper: Vec<String> = entries
|
|
210
|
+
.iter()
|
|
211
|
+
.map(|entry| {
|
|
212
|
+
entry
|
|
213
|
+
.split_once('=')
|
|
214
|
+
.expect("an environment entry has a name and value")
|
|
215
|
+
.0
|
|
216
|
+
.to_uppercase()
|
|
217
|
+
})
|
|
218
|
+
.collect();
|
|
204
219
|
let mut sorted = upper.clone();
|
|
205
220
|
sorted.sort();
|
|
206
|
-
assert_eq!(upper, sorted, "the block must be sorted");
|
|
221
|
+
assert_eq!(upper, sorted, "the block must be sorted by name");
|
|
207
222
|
}
|
|
208
223
|
}
|
|
@@ -15,6 +15,7 @@ use flowproof_driver::{
|
|
|
15
15
|
use headless_chrome::browser::tab::{ModifierKey, Tab};
|
|
16
16
|
use headless_chrome::protocol::cdp::Target::CreateTarget;
|
|
17
17
|
use headless_chrome::protocol::cdp::{Emulation, Input, Network, Page};
|
|
18
|
+
use headless_chrome::types::Bounds;
|
|
18
19
|
use headless_chrome::{Browser, LaunchOptions};
|
|
19
20
|
|
|
20
21
|
use crate::AdapterError;
|
|
@@ -526,6 +527,16 @@ fn headed_requested() -> bool {
|
|
|
526
527
|
std::env::var_os("FLOWPROOF_HEADED").is_some()
|
|
527
528
|
}
|
|
528
529
|
|
|
530
|
+
/// Headed runs are deliberately private. A shared browser isolates each flow
|
|
531
|
+
/// in an incognito context, which Chromium presents as a second window. When
|
|
532
|
+
/// the flow tab closes, the original window (including the keep-alive tab)
|
|
533
|
+
/// becomes visible again and looks like a leaked browser. A visible run is for
|
|
534
|
+
/// a person to watch, so cold-start reuse is worth less than owning one window
|
|
535
|
+
/// whose lifetime exactly matches the flow.
|
|
536
|
+
fn should_share_browser(headed: bool, no_shared: bool) -> bool {
|
|
537
|
+
!headed && !no_shared
|
|
538
|
+
}
|
|
539
|
+
|
|
529
540
|
/// Build the launch options, split out from [`launch_browser`] so the headless
|
|
530
541
|
/// decision is testable without starting a browser.
|
|
531
542
|
fn launch_options_for(
|
|
@@ -813,9 +824,14 @@ impl WebAppDriver {
|
|
|
813
824
|
}
|
|
814
825
|
|
|
815
826
|
/// A driver on the shared browser (isolated context per flow), or a
|
|
816
|
-
/// private browser when `FLOWPROOF_NO_SHARED_BROWSER=1
|
|
827
|
+
/// private browser when `FLOWPROOF_NO_SHARED_BROWSER=1` or the browser is
|
|
828
|
+
/// headed. Headed ownership prevents the keep-alive window from surviving
|
|
829
|
+
/// after the visible flow window closes.
|
|
817
830
|
pub fn new() -> Result<Self, AdapterError> {
|
|
818
|
-
if
|
|
831
|
+
if !should_share_browser(
|
|
832
|
+
headed_requested(),
|
|
833
|
+
std::env::var_os("FLOWPROOF_NO_SHARED_BROWSER").is_some(),
|
|
834
|
+
) {
|
|
819
835
|
return Ok(Self {
|
|
820
836
|
browser: launch_browser(&[])?,
|
|
821
837
|
context_id: None,
|
|
@@ -1427,9 +1443,9 @@ fn xpath_literal(text: &str) -> String {
|
|
|
1427
1443
|
|
|
1428
1444
|
impl Drop for WebAppDriver {
|
|
1429
1445
|
fn drop(&mut self) {
|
|
1430
|
-
// The shared browser outlives this driver: close the tab so pages
|
|
1431
|
-
// don't accumulate across a suite. A private browser (opt-out
|
|
1432
|
-
// torn down with its
|
|
1446
|
+
// The shared browser outlives this driver: close the flow tab so pages
|
|
1447
|
+
// don't accumulate across a suite. A private browser (opt-out or
|
|
1448
|
+
// headed) is torn down with its process.
|
|
1433
1449
|
if self.context_id.is_some() {
|
|
1434
1450
|
if let Some(tab) = self.tab.take() {
|
|
1435
1451
|
let _ = tab.close(false);
|
|
@@ -1633,6 +1649,16 @@ impl AppDriver for WebAppDriver {
|
|
|
1633
1649
|
None => self.browser.new_tab(),
|
|
1634
1650
|
}
|
|
1635
1651
|
.map_err(|e| web_err("opening tab", e))?;
|
|
1652
|
+
// A visible flow is the only Chrome window the user should have to
|
|
1653
|
+
// manage. Chromium's default launch bounds can be a narrow utility
|
|
1654
|
+
// window (and an incognito context used to create another one), so
|
|
1655
|
+
// present the privately-owned headed window at the desktop's normal
|
|
1656
|
+
// maximized size. Device emulation below still pins the page viewport
|
|
1657
|
+
// when the spec requests one.
|
|
1658
|
+
if headed_requested() {
|
|
1659
|
+
tab.set_bounds(Bounds::Maximized)
|
|
1660
|
+
.map_err(|e| web_err("maximizing headed browser window", e))?;
|
|
1661
|
+
}
|
|
1636
1662
|
// Viewport/UA emulation BEFORE navigation, so the app boots into
|
|
1637
1663
|
// the emulated device (responsive breakpoints, UA sniffing). NOT
|
|
1638
1664
|
// best-effort: a flow recorded mobile must never run desktop.
|
|
@@ -3097,31 +3123,259 @@ impl AppDriver for WebAppDriver {
|
|
|
3097
3123
|
}
|
|
3098
3124
|
|
|
3099
3125
|
fn scene(&mut self) -> Result<Option<String>, DriverError> {
|
|
3100
|
-
// Enumerate visible interactable elements
|
|
3126
|
+
// Enumerate visible interactable elements plus readable leaf text —
|
|
3101
3127
|
// the grounding set an authoring model must choose targets from.
|
|
3128
|
+
// Static text matters for natural `Remember the order number` steps,
|
|
3129
|
+
// not only for assertions.
|
|
3102
3130
|
// `target` is the provenance-neutral token the model echoes; the
|
|
3103
3131
|
// bare `css` key is kept one release for older agents.
|
|
3104
3132
|
const SCENE_JS: &str = r#"
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3133
|
+
(() => {
|
|
3134
|
+
function semanticCss(el) {
|
|
3135
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
3136
|
+
for (const attr of ['data-testid', 'data-test', 'data-qa', 'aria-label', 'name']) {
|
|
3137
|
+
const value = el.getAttribute(attr);
|
|
3138
|
+
if (value) {
|
|
3139
|
+
const candidate = el.tagName.toLowerCase() + '[' + attr + '="' +
|
|
3140
|
+
CSS.escape(value) + '"]';
|
|
3141
|
+
if (document.querySelectorAll(candidate).length === 1) return candidate;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
for (const cls of Array.from(el.classList)) {
|
|
3145
|
+
const candidate = el.tagName.toLowerCase() + '.' + CSS.escape(cls);
|
|
3146
|
+
if (document.querySelectorAll(candidate).length === 1) return candidate;
|
|
3147
|
+
}
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
function cssPath(el) {
|
|
3151
|
+
const semantic = semanticCss(el);
|
|
3152
|
+
if (semantic) return semantic;
|
|
3153
|
+
const parts = [];
|
|
3154
|
+
while (el && el.nodeType === 1 && el !== document.body) {
|
|
3155
|
+
const tag = el.tagName.toLowerCase();
|
|
3156
|
+
const siblings = Array.from(el.parentElement.children)
|
|
3157
|
+
.filter(s => s.tagName === el.tagName);
|
|
3158
|
+
parts.unshift(tag + ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')');
|
|
3159
|
+
el = el.parentElement;
|
|
3160
|
+
}
|
|
3161
|
+
return 'body > ' + parts.join(' > ');
|
|
3162
|
+
}
|
|
3163
|
+
function compoundCss(el) {
|
|
3164
|
+
const classes = Array.from(el.classList);
|
|
3165
|
+
if (!classes.length) return null;
|
|
3166
|
+
return el.tagName.toLowerCase() + classes.map(cls => '.' + CSS.escape(cls)).join('');
|
|
3167
|
+
}
|
|
3168
|
+
function relativeCss(el, container) {
|
|
3169
|
+
const semantic = semanticCss(el);
|
|
3170
|
+
if (semantic && container.querySelectorAll(semantic).length === 1) return semantic;
|
|
3171
|
+
const compound = compoundCss(el);
|
|
3172
|
+
if (!compound) return null;
|
|
3173
|
+
if (container.querySelectorAll(compound).length === 1) return compound;
|
|
3174
|
+
|
|
3175
|
+
// A value cell often shares its base classes with the label
|
|
3176
|
+
// beside it, while the label has one extra presentation class
|
|
3177
|
+
// (`bg-info`, for example). Excluding that distinguishing
|
|
3178
|
+
// class gives the scoped target a stable, non-positional inner
|
|
3179
|
+
// selector without smuggling visible text into CSS.
|
|
3180
|
+
const extras = new Set();
|
|
3181
|
+
for (const other of container.querySelectorAll(compound)) {
|
|
3182
|
+
if (other === el) continue;
|
|
3183
|
+
for (const cls of other.classList) {
|
|
3184
|
+
if (!el.classList.contains(cls)) extras.add(cls);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
for (const extra of extras) {
|
|
3188
|
+
const candidate = compound + ':not(.' + CSS.escape(extra) + ')';
|
|
3189
|
+
if (container.querySelectorAll(candidate).length === 1) return candidate;
|
|
3190
|
+
}
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
function scopedReadable(el) {
|
|
3194
|
+
const container = el.parentElement;
|
|
3195
|
+
if (!container) return null;
|
|
3196
|
+
const containerCss = semanticCss(container) || compoundCss(container);
|
|
3197
|
+
const innerCss = relativeCss(el, container);
|
|
3198
|
+
if (!containerCss || !innerCss) return null;
|
|
3199
|
+
|
|
3200
|
+
// Use a neighbouring leaf as the row/card identity. Require
|
|
3201
|
+
// that the anchor identifies one matching container on the
|
|
3202
|
+
// current screen; runtime scoped resolution will enforce the
|
|
3203
|
+
// same relationship again on every replay.
|
|
3204
|
+
const siblings = Array.from(container.children);
|
|
3205
|
+
const before = siblings.slice(0, siblings.indexOf(el)).reverse();
|
|
3206
|
+
const after = siblings.slice(siblings.indexOf(el) + 1);
|
|
3207
|
+
for (const anchorEl of before.concat(after)) {
|
|
3208
|
+
const anchor = (anchorEl.textContent || '').trim();
|
|
3209
|
+
if (!anchor || anchor.length > 120) continue;
|
|
3210
|
+
const matching = Array.from(document.querySelectorAll(containerCss)).filter(candidate =>
|
|
3211
|
+
Array.from(candidate.children).some(child =>
|
|
3212
|
+
child !== el && (child.textContent || '').trim() === anchor
|
|
3213
|
+
)
|
|
3214
|
+
);
|
|
3215
|
+
if (matching.length !== 1 || matching[0] !== container) continue;
|
|
3216
|
+
const containerTarget = 'css:' + containerCss;
|
|
3217
|
+
const innerTarget = 'css:' + innerCss;
|
|
3218
|
+
return {
|
|
3219
|
+
token: 'scoped:' + containerTarget + ' containing ' + JSON.stringify(anchor) +
|
|
3220
|
+
' > ' + innerTarget,
|
|
3221
|
+
container: containerTarget,
|
|
3222
|
+
anchor,
|
|
3223
|
+
inner: innerTarget,
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
return null;
|
|
3227
|
+
}
|
|
3228
|
+
const all = Array.from(document.querySelectorAll('body *'));
|
|
3229
|
+
const styledLeaf = el => el.children.length === 0 &&
|
|
3230
|
+
['DIV', 'SPAN'].includes(el.tagName) &&
|
|
3231
|
+
/(?:background|color)\s*:/i.test(el.getAttribute('style') || '');
|
|
3232
|
+
const interactive = el => el.matches(
|
|
3233
|
+
'input, button, a, select, textarea, [role=button], [role=checkbox], [role=radio], [role=menuitem], [draggable], [ondrop], .draggable-row, .droparea'
|
|
3234
|
+
) || (!!el.id && el.children.length === 0 && ['DIV', 'SPAN'].includes(el.tagName)) ||
|
|
3235
|
+
styledLeaf(el);
|
|
3236
|
+
const readableLeaf = el => {
|
|
3237
|
+
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return false;
|
|
3238
|
+
const text = (el.textContent || '').trim();
|
|
3239
|
+
return text && !Array.from(el.children).some(child =>
|
|
3240
|
+
(child.textContent || '').trim()
|
|
3241
|
+
) && (semanticCss(el) || scopedReadable(el));
|
|
3242
|
+
};
|
|
3243
|
+
const ordered = all.filter(interactive).concat(
|
|
3244
|
+
all.filter(el => !interactive(el) && readableLeaf(el))
|
|
3245
|
+
);
|
|
3246
|
+
const seen = new Set();
|
|
3247
|
+
const chosen = ordered.filter(el => {
|
|
3108
3248
|
const r = el.getBoundingClientRect();
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3249
|
+
const style = getComputedStyle(el);
|
|
3250
|
+
// Authoring inventories the rendered PAGE, not only the
|
|
3251
|
+
// current viewport. A user naturally says "enter the date"
|
|
3252
|
+
// even when the field starts below the fold, and every web
|
|
3253
|
+
// action already scrolls its target into view before acting.
|
|
3254
|
+
const rendered = style.display !== 'none' && style.visibility !== 'hidden' &&
|
|
3255
|
+
Number(style.opacity) > 0 && r.width > 0 && r.height > 0;
|
|
3256
|
+
if (!rendered || seen.has(el)) return false;
|
|
3257
|
+
seen.add(el);
|
|
3258
|
+
return true;
|
|
3259
|
+
}).slice(0, 100);
|
|
3260
|
+
const entries = chosen.map(el => {
|
|
3261
|
+
const css = cssPath(el);
|
|
3262
|
+
const scoped = !interactive(el) && !semanticCss(el) ? scopedReadable(el) : null;
|
|
3114
3263
|
const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
|
|
3115
3264
|
: (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
|
|
3116
|
-
|
|
3117
|
-
target: 'css:' + css,
|
|
3265
|
+
const entry = {
|
|
3266
|
+
target: scoped ? scoped.token : 'css:' + css,
|
|
3118
3267
|
css,
|
|
3119
3268
|
tag: el.tagName.toLowerCase(),
|
|
3269
|
+
actionable: interactive(el),
|
|
3120
3270
|
type: el.getAttribute('type') || undefined,
|
|
3121
3271
|
text: (el.textContent || '').trim().slice(0, 80) || undefined,
|
|
3122
3272
|
label: label || undefined,
|
|
3273
|
+
background_color: styledLeaf(el) ? getComputedStyle(el).backgroundColor : undefined,
|
|
3123
3274
|
};
|
|
3124
|
-
|
|
3275
|
+
if (scoped) entry.scope = {
|
|
3276
|
+
container: scoped.container,
|
|
3277
|
+
anchor: scoped.anchor,
|
|
3278
|
+
inner: scoped.inner,
|
|
3279
|
+
};
|
|
3280
|
+
return entry;
|
|
3281
|
+
});
|
|
3282
|
+
|
|
3283
|
+
// A human can ask for the number of displayed rows or the
|
|
3284
|
+
// final cell without naming one current row. Represent those
|
|
3285
|
+
// collection identities directly so the model can ground the
|
|
3286
|
+
// intent without inventing a selector from today's DOM.
|
|
3287
|
+
for (const table of document.querySelectorAll('table')) {
|
|
3288
|
+
const tableCss = semanticCss(table);
|
|
3289
|
+
if (!tableCss) continue;
|
|
3290
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
3291
|
+
if (rows.length) {
|
|
3292
|
+
const css = tableCss + ' tr';
|
|
3293
|
+
entries.push({
|
|
3294
|
+
target: 'css:' + css,
|
|
3295
|
+
css,
|
|
3296
|
+
tag: 'collection',
|
|
3297
|
+
actionable: false,
|
|
3298
|
+
count: rows.length,
|
|
3299
|
+
label: 'rows in ' + tableCss,
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
const lastCell = table.querySelector('tr:last-child td:last-child');
|
|
3303
|
+
if (lastCell) {
|
|
3304
|
+
const css = tableCss + ' tr:last-child td:last-child';
|
|
3305
|
+
entries.push({
|
|
3306
|
+
target: 'css:' + css,
|
|
3307
|
+
css,
|
|
3308
|
+
tag: 'td',
|
|
3309
|
+
actionable: false,
|
|
3310
|
+
text: (lastCell.textContent || '').trim().slice(0, 80) || undefined,
|
|
3311
|
+
label: 'last cell in the final row of ' + tableCss,
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// Same-origin iframe values are a normal part of the live
|
|
3317
|
+
// scene. Their synthetic token is translated to Target::Framed
|
|
3318
|
+
// before tracing, just like a scoped row token; no synthetic
|
|
3319
|
+
// authoring syntax is persisted or shown to the human.
|
|
3320
|
+
for (const frameEl of document.querySelectorAll('iframe, frame')) {
|
|
3321
|
+
const frameRect = frameEl.getBoundingClientRect();
|
|
3322
|
+
const frameStyle = getComputedStyle(frameEl);
|
|
3323
|
+
if (frameStyle.display === 'none' || frameStyle.visibility === 'hidden' ||
|
|
3324
|
+
Number(frameStyle.opacity) <= 0 || frameRect.width <= 0 || frameRect.height <= 0) {
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
const frame = frameEl.getAttribute('title') || frameEl.getAttribute('name') ||
|
|
3328
|
+
frameEl.id || frameEl.getAttribute('aria-label');
|
|
3329
|
+
if (!frame) continue;
|
|
3330
|
+
let frameDoc;
|
|
3331
|
+
try { frameDoc = frameEl.contentDocument; } catch (_) { continue; }
|
|
3332
|
+
if (!frameDoc || !frameDoc.body) continue;
|
|
3333
|
+
const inner = [frameDoc.body].concat(Array.from(frameDoc.querySelectorAll(
|
|
3334
|
+
'input, button, select, textarea, a, [role=button], [role=checkbox], [role=radio], [role=menuitem], [id]'
|
|
3335
|
+
)));
|
|
3336
|
+
for (const el of inner) {
|
|
3337
|
+
const style = frameDoc.defaultView.getComputedStyle(el);
|
|
3338
|
+
const rect = el.getBoundingClientRect();
|
|
3339
|
+
if (style.display === 'none' || style.visibility === 'hidden' ||
|
|
3340
|
+
Number(style.opacity) <= 0 || rect.width <= 0 || rect.height <= 0) {
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
let css = el === frameDoc.body ? 'body' : null;
|
|
3344
|
+
if (!css && el.id) css = '#' + CSS.escape(el.id);
|
|
3345
|
+
if (!css) {
|
|
3346
|
+
for (const attr of ['data-testid', 'data-test', 'data-qa', 'aria-label', 'name']) {
|
|
3347
|
+
const value = el.getAttribute(attr);
|
|
3348
|
+
if (!value) continue;
|
|
3349
|
+
const candidate = el.tagName.toLowerCase() + '[' + attr + '="' +
|
|
3350
|
+
CSS.escape(value) + '"]';
|
|
3351
|
+
if (frameDoc.querySelectorAll(candidate).length === 1) {
|
|
3352
|
+
css = candidate;
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (!css) continue;
|
|
3358
|
+
const innerTarget = 'css:' + css;
|
|
3359
|
+
const token = 'framed:' + JSON.stringify(frame) + ' > ' + innerTarget;
|
|
3360
|
+
const actionable = el.matches(
|
|
3361
|
+
'input, button, select, textarea, a, [role=button], [role=checkbox], [role=radio], [role=menuitem]'
|
|
3362
|
+
);
|
|
3363
|
+
const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
|
|
3364
|
+
: (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
|
|
3365
|
+
entries.push({
|
|
3366
|
+
target: token,
|
|
3367
|
+
css,
|
|
3368
|
+
tag: el.tagName.toLowerCase(),
|
|
3369
|
+
actionable,
|
|
3370
|
+
text: el === frameDoc.body ? undefined
|
|
3371
|
+
: (el.value || el.textContent || '').trim().slice(0, 80) || undefined,
|
|
3372
|
+
label: label || (el === frameDoc.body ? 'scroll surface' : undefined),
|
|
3373
|
+
scope: { frame, inner: innerTarget },
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return JSON.stringify(entries);
|
|
3378
|
+
})()
|
|
3125
3379
|
"#;
|
|
3126
3380
|
let value = self
|
|
3127
3381
|
.tab()?
|
|
@@ -3174,6 +3428,17 @@ mod tests {
|
|
|
3174
3428
|
assert!(!headed.headless, "FLOWPROOF_HEADED must show the window");
|
|
3175
3429
|
}
|
|
3176
3430
|
|
|
3431
|
+
/// A visible shared browser leaks its keep-alive window into the user's
|
|
3432
|
+
/// desktop after the flow tab closes. Headless suites retain reuse; either
|
|
3433
|
+
/// explicit opt-out or headed mode must select one privately-owned process.
|
|
3434
|
+
#[test]
|
|
3435
|
+
fn headed_runs_never_use_the_shared_keep_alive_browser() {
|
|
3436
|
+
assert!(super::should_share_browser(false, false));
|
|
3437
|
+
assert!(!super::should_share_browser(true, false));
|
|
3438
|
+
assert!(!super::should_share_browser(false, true));
|
|
3439
|
+
assert!(!super::should_share_browser(true, true));
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3177
3442
|
/// The headed launch failure must name the display, and must not invent one
|
|
3178
3443
|
/// when headless fails for an unrelated reason.
|
|
3179
3444
|
///
|