fleet-python 0.2.99__tar.gz → 0.2.101__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.
- {fleet_python-0.2.99/fleet_python.egg-info → fleet_python-0.2.101}/PKG-INFO +1 -1
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/__init__.py +1 -1
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/instance/client.py +6 -2
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/resources/sqlite.py +88 -3
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/base.py +1 -1
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/instance/client.py +6 -2
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/resources/sqlite.py +88 -3
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/db.py +483 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet_python.egg-info/SOURCES.txt +1 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/pyproject.toml +1 -1
- fleet_python-0.2.101/tests/test_expect_exactly.py +4148 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/LICENSE +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/README.md +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/diff_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_account.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_sync.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_task.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/openai_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/quickstart.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/resources/api.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/cli.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/config.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/env/client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/global_client.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/models.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/resources/api.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/tasks.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/types.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/scripts/unasync.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/setup.cfg +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/__init__.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.99 → fleet_python-0.2.101}/tests/test_verifier_from_string.py +0 -0
|
@@ -166,8 +166,12 @@ class AsyncInstanceClient:
|
|
|
166
166
|
if self._resources is None:
|
|
167
167
|
response = await self.client.request("GET", "/resources")
|
|
168
168
|
if response.status_code != 200:
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
response_body = response.text[:500] if response.text else "empty"
|
|
170
|
+
self._resources = [] # Mark as loaded (empty) to prevent retries
|
|
171
|
+
raise FleetEnvironmentError(
|
|
172
|
+
f"Failed to load instance resources: status_code={response.status_code} "
|
|
173
|
+
f"(url={self.base_url}, response={response_body})"
|
|
174
|
+
)
|
|
171
175
|
|
|
172
176
|
# Handle both old and new response formats
|
|
173
177
|
response_data = response.json()
|
|
@@ -8,6 +8,7 @@ import sqlite3
|
|
|
8
8
|
import os
|
|
9
9
|
import asyncio
|
|
10
10
|
import re
|
|
11
|
+
import json
|
|
11
12
|
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
13
14
|
|
|
@@ -21,6 +22,7 @@ from fleet.verifiers.db import (
|
|
|
21
22
|
_get_row_identifier,
|
|
22
23
|
_format_row_for_error,
|
|
23
24
|
_values_equivalent,
|
|
25
|
+
validate_diff_expect_exactly,
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
@@ -1754,7 +1756,8 @@ class AsyncSnapshotDiff:
|
|
|
1754
1756
|
return await self._expect_no_changes()
|
|
1755
1757
|
|
|
1756
1758
|
resource = self.after.resource
|
|
1757
|
-
|
|
1759
|
+
# Disabled: structured diff endpoint not yet available
|
|
1760
|
+
if False and resource.client is not None and resource._mode == "http":
|
|
1758
1761
|
api_diff = None
|
|
1759
1762
|
try:
|
|
1760
1763
|
payload = {}
|
|
@@ -1778,7 +1781,7 @@ class AsyncSnapshotDiff:
|
|
|
1778
1781
|
# Fall back to local diff if API call fails
|
|
1779
1782
|
print(f"Warning: Failed to fetch structured diff from API: {e}")
|
|
1780
1783
|
print("Falling back to local diff computation...")
|
|
1781
|
-
|
|
1784
|
+
|
|
1782
1785
|
# Validate outside try block so AssertionError propagates
|
|
1783
1786
|
if api_diff is not None:
|
|
1784
1787
|
return await self._validate_diff_against_allowed_changes_v2(api_diff, allowed_changes)
|
|
@@ -1793,6 +1796,81 @@ class AsyncSnapshotDiff:
|
|
|
1793
1796
|
diff, allowed_changes
|
|
1794
1797
|
)
|
|
1795
1798
|
|
|
1799
|
+
async def expect_exactly(self, expected_changes: List[Dict[str, Any]]):
|
|
1800
|
+
"""Verify that EXACTLY the specified changes occurred.
|
|
1801
|
+
|
|
1802
|
+
This is stricter than expect_only_v2:
|
|
1803
|
+
1. All changes in diff must match a spec (no unexpected changes)
|
|
1804
|
+
2. All specs must have a matching change in diff (no missing expected changes)
|
|
1805
|
+
|
|
1806
|
+
This method is ideal for verifying that an agent performed exactly what was expected -
|
|
1807
|
+
not more, not less.
|
|
1808
|
+
|
|
1809
|
+
Args:
|
|
1810
|
+
expected_changes: List of expected change specs. Each spec requires:
|
|
1811
|
+
- "type": "insert", "modify", or "delete" (required)
|
|
1812
|
+
- "table": table name (required)
|
|
1813
|
+
- "pk": primary key value (required)
|
|
1814
|
+
|
|
1815
|
+
Spec formats by type:
|
|
1816
|
+
- Insert: {"type": "insert", "table": "t", "pk": 1, "fields": [...]}
|
|
1817
|
+
- Modify: {"type": "modify", "table": "t", "pk": 1, "resulting_fields": [...], "no_other_changes": True/False}
|
|
1818
|
+
- Delete: {"type": "delete", "table": "t", "pk": 1}
|
|
1819
|
+
|
|
1820
|
+
Field specs are 2-tuples: (field_name, expected_value)
|
|
1821
|
+
- ("name", "Alice"): check field equals "Alice"
|
|
1822
|
+
- ("name", ...): accept any value (ellipsis)
|
|
1823
|
+
- ("name", None): check field is SQL NULL
|
|
1824
|
+
|
|
1825
|
+
Note: Legacy specs without explicit "type" are not supported.
|
|
1826
|
+
|
|
1827
|
+
Returns:
|
|
1828
|
+
self for method chaining
|
|
1829
|
+
|
|
1830
|
+
Raises:
|
|
1831
|
+
AssertionError: If there are unexpected changes OR if expected changes are missing
|
|
1832
|
+
ValueError: If specs are missing required fields or have invalid format
|
|
1833
|
+
"""
|
|
1834
|
+
# Get the diff (using HTTP if available, otherwise local)
|
|
1835
|
+
resource = self.after.resource
|
|
1836
|
+
diff = None
|
|
1837
|
+
|
|
1838
|
+
if resource.client is not None and resource._mode == "http":
|
|
1839
|
+
try:
|
|
1840
|
+
payload = {}
|
|
1841
|
+
if self.ignore_config:
|
|
1842
|
+
payload["ignore_config"] = {
|
|
1843
|
+
"tables": list(self.ignore_config.tables),
|
|
1844
|
+
"fields": list(self.ignore_config.fields),
|
|
1845
|
+
"table_fields": {
|
|
1846
|
+
table: list(fields) for table, fields in self.ignore_config.table_fields.items()
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
response = await resource.client.request(
|
|
1850
|
+
"POST",
|
|
1851
|
+
"/diff/structured",
|
|
1852
|
+
json=payload,
|
|
1853
|
+
)
|
|
1854
|
+
result = response.json()
|
|
1855
|
+
if result.get("success") and "diff" in result:
|
|
1856
|
+
diff = result["diff"]
|
|
1857
|
+
except Exception as e:
|
|
1858
|
+
print(f"Warning: Failed to fetch structured diff from API: {e}")
|
|
1859
|
+
print("Falling back to local diff computation...")
|
|
1860
|
+
|
|
1861
|
+
if diff is None:
|
|
1862
|
+
diff = await self._collect()
|
|
1863
|
+
|
|
1864
|
+
# Use shared validation logic
|
|
1865
|
+
success, error_msg, _ = validate_diff_expect_exactly(
|
|
1866
|
+
diff, expected_changes, self.ignore_config
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
if not success:
|
|
1870
|
+
raise AssertionError(error_msg)
|
|
1871
|
+
|
|
1872
|
+
return self
|
|
1873
|
+
|
|
1796
1874
|
async def _ensure_all_fetched(self):
|
|
1797
1875
|
"""Fetch ALL data from ALL tables upfront (non-lazy loading).
|
|
1798
1876
|
|
|
@@ -2114,7 +2192,14 @@ class AsyncSQLiteResource(Resource):
|
|
|
2114
2192
|
response = await self.client.request(
|
|
2115
2193
|
"GET", f"/resources/sqlite/{self.resource.name}/describe"
|
|
2116
2194
|
)
|
|
2117
|
-
|
|
2195
|
+
try:
|
|
2196
|
+
return DescribeResponse(**response.json())
|
|
2197
|
+
except json.JSONDecodeError as e:
|
|
2198
|
+
raise ValueError(
|
|
2199
|
+
f"Failed to parse JSON response from SQLite describe endpoint. "
|
|
2200
|
+
f"Status: {response.status_code}, "
|
|
2201
|
+
f"Response text: {response.text[:500]}"
|
|
2202
|
+
) from e
|
|
2118
2203
|
|
|
2119
2204
|
async def _describe_direct(self) -> DescribeResponse:
|
|
2120
2205
|
"""Describe database schema from local file or in-memory database."""
|
|
@@ -164,8 +164,12 @@ class InstanceClient:
|
|
|
164
164
|
if self._resources is None:
|
|
165
165
|
response = self.client.request("GET", "/resources")
|
|
166
166
|
if response.status_code != 200:
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
response_body = response.text[:500] if response.text else "empty"
|
|
168
|
+
self._resources = [] # Mark as loaded (empty) to prevent retries
|
|
169
|
+
raise FleetEnvironmentError(
|
|
170
|
+
f"Failed to load instance resources: status_code={response.status_code} "
|
|
171
|
+
f"(url={self.base_url}, response={response_body})"
|
|
172
|
+
)
|
|
169
173
|
|
|
170
174
|
# Handle both old and new response formats
|
|
171
175
|
response_data = response.json()
|
|
@@ -7,6 +7,7 @@ import tempfile
|
|
|
7
7
|
import sqlite3
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
import json
|
|
10
11
|
|
|
11
12
|
from typing import TYPE_CHECKING
|
|
12
13
|
|
|
@@ -20,6 +21,7 @@ from fleet.verifiers.db import (
|
|
|
20
21
|
_get_row_identifier,
|
|
21
22
|
_format_row_for_error,
|
|
22
23
|
_values_equivalent,
|
|
24
|
+
validate_diff_expect_exactly,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -1802,7 +1804,8 @@ class SyncSnapshotDiff:
|
|
|
1802
1804
|
return self._expect_no_changes()
|
|
1803
1805
|
|
|
1804
1806
|
resource = self.after.resource
|
|
1805
|
-
|
|
1807
|
+
# Disabled: structured diff endpoint not yet available
|
|
1808
|
+
if False and resource.client is not None and resource._mode == "http":
|
|
1806
1809
|
api_diff = None
|
|
1807
1810
|
try:
|
|
1808
1811
|
payload = {}
|
|
@@ -1826,7 +1829,7 @@ class SyncSnapshotDiff:
|
|
|
1826
1829
|
# Fall back to local diff if API call fails
|
|
1827
1830
|
print(f"Warning: Failed to fetch structured diff from API: {e}")
|
|
1828
1831
|
print("Falling back to local diff computation...")
|
|
1829
|
-
|
|
1832
|
+
|
|
1830
1833
|
# Validate outside try block so AssertionError propagates
|
|
1831
1834
|
if api_diff is not None:
|
|
1832
1835
|
return self._validate_diff_against_allowed_changes_v2(api_diff, allowed_changes)
|
|
@@ -1839,6 +1842,81 @@ class SyncSnapshotDiff:
|
|
|
1839
1842
|
diff = self._collect()
|
|
1840
1843
|
return self._validate_diff_against_allowed_changes_v2(diff, allowed_changes)
|
|
1841
1844
|
|
|
1845
|
+
def expect_exactly(self, expected_changes: List[Dict[str, Any]]):
|
|
1846
|
+
"""Verify that EXACTLY the specified changes occurred.
|
|
1847
|
+
|
|
1848
|
+
This is stricter than expect_only_v2:
|
|
1849
|
+
1. All changes in diff must match a spec (no unexpected changes)
|
|
1850
|
+
2. All specs must have a matching change in diff (no missing expected changes)
|
|
1851
|
+
|
|
1852
|
+
This method is ideal for verifying that an agent performed exactly what was expected -
|
|
1853
|
+
not more, not less.
|
|
1854
|
+
|
|
1855
|
+
Args:
|
|
1856
|
+
expected_changes: List of expected change specs. Each spec requires:
|
|
1857
|
+
- "type": "insert", "modify", or "delete" (required)
|
|
1858
|
+
- "table": table name (required)
|
|
1859
|
+
- "pk": primary key value (required)
|
|
1860
|
+
|
|
1861
|
+
Spec formats by type:
|
|
1862
|
+
- Insert: {"type": "insert", "table": "t", "pk": 1, "fields": [...]}
|
|
1863
|
+
- Modify: {"type": "modify", "table": "t", "pk": 1, "resulting_fields": [...], "no_other_changes": True/False}
|
|
1864
|
+
- Delete: {"type": "delete", "table": "t", "pk": 1}
|
|
1865
|
+
|
|
1866
|
+
Field specs are 2-tuples: (field_name, expected_value)
|
|
1867
|
+
- ("name", "Alice"): check field equals "Alice"
|
|
1868
|
+
- ("name", ...): accept any value (ellipsis)
|
|
1869
|
+
- ("name", None): check field is SQL NULL
|
|
1870
|
+
|
|
1871
|
+
Note: Legacy specs without explicit "type" are not supported.
|
|
1872
|
+
|
|
1873
|
+
Returns:
|
|
1874
|
+
self for method chaining
|
|
1875
|
+
|
|
1876
|
+
Raises:
|
|
1877
|
+
AssertionError: If there are unexpected changes OR if expected changes are missing
|
|
1878
|
+
ValueError: If specs are missing required fields or have invalid format
|
|
1879
|
+
"""
|
|
1880
|
+
# Get the diff (using HTTP if available, otherwise local)
|
|
1881
|
+
resource = self.after.resource
|
|
1882
|
+
diff = None
|
|
1883
|
+
|
|
1884
|
+
if resource.client is not None and resource._mode == "http":
|
|
1885
|
+
try:
|
|
1886
|
+
payload = {}
|
|
1887
|
+
if self.ignore_config:
|
|
1888
|
+
payload["ignore_config"] = {
|
|
1889
|
+
"tables": list(self.ignore_config.tables),
|
|
1890
|
+
"fields": list(self.ignore_config.fields),
|
|
1891
|
+
"table_fields": {
|
|
1892
|
+
table: list(fields) for table, fields in self.ignore_config.table_fields.items()
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
response = resource.client.request(
|
|
1896
|
+
"POST",
|
|
1897
|
+
"/diff/structured",
|
|
1898
|
+
json=payload,
|
|
1899
|
+
)
|
|
1900
|
+
result = response.json()
|
|
1901
|
+
if result.get("success") and "diff" in result:
|
|
1902
|
+
diff = result["diff"]
|
|
1903
|
+
except Exception as e:
|
|
1904
|
+
print(f"Warning: Failed to fetch structured diff from API: {e}")
|
|
1905
|
+
print("Falling back to local diff computation...")
|
|
1906
|
+
|
|
1907
|
+
if diff is None:
|
|
1908
|
+
diff = self._collect()
|
|
1909
|
+
|
|
1910
|
+
# Use shared validation logic
|
|
1911
|
+
success, error_msg, _ = validate_diff_expect_exactly(
|
|
1912
|
+
diff, expected_changes, self.ignore_config
|
|
1913
|
+
)
|
|
1914
|
+
|
|
1915
|
+
if not success:
|
|
1916
|
+
raise AssertionError(error_msg)
|
|
1917
|
+
|
|
1918
|
+
return self
|
|
1919
|
+
|
|
1842
1920
|
def _ensure_all_fetched(self):
|
|
1843
1921
|
"""Fetch ALL data from ALL tables upfront (non-lazy loading).
|
|
1844
1922
|
|
|
@@ -2160,7 +2238,14 @@ class SQLiteResource(Resource):
|
|
|
2160
2238
|
response = self.client.request(
|
|
2161
2239
|
"GET", f"/resources/sqlite/{self.resource.name}/describe"
|
|
2162
2240
|
)
|
|
2163
|
-
|
|
2241
|
+
try:
|
|
2242
|
+
return DescribeResponse(**response.json())
|
|
2243
|
+
except json.JSONDecodeError as e:
|
|
2244
|
+
raise ValueError(
|
|
2245
|
+
f"Failed to parse JSON response from SQLite describe endpoint. "
|
|
2246
|
+
f"Status: {response.status_code}, "
|
|
2247
|
+
f"Response text: {response.text[:500]}"
|
|
2248
|
+
) from e
|
|
2164
2249
|
|
|
2165
2250
|
def _describe_direct(self) -> DescribeResponse:
|
|
2166
2251
|
"""Describe database schema from local file or in-memory database."""
|