flowproof 0.6.0__tar.gz → 0.6.1__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.6.0 → flowproof-0.6.1}/Cargo.lock +7 -7
- {flowproof-0.6.0 → flowproof-0.6.1}/Cargo.toml +1 -1
- {flowproof-0.6.0 → flowproof-0.6.1}/PKG-INFO +8 -5
- {flowproof-0.6.0 → flowproof-0.6.1}/README.md +6 -3
- flowproof-0.6.1/crates/flowproof-cli/tests/examples_resolve.rs +154 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/cassette.rs +89 -3
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/cassette_diff.rs +4 -1
- {flowproof-0.6.0 → flowproof-0.6.1}/flowproof/__init__.py +1 -1
- {flowproof-0.6.0 → flowproof-0.6.1}/pyproject.toml +2 -2
- flowproof-0.6.0/crates/flowproof-cli/tests/examples_resolve.rs +0 -72
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/egress.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-adapters/src/web.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/author.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/clarify.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/llm.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/recorder.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/rules.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-agent/src/spec.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/src/agent_flow.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/oob.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-python/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-replay/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-replay/src/runrecord.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/format.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/secret_scan.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/flowproof/cli.py +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/flowproof/flow.py +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/flowproof/mcp_server.py +0 -0
- {flowproof-0.6.0 → flowproof-0.6.1}/flowproof/py.typed +0 -0
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "flowproof-adapters"
|
|
763
|
-
version = "0.6.
|
|
763
|
+
version = "0.6.1"
|
|
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.6.
|
|
782
|
+
version = "0.6.1"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"chrono",
|
|
785
785
|
"flowproof-driver",
|
|
@@ -795,7 +795,7 @@ dependencies = [
|
|
|
795
795
|
|
|
796
796
|
[[package]]
|
|
797
797
|
name = "flowproof-cli"
|
|
798
|
-
version = "0.6.
|
|
798
|
+
version = "0.6.1"
|
|
799
799
|
dependencies = [
|
|
800
800
|
"ab_glyph",
|
|
801
801
|
"chrono",
|
|
@@ -816,7 +816,7 @@ dependencies = [
|
|
|
816
816
|
|
|
817
817
|
[[package]]
|
|
818
818
|
name = "flowproof-driver"
|
|
819
|
-
version = "0.6.
|
|
819
|
+
version = "0.6.1"
|
|
820
820
|
dependencies = [
|
|
821
821
|
"image",
|
|
822
822
|
"postgres",
|
|
@@ -830,7 +830,7 @@ dependencies = [
|
|
|
830
830
|
|
|
831
831
|
[[package]]
|
|
832
832
|
name = "flowproof-python"
|
|
833
|
-
version = "0.6.
|
|
833
|
+
version = "0.6.1"
|
|
834
834
|
dependencies = [
|
|
835
835
|
"flowproof-agent",
|
|
836
836
|
"flowproof-cli",
|
|
@@ -842,7 +842,7 @@ dependencies = [
|
|
|
842
842
|
|
|
843
843
|
[[package]]
|
|
844
844
|
name = "flowproof-replay"
|
|
845
|
-
version = "0.6.
|
|
845
|
+
version = "0.6.1"
|
|
846
846
|
dependencies = [
|
|
847
847
|
"chrono",
|
|
848
848
|
"flowproof-agent",
|
|
@@ -856,7 +856,7 @@ dependencies = [
|
|
|
856
856
|
|
|
857
857
|
[[package]]
|
|
858
858
|
name = "flowproof-trace"
|
|
859
|
-
version = "0.6.
|
|
859
|
+
version = "0.6.1"
|
|
860
860
|
dependencies = [
|
|
861
861
|
"jsonschema",
|
|
862
862
|
"regex",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproof-replay", "crates/flowproof-agent", "crates/flowproof-adapters", "crates/flowproof-cli", "crates/flowproof-python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.6.
|
|
6
|
+
version = "0.6.1"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/automators-com/flowproof"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowproof
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -8,7 +8,7 @@ Classifier: Programming Language :: Rust
|
|
|
8
8
|
Classifier: Topic :: Software Development :: Testing
|
|
9
9
|
Requires-Dist: mcp>=1.2 ; python_full_version >= '3.10' and extra == 'mcp'
|
|
10
10
|
Provides-Extra: mcp
|
|
11
|
-
Summary:
|
|
11
|
+
Summary: Deterministic tests for AI agents: record a run once, replay it with zero LLM calls. Assert which tools an agent called, with which arguments, in which order.
|
|
12
12
|
Author-email: Automators <hello@automators.com>
|
|
13
13
|
License-Expression: Apache-2.0
|
|
14
14
|
Requires-Python: >=3.9
|
|
@@ -18,9 +18,12 @@ Project-URL: Repository, https://github.com/automators-com/flowproof
|
|
|
18
18
|
|
|
19
19
|
# flowproof
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
and
|
|
21
|
+
Run your AI agent once, keep the recording, and assert against it from then
|
|
22
|
+
on. flowproof captures the run at the model boundary - every request and
|
|
23
|
+
every tool-call decision - and serves it back on later runs, so replay makes
|
|
24
|
+
**zero LLM calls**. You assert which tools were called, with which arguments,
|
|
25
|
+
in which order, and which were not. The same engine drives web, desktop and
|
|
26
|
+
Citrix.
|
|
24
27
|
|
|
25
28
|
**Agents author, a deterministic engine executes.** A flow is described in
|
|
26
29
|
YAML with natural-language steps, recorded once against the live app, and
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# flowproof
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
and
|
|
3
|
+
Run your AI agent once, keep the recording, and assert against it from then
|
|
4
|
+
on. flowproof captures the run at the model boundary - every request and
|
|
5
|
+
every tool-call decision - and serves it back on later runs, so replay makes
|
|
6
|
+
**zero LLM calls**. You assert which tools were called, with which arguments,
|
|
7
|
+
in which order, and which were not. The same engine drives web, desktop and
|
|
8
|
+
Citrix.
|
|
6
9
|
|
|
7
10
|
**Agents author, a deterministic engine executes.** A flow is described in
|
|
8
11
|
YAML with natural-language steps, recorded once against the live app, and
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//! Shipped examples must stay honest: every step parses and resolves
|
|
2
|
+
//! through the deterministic rules — no live system needed, no model
|
|
3
|
+
//! backend. This is the same role `documented_grammar_examples_all_
|
|
4
|
+
//! resolve` plays for docs/authoring.md.
|
|
5
|
+
|
|
6
|
+
use flowproof_agent::{FlowSpec, SuiteManifest};
|
|
7
|
+
|
|
8
|
+
const FIORI_SPEC: &str = include_str!("../../../examples/fiori/manage-info-records.flow.yaml");
|
|
9
|
+
const FIORI_SUITE: &str = include_str!("../../../examples/fiori/suite.yaml");
|
|
10
|
+
const CONN_TEST_SPEC: &str = include_str!("../../../examples/api/connection-test.flow.yaml");
|
|
11
|
+
/// The npm-path agent quickstart. It is the first example a reader coming
|
|
12
|
+
/// from `npx flowproof` runs, so it has to keep parsing.
|
|
13
|
+
const AGENT_NODE_SPEC: &str = include_str!("../../../examples/agent-demo/weather-node.flow.yaml");
|
|
14
|
+
const AGENT_PY_SPEC: &str = include_str!("../../../examples/agent-demo/weather.flow.yaml");
|
|
15
|
+
|
|
16
|
+
#[test]
|
|
17
|
+
fn connection_test_example_resolves_with_body_and_headers() {
|
|
18
|
+
let spec = FlowSpec::parse(CONN_TEST_SPEC).expect("example parses");
|
|
19
|
+
assert_eq!(spec.app.id(), "api");
|
|
20
|
+
let actions =
|
|
21
|
+
flowproof_agent::rules::resolve_step(spec.app.id(), &spec.steps[0]).expect("step resolves");
|
|
22
|
+
let flowproof_agent::rules::ResolvedAction::AssertApi {
|
|
23
|
+
headers,
|
|
24
|
+
body,
|
|
25
|
+
status,
|
|
26
|
+
..
|
|
27
|
+
} = &actions[0]
|
|
28
|
+
else {
|
|
29
|
+
panic!("expected AssertApi");
|
|
30
|
+
};
|
|
31
|
+
// Raw refs in the resolved action — resolution is probe-time only.
|
|
32
|
+
assert_eq!(
|
|
33
|
+
headers.get("Authorization").map(String::as_str),
|
|
34
|
+
Some("Bearer ${DM_SESSION_TOKEN}")
|
|
35
|
+
);
|
|
36
|
+
let body = body.as_ref().expect("body present");
|
|
37
|
+
assert_eq!(body["connectionString"], "${TEST_CONN_STRING}");
|
|
38
|
+
// Pinned to the real DataMaker /connections/test contract the example
|
|
39
|
+
// mirrors (the api_pipeline mock speaks the same shape): the field is
|
|
40
|
+
// `type`, and an unsupported provider answers 500. Drift fails here.
|
|
41
|
+
assert_eq!(body["type"], "postgres");
|
|
42
|
+
assert_eq!(*status, Some(500));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn fiori_example_resolves_entirely_via_rules() {
|
|
47
|
+
let spec = FlowSpec::parse(FIORI_SPEC).expect("example parses");
|
|
48
|
+
assert_eq!(spec.app.id(), "web", "Fiori is a browser app");
|
|
49
|
+
assert!(
|
|
50
|
+
spec.url
|
|
51
|
+
.as_deref()
|
|
52
|
+
.unwrap_or_default()
|
|
53
|
+
.contains("${FIORI_BASE_URL}"),
|
|
54
|
+
"launch URL stays a ${{VAR}} reference"
|
|
55
|
+
);
|
|
56
|
+
for step in &spec.steps {
|
|
57
|
+
let actions = flowproof_agent::rules::resolve_step(spec.app.id(), step)
|
|
58
|
+
.unwrap_or_else(|e| panic!("step '{}' must resolve via rules: {e}", step.intent()));
|
|
59
|
+
assert!(
|
|
60
|
+
!actions.is_empty(),
|
|
61
|
+
"step '{}' yields actions",
|
|
62
|
+
step.intent()
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn fiori_suite_manifest_declares_the_data_leg() {
|
|
69
|
+
let manifest: SuiteManifest = serde_yaml::from_str(FIORI_SUITE).expect("suite.yaml parses");
|
|
70
|
+
let cmd = manifest.env_from.expect("env_from present");
|
|
71
|
+
assert!(
|
|
72
|
+
cmd.contains("datamaker"),
|
|
73
|
+
"data comes from the DataMaker CLI"
|
|
74
|
+
);
|
|
75
|
+
assert!(manifest.env.contains_key("FIORI_BASE_URL"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// The quickstart's two agent demos must stay runnable and stay TWINS: the
|
|
79
|
+
/// docs present them as the same flow in two languages, so a change to one
|
|
80
|
+
/// that is not mirrored in the other makes the documentation lie.
|
|
81
|
+
#[test]
|
|
82
|
+
fn both_agent_demos_resolve_and_assert_the_same_thing() {
|
|
83
|
+
let node = FlowSpec::parse(AGENT_NODE_SPEC).expect("node example parses");
|
|
84
|
+
let py = FlowSpec::parse(AGENT_PY_SPEC).expect("python example parses");
|
|
85
|
+
|
|
86
|
+
for spec in [&node, &py] {
|
|
87
|
+
assert_eq!(spec.app.id(), "agent");
|
|
88
|
+
// A mocked tool is what makes replay deterministic here; without a
|
|
89
|
+
// result the demo would depend on a live timestamp.
|
|
90
|
+
let mocked = spec
|
|
91
|
+
.tools
|
|
92
|
+
.iter()
|
|
93
|
+
.find(|t| t.name == "get_weather")
|
|
94
|
+
.expect("get_weather declared");
|
|
95
|
+
assert!(!mocked.result.is_null(), "get_weather must carry a result:");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Same trajectory, same assertions - only the runtime differs.
|
|
99
|
+
assert_eq!(
|
|
100
|
+
node.steps.len(),
|
|
101
|
+
py.steps.len(),
|
|
102
|
+
"the two demos must stay the same flow"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// The Node demo must not need Python, which is the entire reason it
|
|
106
|
+
// exists: the npm install path has to stand on its own.
|
|
107
|
+
let command = node
|
|
108
|
+
.agent
|
|
109
|
+
.as_ref()
|
|
110
|
+
.and_then(|a| a.command.clone())
|
|
111
|
+
.expect("node demo has a command");
|
|
112
|
+
assert!(
|
|
113
|
+
command.starts_with("node "),
|
|
114
|
+
"the npm-path demo must run under node, got: {command}"
|
|
115
|
+
);
|
|
116
|
+
assert!(
|
|
117
|
+
!command.contains("python"),
|
|
118
|
+
"the npm-path demo must not need Python: {command}"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// The quickstart is the most-read page we have and it is now the front
|
|
123
|
+
/// door for the npm audience, so its YAML must not drift from the file it
|
|
124
|
+
/// claims to quote. A reader who copies a block that no longer parses is
|
|
125
|
+
/// the worst possible first experience.
|
|
126
|
+
#[test]
|
|
127
|
+
fn the_quickstart_quotes_the_shipped_agent_example_verbatim() {
|
|
128
|
+
const DOC: &str = include_str!("../../../docs/getting-started.md");
|
|
129
|
+
|
|
130
|
+
// Pull the fenced block that names the example file.
|
|
131
|
+
let marker = "```yaml\n# examples/agent-demo/weather-node.flow.yaml\n";
|
|
132
|
+
let start = DOC
|
|
133
|
+
.find(marker)
|
|
134
|
+
.expect("quickstart quotes the node example")
|
|
135
|
+
+ marker.len();
|
|
136
|
+
let block = &DOC[start..];
|
|
137
|
+
let block = &block[..block.find("```").expect("fence closes")];
|
|
138
|
+
|
|
139
|
+
// The shipped file, minus its comment header.
|
|
140
|
+
let shipped: String = AGENT_NODE_SPEC
|
|
141
|
+
.lines()
|
|
142
|
+
.filter(|l| !l.starts_with('#'))
|
|
143
|
+
.collect::<Vec<_>>()
|
|
144
|
+
.join("\n");
|
|
145
|
+
|
|
146
|
+
assert_eq!(
|
|
147
|
+
block.trim(),
|
|
148
|
+
shipped.trim(),
|
|
149
|
+
"docs/getting-started.md has drifted from examples/agent-demo/weather-node.flow.yaml"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// And it must still parse, so the block a reader copies actually runs.
|
|
153
|
+
FlowSpec::parse(block).expect("the quoted quickstart block parses");
|
|
154
|
+
}
|
|
@@ -203,11 +203,35 @@ fn message_divergence(recorded: &[Message], incoming: &[Message]) -> Option<Stri
|
|
|
203
203
|
.collect::<Vec<_>>()
|
|
204
204
|
.join(", ")
|
|
205
205
|
};
|
|
206
|
+
// When the SAME tools were called and only their arguments
|
|
207
|
+
// moved, the two name lists are identical and printing them
|
|
208
|
+
// says nothing at all - the reader is told something changed
|
|
209
|
+
// and left to find it. Name the argument PATH instead.
|
|
210
|
+
let (want_names, got_names) = (names(&want.tool_calls), names(&got.tool_calls));
|
|
211
|
+
if want_names == got_names {
|
|
212
|
+
let changes: Vec<String> = want
|
|
213
|
+
.tool_calls
|
|
214
|
+
.iter()
|
|
215
|
+
.zip(&got.tool_calls)
|
|
216
|
+
.flat_map(|(a, b)| {
|
|
217
|
+
crate::cassette_diff::argument_changes(a, b)
|
|
218
|
+
.into_iter()
|
|
219
|
+
.map(move |(path, before, after)| {
|
|
220
|
+
format!("{}.{path}: recorded {before}, replayed {after}", a.name)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
.collect();
|
|
224
|
+
if !changes.is_empty() {
|
|
225
|
+
return Some(format!(
|
|
226
|
+
"message {i} ({}) tool call arguments changed\n {}",
|
|
227
|
+
want.role,
|
|
228
|
+
changes.join("\n "),
|
|
229
|
+
));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
206
232
|
return Some(format!(
|
|
207
233
|
"message {i} ({}) tool calls changed\n recorded: [{}]\n replayed: [{}]",
|
|
208
|
-
want.role,
|
|
209
|
-
names(&want.tool_calls),
|
|
210
|
-
names(&got.tool_calls),
|
|
234
|
+
want.role, want_names, got_names,
|
|
211
235
|
));
|
|
212
236
|
}
|
|
213
237
|
return Some(format!("message {i} ({}) changed", want.role));
|
|
@@ -380,6 +404,68 @@ mod tests {
|
|
|
380
404
|
}
|
|
381
405
|
}
|
|
382
406
|
|
|
407
|
+
/// An argument-only change used to print two IDENTICAL tool-name lists
|
|
408
|
+
/// ("recorded: [book] / replayed: [book]"), telling the reader that
|
|
409
|
+
/// something moved but not what. It names the PATH now.
|
|
410
|
+
#[test]
|
|
411
|
+
fn an_argument_only_change_names_the_path_that_moved() {
|
|
412
|
+
let recorded = assistant_with(vec![call("book", r#"{"flight":{"id":"KQ311"},"seats":2}"#)]);
|
|
413
|
+
let replayed = assistant_with(vec![call("book", r#"{"flight":{"id":"KQ999"},"seats":2}"#)]);
|
|
414
|
+
let detail =
|
|
415
|
+
message_divergence(&[recorded], &[replayed]).expect("a change is a divergence");
|
|
416
|
+
assert!(
|
|
417
|
+
detail.contains("book.flight.id"),
|
|
418
|
+
"names the path: {detail}"
|
|
419
|
+
);
|
|
420
|
+
assert!(
|
|
421
|
+
detail.contains("KQ311"),
|
|
422
|
+
"shows what was recorded: {detail}"
|
|
423
|
+
);
|
|
424
|
+
assert!(
|
|
425
|
+
detail.contains("KQ999"),
|
|
426
|
+
"shows what was replayed: {detail}"
|
|
427
|
+
);
|
|
428
|
+
// The unchanged argument must not be listed as noise.
|
|
429
|
+
assert!(!detail.contains("seats"), "only the moved path: {detail}");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// An argument that APPEARS is drift too, and the most likely shape of
|
|
433
|
+
/// a real regression: an agent starts passing something extra.
|
|
434
|
+
#[test]
|
|
435
|
+
fn an_added_argument_is_reported_as_absent_before() {
|
|
436
|
+
let recorded = assistant_with(vec![call("book", r#"{"id":"KQ311"}"#)]);
|
|
437
|
+
let replayed = assistant_with(vec![call("book", r#"{"id":"KQ311","override":true}"#)]);
|
|
438
|
+
let detail =
|
|
439
|
+
message_divergence(&[recorded], &[replayed]).expect("an added argument diverges");
|
|
440
|
+
assert!(detail.contains("book.override"), "{detail}");
|
|
441
|
+
assert!(detail.contains("absent"), "{detail}");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// A DIFFERENT tool is a different failure and must keep the name lists,
|
|
445
|
+
/// which are the useful thing in that case.
|
|
446
|
+
#[test]
|
|
447
|
+
fn a_different_tool_still_reports_the_names() {
|
|
448
|
+
let recorded = assistant_with(vec![call("book", "{}")]);
|
|
449
|
+
let replayed = assistant_with(vec![call("cancel", "{}")]);
|
|
450
|
+
let detail =
|
|
451
|
+
message_divergence(&[recorded], &[replayed]).expect("a different tool diverges");
|
|
452
|
+
assert!(detail.contains("tool calls changed"), "{detail}");
|
|
453
|
+
assert!(
|
|
454
|
+
detail.contains("book") && detail.contains("cancel"),
|
|
455
|
+
"{detail}"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Arguments that are not JSON cannot be diffed field by field. Say the
|
|
460
|
+
/// whole thing moved rather than pretending to be precise about it.
|
|
461
|
+
#[test]
|
|
462
|
+
fn unparseable_arguments_report_the_whole_payload() {
|
|
463
|
+
let recorded = assistant_with(vec![call("book", "not json")]);
|
|
464
|
+
let replayed = assistant_with(vec![call("book", "also not json")]);
|
|
465
|
+
let detail = message_divergence(&[recorded], &[replayed]).expect("still a divergence");
|
|
466
|
+
assert!(detail.contains("book.arguments"), "{detail}");
|
|
467
|
+
}
|
|
468
|
+
|
|
383
469
|
/// A two-turn booking trajectory: the model asks for a tool, the tool
|
|
384
470
|
/// result comes back, the model replies.
|
|
385
471
|
fn booking() -> Cassette {
|
|
@@ -271,7 +271,10 @@ fn prompt_change(before: &[Message], after: &[Message]) -> Option<String> {
|
|
|
271
271
|
///
|
|
272
272
|
/// Field-level on purpose. Two JSON blobs side by side make a reviewer do
|
|
273
273
|
/// the diffing; `flight.id KQ311 -> KQ999` is the finding itself.
|
|
274
|
-
|
|
274
|
+
pub(crate) fn argument_changes(
|
|
275
|
+
before: &ToolCall,
|
|
276
|
+
after: &ToolCall,
|
|
277
|
+
) -> Vec<(String, String, String)> {
|
|
275
278
|
let (Some(old), Some(new)) = (before.arguments_json(), after.arguments_json()) else {
|
|
276
279
|
// Unparseable arguments cannot be compared field by field. Say
|
|
277
280
|
// the whole thing moved rather than pretending to be precise.
|
|
@@ -4,8 +4,8 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flowproof"
|
|
7
|
-
version = "0.6.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.6.1"
|
|
8
|
+
description = "Deterministic tests for AI agents: record a run once, replay it with zero LLM calls. Assert which tools an agent called, with which arguments, in which order."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
//! Shipped examples must stay honest: every step parses and resolves
|
|
2
|
-
//! through the deterministic rules — no live system needed, no model
|
|
3
|
-
//! backend. This is the same role `documented_grammar_examples_all_
|
|
4
|
-
//! resolve` plays for docs/authoring.md.
|
|
5
|
-
|
|
6
|
-
use flowproof_agent::{FlowSpec, SuiteManifest};
|
|
7
|
-
|
|
8
|
-
const FIORI_SPEC: &str = include_str!("../../../examples/fiori/manage-info-records.flow.yaml");
|
|
9
|
-
const FIORI_SUITE: &str = include_str!("../../../examples/fiori/suite.yaml");
|
|
10
|
-
const CONN_TEST_SPEC: &str = include_str!("../../../examples/api/connection-test.flow.yaml");
|
|
11
|
-
|
|
12
|
-
#[test]
|
|
13
|
-
fn connection_test_example_resolves_with_body_and_headers() {
|
|
14
|
-
let spec = FlowSpec::parse(CONN_TEST_SPEC).expect("example parses");
|
|
15
|
-
assert_eq!(spec.app.id(), "api");
|
|
16
|
-
let actions =
|
|
17
|
-
flowproof_agent::rules::resolve_step(spec.app.id(), &spec.steps[0]).expect("step resolves");
|
|
18
|
-
let flowproof_agent::rules::ResolvedAction::AssertApi {
|
|
19
|
-
headers,
|
|
20
|
-
body,
|
|
21
|
-
status,
|
|
22
|
-
..
|
|
23
|
-
} = &actions[0]
|
|
24
|
-
else {
|
|
25
|
-
panic!("expected AssertApi");
|
|
26
|
-
};
|
|
27
|
-
// Raw refs in the resolved action — resolution is probe-time only.
|
|
28
|
-
assert_eq!(
|
|
29
|
-
headers.get("Authorization").map(String::as_str),
|
|
30
|
-
Some("Bearer ${DM_SESSION_TOKEN}")
|
|
31
|
-
);
|
|
32
|
-
let body = body.as_ref().expect("body present");
|
|
33
|
-
assert_eq!(body["connectionString"], "${TEST_CONN_STRING}");
|
|
34
|
-
// Pinned to the real DataMaker /connections/test contract the example
|
|
35
|
-
// mirrors (the api_pipeline mock speaks the same shape): the field is
|
|
36
|
-
// `type`, and an unsupported provider answers 500. Drift fails here.
|
|
37
|
-
assert_eq!(body["type"], "postgres");
|
|
38
|
-
assert_eq!(*status, Some(500));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#[test]
|
|
42
|
-
fn fiori_example_resolves_entirely_via_rules() {
|
|
43
|
-
let spec = FlowSpec::parse(FIORI_SPEC).expect("example parses");
|
|
44
|
-
assert_eq!(spec.app.id(), "web", "Fiori is a browser app");
|
|
45
|
-
assert!(
|
|
46
|
-
spec.url
|
|
47
|
-
.as_deref()
|
|
48
|
-
.unwrap_or_default()
|
|
49
|
-
.contains("${FIORI_BASE_URL}"),
|
|
50
|
-
"launch URL stays a ${{VAR}} reference"
|
|
51
|
-
);
|
|
52
|
-
for step in &spec.steps {
|
|
53
|
-
let actions = flowproof_agent::rules::resolve_step(spec.app.id(), step)
|
|
54
|
-
.unwrap_or_else(|e| panic!("step '{}' must resolve via rules: {e}", step.intent()));
|
|
55
|
-
assert!(
|
|
56
|
-
!actions.is_empty(),
|
|
57
|
-
"step '{}' yields actions",
|
|
58
|
-
step.intent()
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
#[test]
|
|
64
|
-
fn fiori_suite_manifest_declares_the_data_leg() {
|
|
65
|
-
let manifest: SuiteManifest = serde_yaml::from_str(FIORI_SUITE).expect("suite.yaml parses");
|
|
66
|
-
let cmd = manifest.env_from.expect("env_from present");
|
|
67
|
-
assert!(
|
|
68
|
-
cmd.contains("datamaker"),
|
|
69
|
-
"data comes from the DataMaker CLI"
|
|
70
|
-
);
|
|
71
|
-
assert!(manifest.env.contains_key("FIORI_BASE_URL"));
|
|
72
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowproof-0.6.0 → flowproof-0.6.1}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|