|
@@ -39,6 +39,27 @@ class FakeAuthoringAgent:
|
|
|
class FakeRuleRepository:
|
|
class FakeRuleRepository:
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
|
self.calls = []
|
|
self.calls = []
|
|
|
|
|
+ self.catalog_items = [
|
|
|
|
|
+ {
|
|
|
|
|
+ "asset_type": "rule",
|
|
|
|
|
+ "version_id": new_governance_uid(),
|
|
|
|
|
+ "asset_uid": new_governance_uid(),
|
|
|
|
|
+ "name": "手机号规范化",
|
|
|
|
|
+ "version": 3,
|
|
|
|
|
+ "owner": new_governance_uid(),
|
|
|
|
|
+ "status": "published",
|
|
|
|
|
+ "schema_compatibility": {
|
|
|
|
|
+ "input": "a" * 64,
|
|
|
|
|
+ "output": "b" * 64,
|
|
|
|
|
+ },
|
|
|
|
|
+ "impact_count": 2,
|
|
|
|
|
+ "backend": "polars_batch",
|
|
|
|
|
+ "latest_evidence": {
|
|
|
|
|
+ "compile": {"status": "success"},
|
|
|
|
|
+ "test": {"status": "success"},
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
|
|
|
def create_rule_version(self, **kwargs):
|
|
def create_rule_version(self, **kwargs):
|
|
|
self.calls.append(("create_rule_version", kwargs))
|
|
self.calls.append(("create_rule_version", kwargs))
|
|
@@ -105,6 +126,35 @@ class FakeRuleRepository:
|
|
|
"spec_hash": "b" * 64,
|
|
"spec_hash": "b" * 64,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ def search_published_assets(self, **kwargs):
|
|
|
|
|
+ self.calls.append(("search_published_assets", kwargs))
|
|
|
|
|
+ return {
|
|
|
|
|
+ "items": self.catalog_items,
|
|
|
|
|
+ "total": 1,
|
|
|
|
|
+ "limit": kwargs["limit"],
|
|
|
|
|
+ "offset": kwargs["offset"],
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def get_asset_evidence(self, **kwargs):
|
|
|
|
|
+ self.calls.append(("get_asset_evidence", kwargs))
|
|
|
|
|
+ return {
|
|
|
|
|
+ "asset_type": kwargs["asset_type"],
|
|
|
|
|
+ "version_id": kwargs["version_id"],
|
|
|
|
|
+ "stages": {
|
|
|
|
|
+ "generation": {"status": "ready"},
|
|
|
|
|
+ "logical_compile": {"status": "success"},
|
|
|
|
|
+ "dry_run": {"status": "success"},
|
|
|
|
|
+ "publication": {"status": "published"},
|
|
|
|
|
+ "physical": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "backend": "polars_batch",
|
|
|
|
|
+ "compile": {"status": "success"},
|
|
|
|
|
+ "test": {"status": "success"},
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class FakeReleaseService:
|
|
class FakeReleaseService:
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
@@ -146,7 +196,10 @@ class FakePublicationService:
|
|
|
|
|
|
|
|
def validate(self, version_id, actor_uid):
|
|
def validate(self, version_id, actor_uid):
|
|
|
self.repository.calls.append(
|
|
self.repository.calls.append(
|
|
|
- ("validate_rule_version", {"version_id": version_id, "actor_uid": actor_uid})
|
|
|
|
|
|
|
+ (
|
|
|
|
|
+ "validate_rule_version",
|
|
|
|
|
+ {"version_id": version_id, "actor_uid": actor_uid},
|
|
|
|
|
+ )
|
|
|
)
|
|
)
|
|
|
return {
|
|
return {
|
|
|
"version_id": version_id,
|
|
"version_id": version_id,
|
|
@@ -177,7 +230,10 @@ class FakePublicationService:
|
|
|
|
|
|
|
|
def publish(self, version_id, actor_uid):
|
|
def publish(self, version_id, actor_uid):
|
|
|
self.repository.calls.append(
|
|
self.repository.calls.append(
|
|
|
- ("publish_rule_version", {"version_id": version_id, "actor_uid": actor_uid})
|
|
|
|
|
|
|
+ (
|
|
|
|
|
+ "publish_rule_version",
|
|
|
|
|
+ {"version_id": version_id, "actor_uid": actor_uid},
|
|
|
|
|
+ )
|
|
|
)
|
|
)
|
|
|
return {
|
|
return {
|
|
|
"id": version_id,
|
|
"id": version_id,
|
|
@@ -237,7 +293,9 @@ class SnapshotOnlyRepository:
|
|
|
|
|
|
|
|
def persist_schema_snapshot(self, *, snapshot):
|
|
def persist_schema_snapshot(self, *, snapshot):
|
|
|
value = {"id": new_governance_uid(), **snapshot}
|
|
value = {"id": new_governance_uid(), **snapshot}
|
|
|
- self.snapshots[(snapshot["schema_ref"], snapshot["schema_hash"])] = value
|
|
|
|
|
|
|
+ self.snapshots[(snapshot["schema_ref"], snapshot["schema_hash"])] = (
|
|
|
|
|
+ value
|
|
|
|
|
+ )
|
|
|
return value
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
@@ -268,7 +326,9 @@ def _use_token_identity(monkeypatch):
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_rule_capabilities_and_validation_are_registered_and_governed(monkeypatch):
|
|
|
|
|
|
|
+def test_rule_capabilities_and_validation_are_registered_and_governed(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|
|
@@ -279,7 +339,9 @@ def test_rule_capabilities_and_validation_are_registered_and_governed(monkeypatc
|
|
|
)
|
|
)
|
|
|
client = app.test_client()
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
- response = client.get("/api/rules/capabilities", headers=_headers(app, "viewer"))
|
|
|
|
|
|
|
+ response = client.get(
|
|
|
|
|
+ "/api/rules/capabilities", headers=_headers(app, "viewer")
|
|
|
|
|
+ )
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
capabilities = response.get_json()["data"]
|
|
capabilities = response.get_json()["data"]
|
|
|
assert capabilities["natural_language_authoring"] is True
|
|
assert capabilities["natural_language_authoring"] is True
|
|
@@ -307,7 +369,9 @@ def test_rule_capabilities_and_validation_are_registered_and_governed(monkeypatc
|
|
|
assert forbidden.status_code == 403
|
|
assert forbidden.status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_rule_interpret_uses_configured_agent_and_preserves_surface(monkeypatch):
|
|
|
|
|
|
|
+def test_rule_interpret_uses_configured_agent_and_preserves_surface(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|
|
@@ -330,9 +394,7 @@ def test_rule_interpret_uses_configured_agent_and_preserves_surface(monkeypatch)
|
|
|
"context": {
|
|
"context": {
|
|
|
"input_schema_snapshot_id": new_governance_uid(),
|
|
"input_schema_snapshot_id": new_governance_uid(),
|
|
|
"output_schema_snapshot_id": new_governance_uid(),
|
|
"output_schema_snapshot_id": new_governance_uid(),
|
|
|
- "input_sample_artifact_ref": (
|
|
|
|
|
- "minio://trusted/input.parquet"
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ "input_sample_artifact_ref": ("minio://trusted/input.parquet"),
|
|
|
"golden_output_artifact_ref": None,
|
|
"golden_output_artifact_ref": None,
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
@@ -348,7 +410,9 @@ def test_rule_interpret_uses_configured_agent_and_preserves_surface(monkeypatch)
|
|
|
assert repository.calls[1][0] == "record_generation_run"
|
|
assert repository.calls[1][0] == "record_generation_run"
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_rule_interpret_preflights_receipt_signer_before_model_call(monkeypatch):
|
|
|
|
|
|
|
+def test_rule_interpret_preflights_receipt_signer_before_model_call(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|
|
@@ -369,9 +433,7 @@ def test_rule_interpret_preflights_receipt_signer_before_model_call(monkeypatch)
|
|
|
"context": {
|
|
"context": {
|
|
|
"input_schema_snapshot_id": new_governance_uid(),
|
|
"input_schema_snapshot_id": new_governance_uid(),
|
|
|
"output_schema_snapshot_id": new_governance_uid(),
|
|
"output_schema_snapshot_id": new_governance_uid(),
|
|
|
- "input_sample_artifact_ref": (
|
|
|
|
|
- "minio://trusted/input.parquet"
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ "input_sample_artifact_ref": ("minio://trusted/input.parquet"),
|
|
|
"golden_output_artifact_ref": None,
|
|
"golden_output_artifact_ref": None,
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
@@ -430,14 +492,12 @@ def test_published_rule_catalog_uses_canonical_closed_contract(monkeypatch):
|
|
|
_use_token_identity(monkeypatch)
|
|
_use_token_identity(monkeypatch)
|
|
|
app.config["TESTING"] = True
|
|
app.config["TESTING"] = True
|
|
|
repository = FakeRuleRepository()
|
|
repository = FakeRuleRepository()
|
|
|
- app.extensions["rule_publication_service"] = FakePublicationService(
|
|
|
|
|
- repository
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ app.extensions["data_rule_repository"] = repository
|
|
|
client = app.test_client()
|
|
client = app.test_client()
|
|
|
headers = _headers(app, "viewer")
|
|
headers = _headers(app, "viewer")
|
|
|
|
|
|
|
|
response = client.get(
|
|
response = client.get(
|
|
|
- "/api/rules/catalog?query=mobile&limit=10",
|
|
|
|
|
|
|
+ "/api/rules/catalog?query=mobile&asset_type=rule&limit=10&offset=20",
|
|
|
headers=headers,
|
|
headers=headers,
|
|
|
)
|
|
)
|
|
|
rejected = client.get(
|
|
rejected = client.get(
|
|
@@ -446,11 +506,133 @@ def test_published_rule_catalog_uses_canonical_closed_contract(monkeypatch):
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
assert response.status_code == 200
|
|
|
- assert response.get_json()["data"] == {"items": []}
|
|
|
|
|
|
|
+ assert response.get_json()["data"] == {
|
|
|
|
|
+ "items": repository.catalog_items,
|
|
|
|
|
+ "total": 1,
|
|
|
|
|
+ "limit": 10,
|
|
|
|
|
+ "offset": 20,
|
|
|
|
|
+ }
|
|
|
|
|
+ assert repository.calls[-1] == (
|
|
|
|
|
+ "search_published_assets",
|
|
|
|
|
+ {
|
|
|
|
|
+ "query": "mobile",
|
|
|
|
|
+ "asset_type": "rule",
|
|
|
|
|
+ "limit": 10,
|
|
|
|
|
+ "offset": 20,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
assert rejected.status_code == 400
|
|
assert rejected.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_production_line_resolve_preview_expands_standard_without_writing(monkeypatch):
|
|
|
|
|
|
|
+def test_unified_catalog_defaults_to_all_assets_and_preserves_rule_alias(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
|
|
+ from app import create_app
|
|
|
|
|
+
|
|
|
|
|
+ app = create_app()
|
|
|
|
|
+ _use_token_identity(monkeypatch)
|
|
|
|
|
+ app.config["TESTING"] = True
|
|
|
|
|
+ repository = FakeRuleRepository()
|
|
|
|
|
+ app.extensions["data_rule_repository"] = repository
|
|
|
|
|
+ client = app.test_client()
|
|
|
|
|
+ headers = _headers(app, "viewer")
|
|
|
|
|
+
|
|
|
|
|
+ canonical = client.get("/api/rules/catalog", headers=headers)
|
|
|
|
|
+ alias = client.get("/api/rules/catalog/rule-versions", headers=headers)
|
|
|
|
|
+
|
|
|
|
|
+ assert canonical.status_code == 200
|
|
|
|
|
+ assert repository.calls[0] == (
|
|
|
|
|
+ "search_published_assets",
|
|
|
|
|
+ {"query": "", "asset_type": None, "limit": 50, "offset": 0},
|
|
|
|
|
+ )
|
|
|
|
|
+ assert alias.status_code == 200
|
|
|
|
|
+ assert repository.calls[1][1]["asset_type"] == "rule"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_catalog_and_evidence_queries_are_closed_bounded_and_rules_read_only(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
|
|
+ from app import create_app
|
|
|
|
|
+
|
|
|
|
|
+ app = create_app()
|
|
|
|
|
+ _use_token_identity(monkeypatch)
|
|
|
|
|
+ app.config["TESTING"] = True
|
|
|
|
|
+ repository = FakeRuleRepository()
|
|
|
|
|
+ app.extensions["data_rule_repository"] = repository
|
|
|
|
|
+ client = app.test_client()
|
|
|
|
|
+ viewer = _headers(app, "viewer")
|
|
|
|
|
+ version_id = new_governance_uid()
|
|
|
|
|
+
|
|
|
|
|
+ evidence = client.get(
|
|
|
|
|
+ f"/api/rules/catalog/assets/rule/{version_id}/evidence",
|
|
|
|
|
+ headers=viewer,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert evidence.status_code == 200
|
|
|
|
|
+ value = evidence.get_json()["data"]
|
|
|
|
|
+ assert set(value["stages"]) == {
|
|
|
|
|
+ "generation",
|
|
|
|
|
+ "logical_compile",
|
|
|
|
|
+ "dry_run",
|
|
|
|
|
+ "publication",
|
|
|
|
|
+ "physical",
|
|
|
|
|
+ }
|
|
|
|
|
+ assert "source_text" not in str(value)
|
|
|
|
|
+ assert "sample" not in str(value)
|
|
|
|
|
+ assert repository.calls[-1] == (
|
|
|
|
|
+ "get_asset_evidence",
|
|
|
|
|
+ {"asset_type": "rule", "version_id": version_id},
|
|
|
|
|
+ )
|
|
|
|
|
+ assert (
|
|
|
|
|
+ client.get(
|
|
|
|
|
+ "/api/rules/catalog?asset_type=dataflow", headers=viewer
|
|
|
|
|
+ ).status_code
|
|
|
|
|
+ == 400
|
|
|
|
|
+ )
|
|
|
|
|
+ assert (
|
|
|
|
|
+ client.get("/api/rules/catalog?limit=101", headers=viewer).status_code
|
|
|
|
|
+ == 400
|
|
|
|
|
+ )
|
|
|
|
|
+ assert (
|
|
|
|
|
+ client.get("/api/rules/catalog?offset=-1", headers=viewer).status_code
|
|
|
|
|
+ == 400
|
|
|
|
|
+ )
|
|
|
|
|
+ assert (
|
|
|
|
|
+ client.get(
|
|
|
|
|
+ f"/api/rules/catalog/assets/rule/{version_id}/evidence"
|
|
|
|
|
+ ).status_code
|
|
|
|
|
+ == 401
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_legacy_rule_evidence_path_uses_same_safe_repository_contract(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
|
|
+ from app import create_app
|
|
|
|
|
+
|
|
|
|
|
+ app = create_app()
|
|
|
|
|
+ _use_token_identity(monkeypatch)
|
|
|
|
|
+ app.config["TESTING"] = True
|
|
|
|
|
+ repository = FakeRuleRepository()
|
|
|
|
|
+ app.extensions["data_rule_repository"] = repository
|
|
|
|
|
+ client = app.test_client()
|
|
|
|
|
+ version_id = new_governance_uid()
|
|
|
|
|
+
|
|
|
|
|
+ response = client.get(
|
|
|
|
|
+ f"/api/rules/rule-versions/{version_id}/evidence",
|
|
|
|
|
+ headers=_headers(app, "viewer"),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ assert repository.calls[-1] == (
|
|
|
|
|
+ "get_asset_evidence",
|
|
|
|
|
+ {"asset_type": "rule", "version_id": version_id},
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_production_line_resolve_preview_expands_standard_without_writing(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|
|
@@ -602,7 +784,9 @@ def test_create_version_rejects_client_selected_lifecycle_status(monkeypatch):
|
|
|
app.config["TESTING"] = True
|
|
app.config["TESTING"] = True
|
|
|
repository = FakeRuleRepository()
|
|
repository = FakeRuleRepository()
|
|
|
app.extensions["data_rule_repository"] = repository
|
|
app.extensions["data_rule_repository"] = repository
|
|
|
- app.extensions["rule_publication_service"] = FakePublicationService(repository)
|
|
|
|
|
|
|
+ app.extensions["rule_publication_service"] = FakePublicationService(
|
|
|
|
|
+ repository
|
|
|
|
|
+ )
|
|
|
client = app.test_client()
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
response = client.post(
|
|
response = client.post(
|
|
@@ -626,8 +810,9 @@ def test_generation_receipt_signer_requires_dedicated_secret(monkeypatch):
|
|
|
monkeypatch.delenv("RULE_GENERATION_RECEIPT_SECRET", raising=False)
|
|
monkeypatch.delenv("RULE_GENERATION_RECEIPT_SECRET", raising=False)
|
|
|
app = create_app()
|
|
app = create_app()
|
|
|
app.config["RULE_GENERATION_RECEIPT_SECRET"] = None
|
|
app.config["RULE_GENERATION_RECEIPT_SECRET"] = None
|
|
|
- with app.app_context(), pytest.raises(
|
|
|
|
|
- RuntimeError, match="receipt secret"
|
|
|
|
|
|
|
+ with (
|
|
|
|
|
+ app.app_context(),
|
|
|
|
|
+ pytest.raises(RuntimeError, match="receipt secret"),
|
|
|
):
|
|
):
|
|
|
_receipt_signer()
|
|
_receipt_signer()
|
|
|
|
|
|
|
@@ -639,14 +824,18 @@ def test_generation_receipt_signer_requires_dedicated_secret(monkeypatch):
|
|
|
assert signer is not None
|
|
assert signer is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_rule_gates_reject_caller_supplied_compile_or_test_evidence(monkeypatch):
|
|
|
|
|
|
|
+def test_rule_gates_reject_caller_supplied_compile_or_test_evidence(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|
|
|
_use_token_identity(monkeypatch)
|
|
_use_token_identity(monkeypatch)
|
|
|
app.config["TESTING"] = True
|
|
app.config["TESTING"] = True
|
|
|
repository = FakeRuleRepository()
|
|
repository = FakeRuleRepository()
|
|
|
- app.extensions["rule_publication_service"] = FakePublicationService(repository)
|
|
|
|
|
|
|
+ app.extensions["rule_publication_service"] = FakePublicationService(
|
|
|
|
|
+ repository
|
|
|
|
|
+ )
|
|
|
client = app.test_client()
|
|
client = app.test_client()
|
|
|
version_id = new_governance_uid()
|
|
version_id = new_governance_uid()
|
|
|
|
|
|
|
@@ -669,7 +858,9 @@ def test_rule_gates_reject_caller_supplied_compile_or_test_evidence(monkeypatch)
|
|
|
assert repository.calls == []
|
|
assert repository.calls == []
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_create_rule_version_rejects_legacy_v1_payload_before_repository(monkeypatch):
|
|
|
|
|
|
|
+def test_create_rule_version_rejects_legacy_v1_payload_before_repository(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|
|
@@ -691,7 +882,9 @@ def test_create_rule_version_rejects_legacy_v1_payload_before_repository(monkeyp
|
|
|
assert repository.calls == []
|
|
assert repository.calls == []
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_dataflow_release_uses_server_assets_and_release_permission(monkeypatch):
|
|
|
|
|
|
|
+def test_dataflow_release_uses_server_assets_and_release_permission(
|
|
|
|
|
+ monkeypatch,
|
|
|
|
|
+):
|
|
|
from app import create_app
|
|
from app import create_app
|
|
|
|
|
|
|
|
app = create_app()
|
|
app = create_app()
|