from __future__ import annotations import threading import uuid from concurrent.futures import ThreadPoolExecutor from dataclasses import replace from savings_postgres_testkit import ( # noqa: F401 - 注册 pg_factory fixture _pg_factory_fixture, ) from sqlalchemy import func, select from sqlalchemy.orm import Session, sessionmaker from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType from app.models.agent_asset import AgentAsset from app.models.agent_asset_release_telemetry import ( AgentAssetReleaseAuditSample, AgentAssetReleaseLabel, AgentAssetReleaseObservation, ) from app.models.financial_record import ExpenseClaim from app.services.agent_asset_release_review import AgentAssetReleaseReviewService from app.services.agent_asset_release_scheduler import AgentAssetReleaseScheduler from app.services.agent_asset_release_telemetry import ( AgentAssetReleaseTelemetryService, ReleaseObservationInput, ReleaseTelemetryIdempotencyConflict, ReleaseTelemetryStaleRelease, ) from app.services.expense_claim_release_telemetry import ( ExpenseClaimReleaseTelemetryRecorder, ) def test_concurrent_observation_and_label_replay_create_one_fact( pg_factory: sessionmaker[Session], ) -> None: tenant_id, asset_id, rule_code = _seed_asset(pg_factory) payload = _payload(tenant_id, asset_id, rule_code, source="claim-same") ready = threading.Barrier(2) def record_observation() -> str: with pg_factory() as db: ready.wait(timeout=5) row = AgentAssetReleaseTelemetryService(db).record_observation(payload) db.commit() return row.id with ThreadPoolExecutor(max_workers=2) as pool: observation_ids = [ future.result(timeout=10) for future in ( pool.submit(record_observation), pool.submit(record_observation), ) ] assert len(set(observation_ids)) == 1 observation_id = observation_ids[0] label_ready = threading.Barrier(2) def record_label() -> str: with pg_factory() as db: label_ready.wait(timeout=5) row = AgentAssetReleaseTelemetryService(db).record_review_label( tenant_id=tenant_id, observation_id=observation_id, label="confirmed", request_id="review-concurrent-same", actor_id="trusted-reviewer", ) db.commit() return row.id with ThreadPoolExecutor(max_workers=2) as pool: label_ids = [ future.result(timeout=10) for future in (pool.submit(record_label), pool.submit(record_label)) ] assert len(set(label_ids)) == 1 with pg_factory() as db: assert db.scalar( select(func.count()) .select_from(AgentAssetReleaseObservation) .where( AgentAssetReleaseObservation.tenant_id == tenant_id, AgentAssetReleaseObservation.id == observation_id, ) ) == 1 assert db.scalar( select(func.count()) .select_from(AgentAssetReleaseAuditSample) .where( AgentAssetReleaseAuditSample.tenant_id == tenant_id, AgentAssetReleaseAuditSample.observation_id == observation_id, ) ) == 1 assert db.scalar( select(func.count()) .select_from(AgentAssetReleaseLabel) .where( AgentAssetReleaseLabel.tenant_id == tenant_id, AgentAssetReleaseLabel.observation_id == observation_id, ) ) == 1 def test_stage_transition_wins_before_label_and_rejects_stale_fact( pg_factory: sessionmaker[Session], ) -> None: tenant_id, asset_id, rule_code = _seed_asset(pg_factory) with pg_factory() as db: observation = AgentAssetReleaseTelemetryService(db).record_observation( _payload(tenant_id, asset_id, rule_code, source="claim-stage-race") ) db.commit() observation_id = observation.id label_started = threading.Event() def record_stale_label() -> str: with pg_factory() as db: label_started.set() try: AgentAssetReleaseTelemetryService(db).record_review_label( tenant_id=tenant_id, observation_id=observation_id, label="confirmed", request_id="review-after-stage-transition", actor_id="trusted-reviewer", ) db.commit() return "created" except ReleaseTelemetryStaleRelease: db.rollback() return "stale" with pg_factory() as transition_db: asset = transition_db.scalar( select(AgentAsset).where(AgentAsset.id == asset_id).with_for_update() ) assert asset is not None config = dict(asset.config_json or {}) state = dict(config["release_guard"]) state["stage"] = "canary" config["release_guard"] = state asset.config_json = config transition_db.flush() with ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit(record_stale_label) assert label_started.wait(timeout=5) transition_db.commit() outcome = future.result(timeout=10) assert outcome == "stale" with pg_factory() as db: assert db.scalar( select(func.count()) .select_from(AgentAssetReleaseLabel) .where( AgentAssetReleaseLabel.tenant_id == tenant_id, AgentAssetReleaseLabel.observation_id == observation_id, ) ) == 0 def test_concurrent_conflicting_observation_payload_has_one_winner( pg_factory: sessionmaker[Session], ) -> None: tenant_id, asset_id, rule_code = _seed_asset(pg_factory) first = _payload(tenant_id, asset_id, rule_code, source="claim-conflict") second = replace(first, candidate_hit=False) ready = threading.Barrier(2) def record_once(payload: ReleaseObservationInput) -> str: with pg_factory() as db: ready.wait(timeout=5) try: AgentAssetReleaseTelemetryService(db).record_observation(payload) db.commit() return "created" except ReleaseTelemetryIdempotencyConflict: db.rollback() return "conflict" with ThreadPoolExecutor(max_workers=2) as pool: outcomes = [ future.result(timeout=10) for future in ( pool.submit(record_once, first), pool.submit(record_once, second), ) ] assert sorted(outcomes) == ["conflict", "created"] with pg_factory() as db: assert db.scalar( select(func.count()) .select_from(AgentAssetReleaseObservation) .where( AgentAssetReleaseObservation.tenant_id == tenant_id, AgentAssetReleaseObservation.asset_id == asset_id, ) ) == 1 def test_concurrent_conflicting_label_payload_has_one_winner( pg_factory: sessionmaker[Session], ) -> None: tenant_id, asset_id, rule_code = _seed_asset(pg_factory) with pg_factory() as db: observation = AgentAssetReleaseTelemetryService(db).record_observation( _payload(tenant_id, asset_id, rule_code, source="claim-label-conflict") ) db.commit() observation_id = observation.id ready = threading.Barrier(2) def record_once(label: str) -> str: with pg_factory() as db: ready.wait(timeout=5) try: AgentAssetReleaseTelemetryService(db).record_review_label( tenant_id=tenant_id, observation_id=observation_id, label=label, # type: ignore[arg-type] request_id="review-conflicting-same-request", actor_id="trusted-reviewer", ) db.commit() return "created" except ReleaseTelemetryIdempotencyConflict: db.rollback() return "conflict" with ThreadPoolExecutor(max_workers=2) as pool: outcomes = [ future.result(timeout=10) for future in ( pool.submit(record_once, "confirmed"), pool.submit(record_once, "false_positive"), ) ] assert sorted(outcomes) == ["conflict", "created"] with pg_factory() as db: assert db.scalar( select(func.count()) .select_from(AgentAssetReleaseLabel) .where( AgentAssetReleaseLabel.tenant_id == tenant_id, AgentAssetReleaseLabel.observation_id == observation_id, ) ) == 1 def test_negative_blind_review_requires_two_distinct_actors_under_concurrency( pg_factory: sessionmaker[Session], ) -> None: tenant_id, asset_id, rule_code = _seed_asset(pg_factory) with pg_factory() as db: observation = AgentAssetReleaseTelemetryService(db).record_observation( replace( _payload( tenant_id, asset_id, rule_code, source="claim-negative-double-review", ), candidate_hit=False, baseline_hit=True, ) ) db.commit() observation_id = observation.id ready = threading.Barrier(2) def record_same_actor(request_id: str) -> str: with pg_factory() as db: ready.wait(timeout=5) row = AgentAssetReleaseReviewService(db).record_label( tenant_id=tenant_id, asset_id=asset_id, observation_id=observation_id, label="risk_present", actor_id="same-independent-reviewer", request_id=request_id, ) db.commit() return row.id with ThreadPoolExecutor(max_workers=2) as pool: same_actor_label_ids = [ future.result(timeout=10) for future in ( pool.submit(record_same_actor, "same-actor-review-a"), pool.submit(record_same_actor, "same-actor-review-b"), ) ] assert len(set(same_actor_label_ids)) == 2 with pg_factory() as db: queue = AgentAssetReleaseReviewService(db).list_pending( tenant_id=tenant_id, asset_id=asset_id, ) assert queue["pending_total"] == 1 assert queue["items"][0]["reviewer_count"] == 1 assert queue["items"][0]["required_reviewers"] == 2 AgentAssetReleaseReviewService(db).record_label( tenant_id=tenant_id, asset_id=asset_id, observation_id=observation_id, label="risk_present", actor_id="second-independent-reviewer", request_id="second-actor-review", ) db.commit() resolved = AgentAssetReleaseReviewService(db).list_pending( tenant_id=tenant_id, asset_id=asset_id, ) assert resolved["pending_total"] == 0 def test_failed_observation_survives_business_transaction_rollback( pg_factory: sessionmaker[Session], ) -> None: tenant_id, asset_id, rule_code = _seed_asset(pg_factory) claim = ExpenseClaim(id="claim-durable-failure", claim_no="BX-DURABLE-FAILURE") manifest = { "_rule_asset_id": asset_id, "_release_stage": "shadow", "_release_mode": "shadow", "_rule_version": "v2", "rule_code": rule_code, } with pg_factory() as business_db: asset = business_db.get(AgentAsset, asset_id) assert asset is not None asset.description = "this business mutation must roll back" business_db.flush() ExpenseClaimReleaseTelemetryRecorder(business_db).record_failure_durably( tenant_id=tenant_id, claim=claim, manifest=manifest, failure_code="evaluator_error", business_stage="reimbursement", ) business_db.rollback() with pg_factory() as db: asset = db.get(AgentAsset, asset_id) observation = db.scalar( select(AgentAssetReleaseObservation).where( AgentAssetReleaseObservation.tenant_id == tenant_id, AgentAssetReleaseObservation.asset_id == asset_id, AgentAssetReleaseObservation.runtime_status == "failed", ) ) assert asset is not None and asset.description == "" assert observation is not None assert observation.failure_code == "evaluator_error" def test_release_scheduler_advisory_lease_has_single_owner_and_transfers( pg_factory: sessionmaker[Session], ) -> None: scheduler = AgentAssetReleaseScheduler(session_factory=pg_factory) with pg_factory() as first, pg_factory() as second: assert scheduler._try_acquire_lease(first) is True assert scheduler._try_acquire_lease(second) is False scheduler._release_lease(first) assert scheduler._try_acquire_lease(second) is True scheduler._release_lease(second) def _seed_asset(factory: sessionmaker[Session]) -> tuple[str, str, str]: suffix = uuid.uuid4().hex tenant_id = f"tenant-release-{suffix}" asset_id = str(uuid.uuid4()) rule_code = f"risk.release.{suffix}" with factory() as db: db.add( AgentAsset( id=asset_id, asset_type=AgentAssetType.RULE.value, code=rule_code, name="发布遥测并发规则", description="", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["travel"], owner="postgres-test", status=AgentAssetStatus.ACTIVE.value, current_version="v1", published_version="v1", working_version="v2", config_json={ "tenant_id": tenant_id, "detail_mode": "json_risk", "enabled": True, "release_guard": { "release_id": f"release-{suffix}", "stage": "shadow", "candidate_version": "v2", "previous_version": "v1", }, }, ) ) db.commit() return tenant_id, asset_id, rule_code def _payload( tenant_id: str, asset_id: str, rule_code: str, *, source: str, ) -> ReleaseObservationInput: return ReleaseObservationInput( tenant_id=tenant_id, asset_id=asset_id, release_id=f"release-{rule_code.removeprefix('risk.release.')}", stage="shadow", version="v2", rule_code=rule_code, source_key=source, candidate_hit=True, baseline_hit=True, )