from __future__ import annotations import copy import inspect from pathlib import Path from types import SimpleNamespace import pytest from server.agent_core import harness from server.aps_domain import sap_sync from server.integrations.sap_stub import reset_sap_client class _MemStore: def __init__(self, data: dict): self.data = data self.saved = 0 def next_id(self, kind: str) -> int: key = f"_counter_{kind}" self.data[key] = int(self.data.get(key) or 0) + 1 return self.data[key] def save(self) -> None: self.saved += 1 class _NoPushClient: def __init__(self) -> None: self.push_calls = 0 def status(self) -> dict: return {"system": "MOCK-SAP", "plant": "TEST", "connected": True} def push_receipt(self, *_args, **_kwargs): self.push_calls += 1 raise AssertionError("preview/stage must not call push_receipt") def _outbound_world(candidate_count: int = 51) -> dict: orders = [] work_orders = [] for index in range(candidate_count): order_no = f"FO-{index:03d}" orders.append( { "id": index + 1, "orderNo": order_no, "externalAufnr": f"AUF-{index:03d}", "quantity": index + 10, } ) work_orders.append( { "id": f"WO-{index:03d}", "versionId": 7, "orderNo": order_no, "flexOrderNo": order_no, "operationCode": f"OP-{index:03d}", "equipmentCode": f"EQ-{index % 3}", "plannedStartTime": f"2026-08-{(index % 28) + 1:02d} 08:00:00", "plannedEndTime": f"2026-08-{(index % 28) + 1:02d} 09:00:00", "frozen": False, } ) return { "scheduleVersions": [ {"id": 7, "versionNo": "SV-0007", "status": "PUBLISHED"} ], "flexOrders": orders, "flexWorkOrders": work_orders, "sapLinks": [], "sapSyncJournal": [], "auditEvents": [], } def test_outbound_projection_is_pure_deterministic_and_detects_51st_candidate_drift( monkeypatch, ): builder = getattr(sap_sync, "build_outbound_projection", None) assert callable(builder), "Round 68 must expose build_outbound_projection(world, *, limit=50)" monkeypatch.setattr( sap_sync, "get_sap_client", lambda: (_ for _ in ()).throw(AssertionError("projection must not touch SAP client")), ) world = _outbound_world(51) before = copy.deepcopy(world) first = builder(world, limit=50) assert world == before reordered = copy.deepcopy(world) reordered["flexOrders"].reverse() reordered["flexWorkOrders"].reverse() second = builder(reordered, limit=50) for key in ( "items", "idemKeys", "payloadDigest", "worldFingerprint", "fullCandidateDigest", "totalCount", "limit", ): assert first[key] == second[key] assert first["totalCount"] == 51 assert first["limit"] == 50 assert len(first["items"]) == 50 selected_ids = {item["woId"] for item in first["items"]} hidden = next(wo for wo in world["flexWorkOrders"] if wo["id"] not in selected_ids) drifted = copy.deepcopy(world) next(wo for wo in drifted["flexWorkOrders"] if wo["id"] == hidden["id"])[ "equipmentCode" ] = "EQ-DRIFT" changed = builder(drifted, limit=50) assert changed["payloadDigest"] == first["payloadDigest"] assert changed["fullCandidateDigest"] != first["fullCandidateDigest"] assert changed["worldFingerprint"] != first["worldFingerprint"] def test_apply_outbound_requires_complete_keyword_only_p3_envelope(tmp_path: Path): required = { "actor", "confirm_id", "execution_grant", "approval_params", "requester", "approvals", "before_snapshot", "checkpoint_store", } signature = inspect.signature(sap_sync.apply_outbound) missing = required - set(signature.parameters) assert not missing, f"apply_outbound missing required P3 envelope fields: {sorted(missing)}" for name in required: parameter = signature.parameters[name] assert parameter.kind is inspect.Parameter.KEYWORD_ONLY assert parameter.default is inspect.Parameter.empty reset_sap_client(tmp_path / "sap-mirror.json") with pytest.raises(TypeError): sap_sync.apply_outbound(_MemStore(_outbound_world(1))) def test_outbound_stage_freezes_checkpoint_and_canonical_p3_params_without_push( monkeypatch, ): client = _NoPushClient() store = _MemStore(_outbound_world(2)) captured: dict = {} class _CheckpointStore: def create(self, world, **kwargs): captured["checkpoint_world"] = copy.deepcopy(world) captured["checkpoint_kwargs"] = kwargs return {"pairId": "pair-stage-001"} def fake_stage(session_id, action, params, title, summary_lines, **kwargs): captured.update( session_id=session_id, action=action, params=copy.deepcopy(params), title=title, summary_lines=list(summary_lines), stage_kwargs=copy.deepcopy(kwargs), ) return SimpleNamespace(props={"confirmId": "confirm-stage-001"}) monkeypatch.setattr(sap_sync, "get_sap_client", lambda: client) monkeypatch.setattr("server.state.checkpoints.get_checkpoints", lambda: _CheckpointStore()) monkeypatch.setattr(harness, "stage_confirmation", fake_stage) monkeypatch.setattr("server.agent_core.audit.write_audit", lambda *_a, **_k: None) result = sap_sync.stage_sap_sync( store, "outbound", session_id="session-r68", actor="requester" ) assert result["staged"] is True assert client.push_calls == 0 assert harness.power_of("sap.sync.outbound") == "P3" assert captured["action"] == "sap.sync.outbound" params = captured["params"] assert params["direction"] == "outbound" assert params["beforeSnapshot"] == "pair-stage-001" for key in ( "versionId", "versionNo", "items", "idemKeys", "payloadDigest", "worldFingerprint", "fullCandidateDigest", "totalCount", "limit", "evidenceRefs", ): assert key in params assert captured["stage_kwargs"]["before_snapshot"] == "pair-stage-001" assert captured["stage_kwargs"]["evidence_refs"] == params["evidenceRefs"] class _MemoryCheckpointStore: def __init__(self) -> None: self.records: dict[str, dict] = {} def create(self, world: dict, **metadata) -> dict: pair_id = f"pair-r68-{len(self.records) + 1:03d}" record = { "pairId": pair_id, "world": copy.deepcopy(world), **copy.deepcopy(metadata), } self.records[pair_id] = record return copy.deepcopy(record) def get(self, pair_id: str) -> dict | None: record = self.records.get(str(pair_id)) return copy.deepcopy(record) if record is not None else None class _ScopedMemStore(_MemStore): def __init__(self, data: dict): super().__init__(data) self.world_key = "personal-1001" self.tenant_uuid = "tenant-r68" class _FailingOnceSapClient: def __init__(self, backing, *, fail_on_call: int): self.backing = backing self.fail_on_call = fail_on_call self.push_calls = 0 def status(self) -> dict: return self.backing.status() def push_receipt(self, payload: dict, idem_key: str) -> dict: self.push_calls += 1 if self.push_calls == self.fail_on_call: raise RuntimeError(f"synthetic SAP failure on call {self.push_calls}") return self.backing.push_receipt(payload, idem_key) def _as_user(user_id: int, username: str, callback): from server.auth.context import IdentityContext, bind_identity, reset_identity token = bind_identity( IdentityContext( user_id, username, username, "tenant-r68", roles=("planner",), ) ) try: return callback() finally: reset_identity(token) @pytest.fixture() def _isolated_p3_runtime(tmp_path: Path, monkeypatch): import server.aps_domain.workflow as workflow_module class _ProjectStore: def active_world_key(self) -> str: return "default" original_approval_store = harness._approval_store harness.configure_approval_store(path=str(tmp_path / "approvals.json")) checkpoints = _MemoryCheckpointStore() client = reset_sap_client(tmp_path / "sap-mirror.json") monkeypatch.setattr("server.state.checkpoints.get_checkpoints", lambda: checkpoints) monkeypatch.setattr(workflow_module, "get_checkpoints", lambda: checkpoints) monkeypatch.setattr("server.state.projects.get_project_store", lambda: _ProjectStore()) monkeypatch.setattr(harness, "_capture_world_fingerprint", lambda *_args: None) monkeypatch.setattr( "server.agent_core.plan_orchestration.stage_plan_node", lambda **_kwargs: None, ) monkeypatch.setattr( "server.agent_core.plan_orchestration.decide_plan_node", lambda **_kwargs: None, ) monkeypatch.setattr(sap_sync, "get_sap_client", lambda: client) try: yield SimpleNamespace(checkpoints=checkpoints, client=client, tmp_path=tmp_path) finally: harness.configure_approval_store(store=original_approval_store) def _stage_outbound(store: _ScopedMemStore) -> tuple[str, dict]: staged = _as_user( 1001, "requester", lambda: sap_sync.stage_sap_sync( store, "outbound", session_id="session-r68", actor="requester", ), ) assert staged["staged"] is True confirm_id = str(staged["block"].props["confirmId"]) pending = copy.deepcopy(harness._pending[confirm_id]) return confirm_id, pending def _approve_twice(confirm_id: str) -> dict: first = _as_user( 1001, "requester", lambda: harness.take_confirmation(confirm_id, approve=True), ) assert first is not None assert first["needsSecondConfirm"] is True assert first["approvalStep"] == 1 final = _as_user( 2002, "approver-2", lambda: harness.take_confirmation(confirm_id, approve=True), ) assert final is not None assert final["needsSecondConfirm"] is False assert final["approvalStep"] == 2 assert final.get("executionGrant") return final def _apply_decision( store: _ScopedMemStore, checkpoints: _MemoryCheckpointStore, confirm_id: str, decision: dict, *, approval_params: dict | None = None, before_snapshot: str | None = None, ): params = copy.deepcopy(approval_params or decision["params"]) snapshot = str(before_snapshot or decision["beforeSnapshot"]) return _as_user( 2002, "approver-2", lambda: sap_sync.apply_outbound( store, actor="approver-2", confirm_id=confirm_id, execution_grant=str(decision["executionGrant"]), approval_params=params, requester=copy.deepcopy(decision["requester"]), approvals=copy.deepcopy(decision["approvals"]), before_snapshot=snapshot, checkpoint_store=checkpoints, ), ) def test_real_p3_workflow_pushes_only_after_two_distinct_approvers( _isolated_p3_runtime, ): from server.aps_domain.workflow import execute_confirmed store = _ScopedMemStore(_outbound_world(2)) confirm_id, _pending = _stage_outbound(store) client = _isolated_p3_runtime.client first = _as_user( 1001, "requester", lambda: execute_confirmed( store, confirm_id, approve=True, actor="requester", ), ) assert "第一重确认" in first assert client.status()["receiptCount"] == 0 assert _as_user( 1001, "requester", lambda: harness.is_confirmation_pending(confirm_id), ) is True same_user = _as_user( 1001, "requester", lambda: execute_confirmed( store, confirm_id, approve=True, actor="requester", ), ) assert "另一名用户" in same_user assert client.status()["receiptCount"] == 0 assert _as_user( 1001, "requester", lambda: harness.is_confirmation_pending(confirm_id), ) is True final = _as_user( 2002, "approver-2", lambda: execute_confirmed( store, confirm_id, approve=True, actor="approver-2", ), ) assert "SAP 出站完成" in final assert client.status()["receiptCount"] == 2 assert _as_user( 2002, "approver-2", lambda: harness.is_confirmation_pending(confirm_id), ) is False execution = store.data["sapOutboundExecutions"][-1] assert execution["status"] == "SUCCEEDED" assert [approval["userId"] for approval in execution["approvals"]] == [ "1001", "2002", ] def test_51st_candidate_drift_fails_closed_without_consuming_grant( _isolated_p3_runtime, ): store = _ScopedMemStore(_outbound_world(51)) confirm_id, pending = _stage_outbound(store) decision = _approve_twice(confirm_id) approved_ids = {item["woId"] for item in pending["params"]["items"]} hidden = next( work_order for work_order in store.data["flexWorkOrders"] if work_order["id"] not in approved_ids ) original_equipment = hidden["equipmentCode"] hidden["equipmentCode"] = "EQ-51ST-DRIFT" with pytest.raises(PermissionError, match="漂移"): _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 assert store.data.get("sapOutboundExecutions") in (None, []) hidden["equipmentCode"] = original_equipment recovered = _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) assert recovered["execution"]["status"] == "SUCCEEDED" assert _isolated_p3_runtime.client.status()["receiptCount"] == 50 @pytest.mark.parametrize("substitution", ["params", "evidence", "snapshot"]) def test_p3_envelope_substitution_is_rejected_before_push( _isolated_p3_runtime, substitution: str, ): store = _ScopedMemStore(_outbound_world(2)) confirm_id, _pending = _stage_outbound(store) decision = _approve_twice(confirm_id) substituted = copy.deepcopy(decision["params"]) before_snapshot = str(decision["beforeSnapshot"]) if substitution == "params": store.data["scheduleVersions"][-1]["status"] = "DISPATCHED" current = sap_sync.build_outbound_projection(store.data, limit=50) replacement_checkpoint = _isolated_p3_runtime.checkpoints.create( store.data, label="substituted params", reason="test", ) substituted = {**current, "beforeSnapshot": replacement_checkpoint["pairId"]} before_snapshot = str(replacement_checkpoint["pairId"]) elif substitution == "evidence": substituted["evidenceRefs"] = [ substituted["evidenceRefs"][0], "sap-outbound-payload:substituted", substituted["evidenceRefs"][2], ] else: replacement_checkpoint = _isolated_p3_runtime.checkpoints.create( store.data, label="substituted snapshot", reason="test", ) substituted["beforeSnapshot"] = replacement_checkpoint["pairId"] before_snapshot = str(replacement_checkpoint["pairId"]) with pytest.raises(PermissionError): _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, approval_params=substituted, before_snapshot=before_snapshot, ) assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 def test_action_substituted_execution_grant_is_rejected_before_push( _isolated_p3_runtime, ): store = _ScopedMemStore(_outbound_world(2)) _outbound_confirm_id, pending = _stage_outbound(store) mes_block = _as_user( 1001, "requester", lambda: harness.stage_confirmation( "session-r68-action-substitution", "mes.dispatch", copy.deepcopy(pending["params"]), title="action substitution", summary_lines=["must not authorize SAP outbound"], evidence_refs=copy.deepcopy(pending["evidenceRefs"]), before_snapshot=str(pending["beforeSnapshot"]), ), ) mes_confirm_id = str(mes_block.props["confirmId"]) mes_decision = _approve_twice(mes_confirm_id) with pytest.raises(PermissionError, match="最终批准凭据"): _apply_decision( store, _isolated_p3_runtime.checkpoints, mes_confirm_id, mes_decision, ) assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 assert store.data["sapOutboundExecutions"][-1]["status"] == "DENIED" def test_successful_outbound_execution_replay_is_rejected_without_second_push( _isolated_p3_runtime, ): store = _ScopedMemStore(_outbound_world(2)) confirm_id, _pending = _stage_outbound(store) decision = _approve_twice(confirm_id) first = _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) assert first["execution"]["status"] == "SUCCEEDED" assert _isolated_p3_runtime.client.status()["receiptCount"] == 2 with pytest.raises(PermissionError, match="重放"): _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) assert _isolated_p3_runtime.client.status()["receiptCount"] == 2 assert len(store.data["sapOutboundExecutions"]) == 1 def test_partial_failure_persists_item_states_and_new_confirmation_recovery_chain( _isolated_p3_runtime, monkeypatch, ): store = _ScopedMemStore(_outbound_world(3)) confirm_id, _pending = _stage_outbound(store) decision = _approve_twice(confirm_id) backing = _isolated_p3_runtime.client failing = _FailingOnceSapClient(backing, fail_on_call=2) monkeypatch.setattr(sap_sync, "get_sap_client", lambda: failing) with pytest.raises(RuntimeError, match="synthetic SAP failure"): _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) failed = store.data["sapOutboundExecutions"][-1] assert failed["status"] == "PARTIAL_FAILED" assert [item["status"] for item in failed["items"]] == [ "PUSHED", "FAILED", "PENDING", ] assert backing.status()["receiptCount"] == 1 assert store.data["auditEvents"][-1]["result"] == "FAILED" monkeypatch.setattr(sap_sync, "get_sap_client", lambda: backing) recovery_confirm_id, _recovery_pending = _stage_outbound(store) recovery_decision = _approve_twice(recovery_confirm_id) recovered = _apply_decision( store, _isolated_p3_runtime.checkpoints, recovery_confirm_id, recovery_decision, ) recovery = recovered["execution"] assert recovery["previousExecutionId"] == failed["id"] assert recovery["status"] == "SUCCEEDED" assert [item["status"] for item in recovery["items"]] == [ "DUPLICATE", "PUSHED", "PUSHED", ] assert backing.status()["receiptCount"] == 3 def test_preview_and_stage_use_temp_mirror_without_push( _isolated_p3_runtime, ): store = _ScopedMemStore(_outbound_world(2)) client = _isolated_p3_runtime.client assert client.path.parent == _isolated_p3_runtime.tmp_path assert client.status()["receiptCount"] == 0 preview = sap_sync.preview_outbound(store.data) assert preview["selectedCount"] == 2 assert client.status()["receiptCount"] == 0 confirm_id, _pending = _stage_outbound(store) assert confirm_id assert client.status()["receiptCount"] == 0 _ROUND68_NATIVE_REQUIRE_CAN_INITIATE = getattr( harness, "require_can_initiate", None ) @pytest.fixture(autouse=True) def _round68_permission_preflight_adapter(monkeypatch): """Keep deeper contract tests runnable while a missing public preflight stays visible.""" if callable(_ROUND68_NATIVE_REQUIRE_CAN_INITIATE): yield return def require_can_initiate(action: str) -> None: power = harness.power_of(action) if not harness._can_initiate_current(action, power): raise PermissionError("当前身份没有发起审批的角色权限") monkeypatch.setattr( harness, "require_can_initiate", require_can_initiate, raising=False, ) yield def _as_identity( user_id: int, username: str, roles: tuple[str, ...], callback, ): from server.auth.context import IdentityContext, bind_identity, reset_identity token = bind_identity( IdentityContext( user_id, username, username, "tenant-r68", roles=roles, ) ) try: return callback() finally: reset_identity(token) class _AuditedCheckpointStore(_MemoryCheckpointStore): def __init__(self) -> None: super().__init__() self.created: list[str] = [] self.deleted: list[str] = [] def create(self, world: dict, **metadata) -> dict: record = super().create(world, **metadata) self.created.append(str(record["pairId"])) return record def delete(self, pair_id: str) -> bool: pair_id = str(pair_id) existed = pair_id in self.records self.records.pop(pair_id, None) self.deleted.append(pair_id) return existed class _ReadyWorldDriftStore(_ScopedMemStore): def __init__(self, data: dict, drift_kind: str): super().__init__(data) self.drift_kind = drift_kind self.drifted = False def save(self) -> None: super().save() executions = self.data.get("sapOutboundExecutions") or [] if self.drifted or not executions or executions[-1].get("status") != "READY": return if self.drift_kind == "qty": self.data["flexOrders"][0]["quantity"] += 1000 elif self.drift_kind == "aufnr": self.data["flexOrders"][0]["externalAufnr"] = "AUF-READY-DRIFT" else: self.data["flexWorkOrders"][0]["operationCode"] = "OP-READY-DRIFT" self.drifted = True class _RemoteAcceptedSaveFailureStore(_ScopedMemStore): def __init__(self, data: dict): super().__init__(data) self.failed_after_remote_accept = False def save(self) -> None: self.saved += 1 if self.failed_after_remote_accept: return executions = self.data.get("sapOutboundExecutions") or [] if not executions: return states = [item.get("status") for item in executions[-1].get("items") or []] if self.data.get("sapLinks") and any( status in {"PUSHED", "DUPLICATE"} for status in states ): self.failed_after_remote_accept = True raise OSError("synthetic local commit failure after SAP accepted receipt") @pytest.mark.parametrize("field", ["orderNo", "flexOrderNo"]) def test_projection_fingerprint_captures_raw_and_flex_order_number_drift(field: str): world = _outbound_world(1) baseline = sap_sync.build_outbound_projection(world, limit=50) drifted = copy.deepcopy(world) drifted["flexWorkOrders"][0][field] = f"{field}-DRIFT" changed = sap_sync.build_outbound_projection(drifted, limit=50) assert changed["fullCandidateDigest"] != baseline["fullCandidateDigest"] assert changed["worldFingerprint"] != baseline["worldFingerprint"] if field == "orderNo": assert changed["payloadDigest"] == baseline["payloadDigest"] else: assert changed["payloadDigest"] != baseline["payloadDigest"] @pytest.mark.parametrize("drift_kind", ["qty", "aufnr", "work_order"]) def test_ready_boundary_world_drift_is_rejected_before_grant_consume_and_push( _isolated_p3_runtime, monkeypatch, drift_kind: str, ): store = _ReadyWorldDriftStore(_outbound_world(1), drift_kind) confirm_id, _pending = _stage_outbound(store) decision = _approve_twice(confirm_id) original_consume = harness.consume_execution_grant consume_calls: list[str] = [] def consume_spy(token: str, **kwargs) -> bool: consume_calls.append(token) return original_consume(token, **kwargs) monkeypatch.setattr(harness, "consume_execution_grant", consume_spy) denied: PermissionError | None = None try: _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) except PermissionError as exc: denied = exc assert store.drifted is True assert consume_calls == [], "READY 后漂移必须在 execution grant 消费前拒绝" assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 assert denied is not None def test_remote_accept_then_local_save_failure_requires_reconciliation_state( _isolated_p3_runtime, ): store = _RemoteAcceptedSaveFailureStore(_outbound_world(1)) confirm_id, _pending = _stage_outbound(store) decision = _approve_twice(confirm_id) try: _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) except (OSError, RuntimeError) as exc: assert "本地提交失败" in str(exc) or "local commit failure" in str(exc) assert store.failed_after_remote_accept is True assert _isolated_p3_runtime.client.status()["receiptCount"] == 1 execution = store.data["sapOutboundExecutions"][-1] statuses = {str(execution.get("status"))} statuses.update(str(item.get("status")) for item in execution.get("items") or []) assert "FAILED" not in statuses assert { "REMOTE_ACCEPTED_LOCAL_COMMIT_FAILED", "RECONCILE_REQUIRED", }.issubset(statuses) idem_key = str(execution["items"][0]["idemKey"]) outbound_events = [ event for event in store.data.get("auditEvents") or [] if event.get("action") == "sap.sync.outbound" ] assert outbound_events rationale = outbound_events[-1].get("rationale") or {} accepted = set(rationale.get("pushedIdemKeys") or []) accepted.update(rationale.get("remoteAcceptedIdemKeys") or []) failed = set(rationale.get("failedIdemKeys") or []) if rationale.get("failedIdemKey"): failed.add(str(rationale["failedIdemKey"])) assert idem_key in accepted assert idem_key not in failed def test_unauthorized_outbound_stage_leaves_no_checkpoint_or_world_write(monkeypatch): checkpoints = _AuditedCheckpointStore() client = _NoPushClient() store = _ScopedMemStore(_outbound_world(1)) monkeypatch.setattr( "server.state.checkpoints.get_checkpoints", lambda: checkpoints, ) monkeypatch.setattr(sap_sync, "get_sap_client", lambda: client) if not callable(_ROUND68_NATIVE_REQUIRE_CAN_INITIATE): monkeypatch.delattr(harness, "require_can_initiate", raising=False) with pytest.raises(PermissionError, match="发起审批"): _as_identity( 3003, "viewer", ("viewer",), lambda: sap_sync.stage_sap_sync( store, "outbound", session_id="session-r68-unauthorized", actor="viewer", ), ) assert checkpoints.records == {} assert checkpoints.created == [] assert checkpoints.deleted == [] assert store.saved == 0 assert client.push_calls == 0 @pytest.mark.parametrize("failure_point", ["status", "approval_stage"]) def test_outbound_stage_failure_paths_leave_no_checkpoint_pollution( monkeypatch, failure_point: str, ): checkpoints = _AuditedCheckpointStore() store = _ScopedMemStore(_outbound_world(1)) client = _NoPushClient() monkeypatch.setattr( "server.state.checkpoints.get_checkpoints", lambda: checkpoints, ) monkeypatch.setattr(sap_sync, "get_sap_client", lambda: client) if failure_point == "status": monkeypatch.setattr( client, "status", lambda: (_ for _ in ()).throw(RuntimeError("synthetic status failure")), ) else: monkeypatch.setattr( harness, "stage_confirmation", lambda *_args, **_kwargs: (_ for _ in ()).throw( RuntimeError("synthetic approval stage failure") ), ) with pytest.raises(RuntimeError, match="synthetic"): _as_identity( 1001, "requester", ("planner",), lambda: sap_sync.stage_sap_sync( store, "outbound", session_id=f"session-r68-{failure_point}", actor="requester", ), ) assert checkpoints.records == {} if failure_point == "status": assert checkpoints.created == [] assert checkpoints.deleted == [] else: assert checkpoints.created == checkpoints.deleted assert len(checkpoints.created) == 1 assert store.saved == 0 assert client.push_calls == 0 def test_preview_outbound_preserves_sparse_world_byte_for_byte( _isolated_p3_runtime, ): world = {"auditEvents": [{"id": 1, "action": "existing"}]} before = copy.deepcopy(world) preview = sap_sync.preview_outbound(world) assert world == before assert preview["versionId"] is None assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 def test_success_audit_records_before_snapshot_and_grant_consumption( _isolated_p3_runtime, ): store = _ScopedMemStore(_outbound_world(1)) confirm_id, pending = _stage_outbound(store) decision = _approve_twice(confirm_id) result = _apply_decision( store, _isolated_p3_runtime.checkpoints, confirm_id, decision, ) assert result["execution"]["status"] == "SUCCEEDED" event = next( event for event in reversed(store.data["auditEvents"]) if event.get("action") == "sap.sync.outbound" and event.get("result") == "SUCCESS" ) assert event["beforeSnapshot"] == pending["beforeSnapshot"] assert event["rationale"]["confirmId"] == confirm_id assert event["rationale"].get("grantConsumed") is True def test_http_boundary_pushes_only_after_second_distinct_approver( _isolated_p3_runtime, monkeypatch, ): from fastapi.testclient import TestClient import server.gateway.app as gateway_module from tests.auth_provider import install_test_auth class _WritableProjectStore: def active_world_key(self) -> str: return "default" def require_active_write(self) -> None: return None store = _ScopedMemStore(_outbound_world(2)) checkpoints = _isolated_p3_runtime.checkpoints install_test_auth(monkeypatch, "tenant-r68") monkeypatch.setattr(gateway_module, "get_store", lambda: store) monkeypatch.setattr(gateway_module, "get_checkpoints", lambda: checkpoints) monkeypatch.setattr( "server.state.projects.get_project_store", lambda: _WritableProjectStore(), ) with TestClient(gateway_module.create_app()) as client: login = client.post( "/api/auth/login", json={"username": "planner", "password": "test"}, ) assert login.status_code == 200, login.text preview = client.get("/api/sap/outbound/preview") assert preview.status_code == 200, preview.text assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 staged = client.post( "/api/sap/outbound/stage", json={"sessionId": "session-r68-http"}, ) assert staged.status_code == 200, staged.text confirm_id = str(staged.json()["block"]["props"]["confirmId"]) assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 first = client.post( "/api/actions/confirm", json={ "sessionId": "session-r68-http", "confirmId": confirm_id, "approve": True, }, ) assert first.status_code == 200, first.text assert first.json()["secondConfirmRequired"] is True assert _isolated_p3_runtime.client.status()["receiptCount"] == 0 second_login = client.post( "/api/auth/login", json={"username": "collaborator", "password": "test"}, ) assert second_login.status_code == 200, second_login.text second = client.post( "/api/actions/confirm", json={ "sessionId": "session-r68-http", "confirmId": confirm_id, "approve": True, }, ) assert second.status_code == 200, second.text assert second.json()["secondConfirmRequired"] is False assert _isolated_p3_runtime.client.status()["receiptCount"] == 2