From d6b3ecdb31794575d529df9d794a46f8c9459dcb Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 20 Jul 2026 23:10:17 +0800 Subject: [PATCH] test: cover conversational persistence limits --- ...conversational-rectification-store.test.ts | 86 +++- ...t_conversational_rectification_contract.py | 130 +++++- ...sational_rectification_postgres_runtime.py | 387 ++++++++++++++++++ 3 files changed, 591 insertions(+), 12 deletions(-) diff --git a/frontend/tests/conversational-rectification-store.test.ts b/frontend/tests/conversational-rectification-store.test.ts index ae7abd5f..78af6be0 100644 --- a/frontend/tests/conversational-rectification-store.test.ts +++ b/frontend/tests/conversational-rectification-store.test.ts @@ -10,10 +10,12 @@ import { type ConversationalRectificationRpcClient, } from "../src/lib/conversational-rectification/store.ts"; import { ConversationalRectificationBilling } from "../src/lib/conversational-rectification/billing.ts"; +import { conversationalRectificationTurnSchema } from "../src/lib/conversational-rectification/contracts.ts"; import { conversationalRectificationActionReceiptRequestSchema, conversationalRectificationActionReceiptResponseSchema, declaredBirthInputSchema, + lifeEventEvidenceSchema, privateCandidateSchema, validationReceiptSchema, } from "../src/lib/conversational-rectification/persistence-contracts.ts"; @@ -472,6 +474,88 @@ test("declared birth input is strict, source-aware, bounded, and location-comple assert.equal(exactClue.birthTimeClue, " exact clue spacing "); }); +test("durable JSON uses PostgreSQL-stable numeric vectors recursively", () => { + const stableVector = { + zero: 0, + minFraction: 0.000001, + decimal: 0.123456, + maxSafe: 9_007_199_254_740_991, + }; + assert.equal(postgresJsonbTextBytes(stableVector), 86); + assert.equal(declaredBirthInputSchema.safeParse({ + ...storedRow.declared_birth_input, + birthplace: { + ...storedRow.declared_birth_input.birthplace, + latitude: 25.0268, + longitude: 121.5434, + timezoneOffset: 8, + }, + }).success, true); + + for (const candidateWeights of [[1e-100], [0.1234567]]) { + assert.equal(privateCandidateSchema.safeParse({ + resultId, + calculationVersion: "rectification-v3.1", + candidateWeights, + }).success, false); + } + assert.equal(privateCandidateSchema.safeParse({ + resultId, + calculationVersion: "rectification-v3.1", + scoredHistoricalEvidence: [{ + evidenceId: "00000000-0000-4000-8000-000000000105", + domain: "career", + candidateTime: "05:21", + score: 1e-100, + ruleRefs: [], + }], + }).success, false); + assert.equal(declaredBirthInputSchema.safeParse({ + ...storedRow.declared_birth_input, + birthplace: { + ...storedRow.declared_birth_input.birthplace, + latitude: 1e-100, + }, + }).success, false); + assert.equal(postgresJsonbTextBytes({ nested: [{ score: 1e-100 }] }), Number.POSITIVE_INFINITY); +}); + +test("evidence recap enforces the SQL-matched aggregate byte limit", () => { + const evidenceRecap = Array.from({ length: 9 }, (_, index) => ({ + id: `00000000-0000-4000-8000-${(700 + index).toString().padStart(12, "0")}`, + summary: "事".repeat(900), + dateLabel: "2020-01", + })); + const turn = { ...firstTurn, evidenceRecap }; + + assert.ok(postgresJsonbTextBytes(evidenceRecap) > 24_576); + assert.ok(postgresJsonbTextBytes(turn) < 65_536); + assert.equal(conversationalRectificationTurnSchema.safeParse(turn).success, false); +}); + +test("life-event evidence rejects unknown, blank, and non-boolean durable values", () => { + const evidence = { + id: "00000000-0000-4000-8000-000000000105", + rawText: "2019 年 7 月换工作", + domain: "career" as const, + eventSummary: "换工作", + dateValue: "2019-07", + datePrecision: "month" as const, + extractionStatus: "clear" as const, + scoreable: true, + }; + assert.equal(lifeEventEvidenceSchema.safeParse(evidence).success, true); + for (const invalid of [ + { ...evidence, extra: "discard me" }, + { ...evidence, eventSummary: " \t " }, + { ...evidence, dateValue: " \t " }, + { ...evidence, scoreable: null }, + { ...evidence, scoreable: "true" }, + ]) { + assert.equal(lifeEventEvidenceSchema.safeParse(invalid).success, false); + } +}); + test("durable private and receipt schemas accept boundaries and reject oversize or unknown fields", () => { assert.equal(postgresJsonbTextBytes({ a: [1, 2] }), 13); assert.equal(validationReceiptSchema.safeParse({ @@ -579,7 +663,7 @@ test("public turn JSON fields reject field and byte boundary violations", () => const nearTurnLimit = { ...exact, narrative: "n".repeat(12_000), - evidenceRecap: Array.from({ length: 16 }, (_, index) => ({ + evidenceRecap: Array.from({ length: 7 }, (_, index) => ({ id: `00000000-0000-4000-8000-${(300 + index).toString().padStart(12, "0")}`, summary: "事".repeat(1_000), dateLabel: "d".repeat(80), diff --git a/tests/test_conversational_rectification_contract.py b/tests/test_conversational_rectification_contract.py index f556aba9..b7bde3c7 100644 --- a/tests/test_conversational_rectification_contract.py +++ b/tests/test_conversational_rectification_contract.py @@ -194,8 +194,8 @@ def test_legacy_import_is_waived_without_changing_credits() -> None: assert "imported_from_case_id" in body assert "'migration_waived'" in body assert "insert into public.birth_time_rectification_billing" in body - assert "credits =" not in body - assert "credit_transactions" not in body + assert "set credits = profile.credits -" not in body + assert "'reserve', -" not in body assert "update public.birth_time_rectification_cases" not in body for preserved_profile_field in ( "birth_time_clue", @@ -263,16 +263,21 @@ def test_start_identity_and_account_concurrency_are_server_guarded() -> None: def test_new_account_start_recovers_a_committed_pre_case_reservation() -> None: - reserve = _function(_normalized(BILLING), "reserve_conversational_rectification_fee") + billing = _normalized(BILLING) + reserve = _function(billing, "reserve_conversational_rectification_fee") + recovery = _function( + billing, "recover_conversational_rectification_orphan_reservations" + ) - assert "orphan_billing.state = 'reserved'" in reserve - assert "orphan_case.id is null" in reserve - assert "set credits = profile.credits + v_orphan.price" in reserve - assert "'refund', v_orphan.price" in reserve - assert "set state = 'released'" in reserve - assert "'recover_fee'" in reserve - assert "v_orphan.case_id" in reserve - assert reserve.index("set state = 'released'") < reserve.index( + assert "orphan_billing.state = 'reserved'" in recovery + assert "orphan_case.id is null" in recovery + assert "set credits = profile.credits + v_orphan.price" in recovery + assert "'refund', v_orphan.price" in recovery + assert "set state = 'released'" in recovery + assert "'recover_fee'" in recovery + assert "v_orphan.case_id" in recovery + assert "recover_conversational_rectification_orphan_reservations" in reserve + assert reserve.index("recover_conversational_rectification_orphan_reservations") < reserve.index( "set credits = profile.credits - p_price" ) @@ -301,6 +306,109 @@ def test_durable_json_columns_have_byte_and_field_shape_guards() -> None: assert "conversational_rectification_valid_private_candidate(candidate_result)" in sql +def test_durable_json_numbers_and_evidence_recap_share_postgres_bounds() -> None: + sql = _normalized(SCHEMA) + + assert "create or replace function public.conversational_rectification_numbers_are_stable" in sql + for validator in ( + "conversational_rectification_valid_declared_birth_input", + "conversational_rectification_valid_private_candidate", + "conversational_rectification_valid_public_turn", + "conversational_rectification_valid_action_request", + "conversational_rectification_valid_action_response", + ): + assert "conversational_rectification_numbers_are_stable" in _function(sql, validator) + recap = _function(sql, "conversational_rectification_valid_evidence_recap") + assert "octet_length(p_value::text) <= 24576" in recap + + +def test_life_event_evidence_is_strictly_validated_before_insert() -> None: + schema = _normalized(SCHEMA) + transitions = _normalized(TRANSITIONS) + validator = _function(schema, "conversational_rectification_valid_life_event_evidence") + save = _function(transitions, "save_conversational_rectification_turn") + + for invariant in ( + "conversational_rectification_has_only_keys", + "jsonb_typeof(p_value -> 'id')", + "conversational_rectification_valid_uuid_text", + "p_value ->> 'id' !~* '^[0-9a-f]{8}-", + "conversational_rectification_text_is_nonblank( p_value ->> 'rawtext'", + "conversational_rectification_text_is_nonblank( p_value ->> 'eventsummary'", + "conversational_rectification_text_is_nonblank( p_value ->> 'datevalue'", + "jsonb_typeof(p_value -> 'scoreable')", + ): + assert invariant in validator + for key in ( + "id", + "rawtext", + "domain", + "eventsummary", + "datevalue", + "dateprecision", + "extractionstatus", + "scoreable", + ): + assert f"'{key}'" in validator + evidence_array_validator = "conversational_rectification_valid_life_event_evidence_array" + assert evidence_array_validator in save + assert save.index(evidence_array_validator) < save.index( + "insert into public.birth_time_rectification_event_evidence" + ) + assert "coalesce((item ->> 'scoreable')::boolean, false)" not in save + assert "nullif(item ->> 'datevalue', '')" not in save + + +def test_case_mutations_enforce_cumulative_load_limits_under_the_case_lock() -> None: + transitions = _normalized(TRANSITIONS) + helper = _function(transitions, "conversational_rectification_case_fits_load_limits") + + assert "count(*)" in helper + assert "<= 2000" in helper + assert "octet_length(v_projection::text) <= 4194304" in helper + for name in ( + "save_conversational_rectification_turn", + "pause_conversational_rectification_case", + "abandon_conversational_rectification_case", + "confirm_conversational_rectification_candidate", + ): + body = _function(transitions, name) + assert body.index("for update") < body.index( + "conversational_rectification_case_fits_load_limits" + ) + assert body.index("conversational_rectification_case_fits_load_limits") < body.index( + "insert into public.birth_time_rectification_turns" + ) + + +def test_legacy_import_recovers_orphan_reservations_without_a_paid_start() -> None: + billing = _normalized(BILLING) + transitions = _normalized(TRANSITIONS) + recovery = _function( + billing, "recover_conversational_rectification_orphan_reservations" + ) + imported = _function( + transitions, "import_legacy_conversational_rectification_case" + ) + + for invariant in ( + "conversational-rectification-case", + "for update", + "orphan_billing.state = 'reserved'", + "orphan_case.id is null", + "set credits = profile.credits + v_orphan.price", + "'refund', v_orphan.price", + "set state = 'released'", + "'recover_fee'", + ): + assert invariant in recovery + assert "recover_conversational_rectification_orphan_reservations" in imported + assert imported.index("recover_conversational_rectification_orphan_reservations") < imported.index( + "'migration_waived'" + ) + assert "set credits = profile.credits -" not in imported + + def test_declared_birth_input_is_strict_source_aware_and_location_complete() -> None: sql = _normalized(SCHEMA) body = _function(sql, "conversational_rectification_valid_declared_birth_input") diff --git a/tests/test_conversational_rectification_postgres_runtime.py b/tests/test_conversational_rectification_postgres_runtime.py index 2a0179b1..e64bd3e1 100644 --- a/tests/test_conversational_rectification_postgres_runtime.py +++ b/tests/test_conversational_rectification_postgres_runtime.py @@ -303,6 +303,64 @@ def _create_case( ) +def _complete(database: PgDatabase, user_id: str, action_id: str, version: int = 0) -> dict[str, object]: + result = database.sql( + f""" + select row_to_json(completion)::text + from public.complete_conversational_rectification_fee( + '{user_id}'::uuid, + '{action_id}'::uuid, + {version}, + '{action_id}'::uuid + ) completion; + """ + ) + return json.loads(result) + + +def _save_statement( + user_id: str, + case_id: str, + expected_version: int, + action_id: str, + evidence: list[dict[str, object]], + *, + turn: dict[str, object] | None = None, + validation_receipt: dict[str, object] | None = None, + private_candidate: dict[str, object] | None = None, +) -> str: + next_turn = turn or { + **_valid_turn(case_id), + "turnVersion": expected_version + 1, + } + return f""" + select public.save_conversational_rectification_turn( + '{user_id}'::uuid, + '{case_id}'::uuid, + {expected_version}, + '{action_id}'::uuid, + {_jsonb(next_turn)}, + {_jsonb(evidence)}, + {_jsonb(validation_receipt or {"modelId": "synthetic-model", "schemaValidated": True})}, + {_jsonb(private_candidate or _valid_private_candidate())} + )::text; + """ + + +def _create_legacy_case(database: PgDatabase, user_id: str, legacy_case_id: str) -> None: + database.sql( + f""" + insert into public.birth_time_rectification_cases ( + id, user_id, status, reported_date, reported_time, source, + uncertainty_before_minutes, uncertainty_after_minutes, journey_protocol + ) values ( + '{legacy_case_id}'::uuid, '{user_id}'::uuid, 'rectifying', + '1990-01-01', '05:20', 'legacy_import', 0, 0, 'legacy-guided-v1' + ); + """ + ) + + def test_committed_pre_case_reservation_is_recovered_by_a_fresh_account_action( pg14_database: PgDatabase, ) -> None: @@ -572,3 +630,332 @@ def test_database_rejects_oversize_or_unknown_durable_json(pg14_database: PgData where user_id = '{user_id}'::uuid and action_kind = 'reserve_fee'; """ ) + + +def test_postgres_uses_the_shared_stable_numeric_boundary_vectors( + pg14_database: PgDatabase, +) -> None: + stable_vector = { + "zero": 0, + "minFraction": 0.000001, + "decimal": 0.123456, + "maxSafe": 9_007_199_254_740_991, + } + numeric_result = pg14_database.sql( + f""" + select pg_catalog.jsonb_build_object( + 'valid', public.conversational_rectification_numbers_are_stable({_jsonb(stable_vector)}), + 'bytes', pg_catalog.octet_length(({_jsonb(stable_vector)})::text) + )::text; + """ + ) + assert json.loads(numeric_result) == {"valid": True, "bytes": 86} + + for invalid in ( + {"nested": [{"score": 1e-100}]}, + {"nested": [{"score": 0.1234567}]}, + {"nested": [{"score": 9_007_199_254_740_992}]}, + ): + assert pg14_database.sql( + "select public.conversational_rectification_numbers_are_stable(" + f"{_jsonb(invalid)})::text" + ) == "false" + + assert pg14_database.sql( + "select public.conversational_rectification_valid_private_candidate(" + f"{_jsonb({**_valid_private_candidate(), 'candidateWeights': [1e-100]})})::text" + ) == "false" + unstable_declared_input = { + **_valid_declared_birth_input(), + "birthplace": { + **_valid_declared_birth_input()["birthplace"], + "latitude": 1e-100, + }, + } + assert pg14_database.sql( + "select public.conversational_rectification_valid_declared_birth_input(" + f"{_jsonb(unstable_declared_input)})::text" + ) == "false" + + +def test_save_rejects_invalid_evidence_without_discarding_or_coercing_fields( + pg14_database: PgDatabase, +) -> None: + user_id = "00000000-0000-4000-8000-000000000961" + case_id = "00000000-0000-4000-8000-000000000962" + _create_user(pg14_database, user_id) + _reserve(pg14_database, user_id, case_id) + _create_case(pg14_database, user_id, case_id, _valid_declared_birth_input()) + _complete(pg14_database, user_id, case_id) + + evidence = { + "id": "00000000-0000-4000-8000-000000000963", + "rawText": "2019 年 7 月换工作", + "domain": "career", + "eventSummary": "换工作", + "dateValue": "2019-07", + "datePrecision": "month", + "extractionStatus": "clear", + "scoreable": True, + } + invalid_values = ( + {**evidence, "unknown": "must not be discarded"}, + {**evidence, "id": "not-a-uuid"}, + {**evidence, "id": "00000000000040008000000000000001"}, + {**evidence, "rawText": 42}, + {**evidence, "domain": "finance"}, + {**evidence, "eventSummary": " \t "}, + {**evidence, "eventSummary": "\u00a0\u2007\ufeff"}, + {**evidence, "dateValue": " \t "}, + {**evidence, "datePrecision": "quarter"}, + {**evidence, "extractionStatus": "guessed"}, + {**evidence, "scoreable": None}, + {**evidence, "scoreable": "true"}, + ) + for index, invalid in enumerate(invalid_values): + action_id = f"00000000-0000-4000-8000-{970 + index:012d}" + assert pg14_database.rejects( + _save_statement(user_id, case_id, 0, action_id, [invalid]) + ) + + assert pg14_database.sql( + f"select count(*) from public.birth_time_rectification_event_evidence where case_id = '{case_id}'::uuid" + ) == "0" + valid_without_optional_scoreable = {key: value for key, value in evidence.items() if key != "scoreable"} + pg14_database.sql( + _save_statement( + user_id, + case_id, + 0, + "00000000-0000-4000-8000-000000000999", + [valid_without_optional_scoreable], + ) + ) + assert pg14_database.sql( + f"select scoreable::text from public.birth_time_rectification_event_evidence where case_id = '{case_id}'::uuid" + ) == "false" + + +def test_save_rejects_cumulative_evidence_count_before_inserting( + pg14_database: PgDatabase, +) -> None: + user_id = "00000000-0000-4000-8000-000000000981" + case_id = "00000000-0000-4000-8000-000000000982" + _create_user(pg14_database, user_id) + _reserve(pg14_database, user_id, case_id) + _create_case(pg14_database, user_id, case_id, _valid_declared_birth_input()) + _complete(pg14_database, user_id, case_id) + pg14_database.sql( + f""" + insert into public.birth_time_rectification_event_evidence ( + id, case_id, source_turn_id, raw_text, domain, event_summary, + date_value, date_precision, extraction_status, scoreable + ) + select pg_catalog.md5('evidence-count-' || series)::uuid, + '{case_id}'::uuid, turn.id, 'event', 'career', 'summary', + null, 'unknown', 'clear', false + from pg_catalog.generate_series(1, 2000) series + cross join public.birth_time_rectification_turns turn + where turn.case_id = '{case_id}'::uuid and turn.turn_version = 0; + """ + ) + extra = { + "id": "00000000-0000-4000-8000-000000000983", + "rawText": "one more", + "domain": "career", + "eventSummary": "one more", + "dateValue": None, + "datePrecision": "unknown", + "extractionStatus": "clear", + "scoreable": False, + } + + assert pg14_database.rejects( + _save_statement( + user_id, + case_id, + 0, + "00000000-0000-4000-8000-000000000984", + [extra], + ) + ) + assert pg14_database.sql( + f"select count(*) from public.birth_time_rectification_event_evidence where case_id = '{case_id}'::uuid" + ) == "2000" + assert pg14_database.sql( + f"select count(*) from public.birth_time_rectification_turns where case_id = '{case_id}'::uuid" + ) == "1" + + +def test_save_rejects_cumulative_validation_receipt_count_before_inserting( + pg14_database: PgDatabase, +) -> None: + user_id = "00000000-0000-4000-8000-000000000985" + case_id = "00000000-0000-4000-8000-000000000986" + _create_user(pg14_database, user_id) + _reserve(pg14_database, user_id, case_id) + _create_case(pg14_database, user_id, case_id, _valid_declared_birth_input()) + _complete(pg14_database, user_id, case_id) + base_turn = _valid_turn(case_id) + last_turn = {**base_turn, "turnVersion": 1999} + pg14_database.sql( + f""" + insert into public.birth_time_rectification_turns ( + case_id, turn_version, narrative, candidate, technical_receipt, + evidence_request, evidence_recap, actions, output_validation_receipt + ) + select '{case_id}'::uuid, series, 'seeded turn', + {_jsonb(base_turn['candidate'])}, {_jsonb(base_turn['technicalReceipt'])}, + {_jsonb(base_turn['evidenceRequest'])}, '[]'::jsonb, + {_jsonb(base_turn['actions'])}, + {_jsonb({'modelId': 'synthetic-model', 'schemaValidated': True})} + from pg_catalog.generate_series(1, 1999) series; + update public.birth_time_rectification_cases + set turn_version = 1999, + turn_state = {_jsonb(last_turn)}, + journey_snapshot = {_jsonb(last_turn)} + where id = '{case_id}'::uuid; + """ + ) + + assert pg14_database.rejects( + _save_statement( + user_id, + case_id, + 1999, + "00000000-0000-4000-8000-000000000987", + [], + ) + ) + assert pg14_database.sql( + f"select count(*) from public.birth_time_rectification_turns where case_id = '{case_id}'::uuid" + ) == "2000" + + +def test_save_rejects_a_projected_load_envelope_over_four_mib( + pg14_database: PgDatabase, +) -> None: + user_id = "00000000-0000-4000-8000-000000000988" + case_id = "00000000-0000-4000-8000-000000000989" + _create_user(pg14_database, user_id) + _reserve(pg14_database, user_id, case_id) + _create_case(pg14_database, user_id, case_id, _valid_declared_birth_input()) + _complete(pg14_database, user_id, case_id) + pg14_database.sql( + f""" + insert into public.birth_time_rectification_event_evidence ( + id, case_id, source_turn_id, raw_text, domain, event_summary, + date_value, date_precision, extraction_status, scoreable + ) + select pg_catalog.md5('evidence-bytes-' || series)::uuid, + '{case_id}'::uuid, turn.id, pg_catalog.repeat('事', 4000), + 'career', pg_catalog.repeat('事', 1000), null, 'unknown', 'clear', false + from pg_catalog.generate_series(1, 275) series + cross join public.birth_time_rectification_turns turn + where turn.case_id = '{case_id}'::uuid and turn.turn_version = 0; + """ + ) + before_bytes = int(pg14_database.sql( + f"select pg_catalog.octet_length(public.load_conversational_rectification_case('{user_id}'::uuid, '{case_id}'::uuid)::text)" + )) + assert 4_194_304 - 16_384 < before_bytes <= 4_194_304 + extra = { + "id": "00000000-0000-4000-8000-000000000990", + "rawText": "事" * 4_000, + "domain": "career", + "eventSummary": "事" * 1_000, + "dateValue": "d" * 80, + "datePrecision": "range", + "extractionStatus": "corrected", + "scoreable": True, + } + + assert pg14_database.rejects( + _save_statement( + user_id, + case_id, + 0, + "00000000-0000-4000-8000-000000000991", + [extra], + ) + ) + assert pg14_database.sql( + f"select count(*) from public.birth_time_rectification_event_evidence where case_id = '{case_id}'::uuid" + ) == "275" + + +def test_crash_then_legacy_import_refunds_orphan_without_an_unrelated_paid_start( + pg14_database: PgDatabase, +) -> None: + user_id = "00000000-0000-4000-8000-000000000992" + lost_action = "00000000-0000-4000-8000-000000000993" + legacy_case_id = "00000000-0000-4000-8000-000000000994" + import_action = "00000000-0000-4000-8000-000000000995" + _create_user(pg14_database, user_id, credits=10) + _create_legacy_case(pg14_database, user_id, legacy_case_id) + assert _reserve(pg14_database, user_id, lost_action)["credits"] == 7 + + statement = f""" + select public.import_legacy_conversational_rectification_case( + '{user_id}'::uuid, '{import_action}'::uuid, '{legacy_case_id}'::uuid, + 0, '{import_action}'::uuid, 3, null, + {_jsonb(_valid_turn(import_action))}, + {_jsonb({'modelId': 'synthetic-model', 'schemaValidated': True})}, + {_jsonb(_valid_private_candidate())} + )::text; + """ + imported = json.loads(pg14_database.sql(statement)) + assert imported["billing_state"] == "migration_waived" + assert json.loads(pg14_database.sql( + f""" + select pg_catalog.jsonb_build_object( + 'credits', profile.credits, + 'orphanState', orphan.state, + 'importState', imported.state, + 'importBalance', imported.balance_after, + 'reserves', pg_catalog.count(*) filter (where tx.transaction_type = 'reserve'), + 'refunds', pg_catalog.count(*) filter (where tx.transaction_type = 'refund'), + 'recoveryReceipts', ( + select pg_catalog.count(*) + from public.birth_time_rectification_action_receipts receipt + where receipt.user_id = profile.id and receipt.action_kind = 'recover_fee' + ) + )::text + from public.profiles profile + join public.birth_time_rectification_billing orphan on orphan.case_id = '{lost_action}'::uuid + join public.birth_time_rectification_billing imported on imported.case_id = '{import_action}'::uuid + left join public.credit_transactions tx on tx.user_id = profile.id + where profile.id = '{user_id}'::uuid + group by profile.id, profile.credits, orphan.state, imported.state, imported.balance_after; + """ + )) == { + "credits": 10, + "orphanState": "released", + "importState": "migration_waived", + "importBalance": 10, + "reserves": 1, + "refunds": 1, + "recoveryReceipts": 1, + } + assert json.loads(pg14_database.sql( + f""" + select pg_catalog.jsonb_build_object( + 'kind', receipt.request ->> 'kind', + 'credits', (receipt.response ->> 'credits')::integer, + 'transactionRequest', tx.request_id + )::text + from public.birth_time_rectification_action_receipts receipt + join public.credit_transactions tx + on tx.user_id = receipt.user_id and tx.transaction_type = 'refund' + where receipt.user_id = '{user_id}'::uuid and receipt.action_kind = 'recover_fee'; + """ + )) == { + "kind": "recover_fee", + "credits": 10, + "transactionRequest": f"rectification:{lost_action}", + } + + assert json.loads(pg14_database.sql(statement)) == imported + assert pg14_database.sql( + f"select count(*) from public.credit_transactions where user_id = '{user_id}'::uuid" + ) == "2"