|
43 | 43 | load_reward_memory_experiment_config, |
44 | 44 | resolve_reward_memory_surface_config, |
45 | 45 | ) |
| 46 | +from loopx.control_plane.quota.effect_program import SettlementIdentity |
46 | 47 |
|
47 | 48 |
|
48 | 49 | SURFACE = "agent_workflow.turn_admission" |
@@ -436,6 +437,75 @@ def test_configured_session_scope_is_canonical() -> None: |
436 | 437 | _resolve_session_ref(identity, "session:other") |
437 | 438 |
|
438 | 439 |
|
| 440 | +def bound_quota_decision(*, status: str = "upgraded") -> dict[str, Any]: |
| 441 | + turn = "turn-selected-recall" |
| 442 | + return quota_decision() | { |
| 443 | + "goal_id": "goal", |
| 444 | + "agent_identity": {"agent_id": "pilot"}, |
| 445 | + "heartbeat_receipt": { |
| 446 | + "turn_instance_id": turn, |
| 447 | + "status": status, |
| 448 | + "settlement_identity": SettlementIdentity( |
| 449 | + goal_id="goal", agent_id="pilot", todo_id="todo_123", |
| 450 | + turn_instance_id=turn, |
| 451 | + ).as_dict(), |
| 452 | + }, |
| 453 | + } |
| 454 | + |
| 455 | + |
| 456 | +@pytest.mark.parametrize("status", ["committed", "replayed", "upgraded"]) |
| 457 | +def test_bound_quota_receipt_matches_selected_work(status: str) -> None: |
| 458 | + _validate_quota_identity( |
| 459 | + bound_quota_decision(status=status), goal_id="goal", agent_id="pilot", |
| 460 | + turn_instance_id="turn-selected-recall", |
| 461 | + ) |
| 462 | + |
| 463 | + |
| 464 | +@pytest.mark.parametrize( |
| 465 | + ("field", "value"), |
| 466 | + [("goal_id", "other"), ("agent_id", "other"), |
| 467 | + ("turn_instance_id", "other"), ("todo_id", "todo_other"), |
| 468 | + ("effect_id", "unrelated-effect"), ("schema_version", "unknown")], |
| 469 | +) |
| 470 | +def test_selected_quota_receipt_rejects_inconsistent_binding(field: str, value: str) -> None: |
| 471 | + decision = bound_quota_decision() |
| 472 | + decision["heartbeat_receipt"]["settlement_identity"][field] = value |
| 473 | + with pytest.raises(ValueError, match="settlement"): |
| 474 | + _validate_quota_identity( |
| 475 | + decision, goal_id="goal", agent_id="pilot", |
| 476 | + turn_instance_id="turn-selected-recall", |
| 477 | + ) |
| 478 | + |
| 479 | + |
| 480 | +def test_upgraded_quota_receipt_requires_committed_binding() -> None: |
| 481 | + decision = bound_quota_decision() |
| 482 | + del decision["heartbeat_receipt"]["settlement_identity"] |
| 483 | + with pytest.raises(ValueError, match="settlement identity"): |
| 484 | + _validate_quota_identity( |
| 485 | + decision, goal_id="goal", agent_id="pilot", |
| 486 | + turn_instance_id="turn-selected-recall", |
| 487 | + ) |
| 488 | + |
| 489 | + |
| 490 | +def test_upgraded_replan_receipt_uses_its_own_binding() -> None: |
| 491 | + decision = bound_quota_decision() |
| 492 | + decision["heartbeat_receipt"]["settlement_identity"] = SettlementIdentity( |
| 493 | + goal_id="goal", agent_id="pilot", todo_id=None, |
| 494 | + turn_instance_id="turn-selected-recall", replan_obligation_id="replan-0123456789abcdef", |
| 495 | + ).as_dict() |
| 496 | + decision["autonomous_replan_obligation"] = {"obligation_id": "replan-0123456789abcdef"} |
| 497 | + _validate_quota_identity( |
| 498 | + decision, goal_id="goal", agent_id="pilot", |
| 499 | + turn_instance_id="turn-selected-recall", |
| 500 | + ) |
| 501 | + decision["autonomous_replan_obligation"]["obligation_id"] = "replan-fedcba9876543210" |
| 502 | + with pytest.raises(ValueError, match="replan obligation"): |
| 503 | + _validate_quota_identity( |
| 504 | + decision, goal_id="goal", agent_id="pilot", |
| 505 | + turn_instance_id="turn-selected-recall", |
| 506 | + ) |
| 507 | + |
| 508 | + |
439 | 509 | def test_same_turn_receipt_round_trip_reuses_private_context(tmp_path: Path) -> None: |
440 | 510 | receipt_path = tmp_path / "pilot.json" |
441 | 511 | receipt = { |
@@ -613,6 +683,37 @@ def unavailable(operation: str, _params: Any) -> Any: |
613 | 683 | assert not agent_turn_recall_receipt_path(tmp_path, goal_id="goal", agent_id="pilot").exists() |
614 | 684 |
|
615 | 685 |
|
| 686 | +def test_selected_receipt_identity_transport_failure_stops_before_provider( |
| 687 | + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, |
| 688 | +) -> None: |
| 689 | + from loopx.control_plane import effect_program |
| 690 | + |
| 691 | + config = normalized_config(tmp_path) |
| 692 | + decision = bound_quota_decision() |
| 693 | + monkeypatch.setattr(recall_cli, "resolve_reward_memory_experiment", lambda **_kwargs: ({"status": "available"}, config)) |
| 694 | + monkeypatch.setattr(recall_cli, "_goal_repo", lambda *_args: tmp_path) |
| 695 | + monkeypatch.setattr(recall_cli, "_quota_decision", lambda *_args: decision) |
| 696 | + |
| 697 | + def unavailable(*_args: Any, **_kwargs: Any) -> Any: |
| 698 | + raise RuntimeError("private runtime startup detail") |
| 699 | + |
| 700 | + monkeypatch.setattr(effect_program, "effect_runtime_result", unavailable) |
| 701 | + effect_program._settlement_identity_full.cache_clear() |
| 702 | + monkeypatch.setattr(recall_cli, "run_agent_turn_recall", lambda *_args, **_kwargs: pytest.fail("provider boundary reached")) |
| 703 | + payloads: list[dict[str, Any]] = [] |
| 704 | + code = recall_cli.handle_agent_turn_recall_command( |
| 705 | + argparse.Namespace(command="agent-turn-recall", goal_id="goal", agent_id="pilot", |
| 706 | + turn_instance_id="turn-selected-recall", quota_decision_json="unused", |
| 707 | + session_ref=None, force_refresh=False, execute=True), |
| 708 | + registry_path=tmp_path / "registry.json", output_format=lambda *_args: "json", |
| 709 | + print_payload=lambda payload, *_args: payloads.append(payload), |
| 710 | + ) |
| 711 | + assert code == 2 and payloads[0]["status"] == "runtime_unavailable" |
| 712 | + assert payloads[0]["provider_call_count"] == 0 |
| 713 | + assert "private runtime" not in json.dumps(payloads[0]) |
| 714 | + assert not agent_turn_recall_receipt_path(tmp_path, goal_id="goal", agent_id="pilot").exists() |
| 715 | + |
| 716 | + |
616 | 717 | def test_expiry_survives_candidate_review_and_activation(tmp_path: Path) -> None: |
617 | 718 | config = normalized_config(tmp_path) |
618 | 719 | corpus = resolve_reward_memory_surface_config(config, SURFACE)["corpus"] |
|
0 commit comments