@@ -421,3 +421,78 @@ def test_returns_none_on_redis_error(self):
421421
422422 result = mod .read_verified_balance (mock_redis )
423423 assert result is None
424+
425+
426+ # ---------------------------------------------------------------------------
427+ # GcsLedgerProvider / ObjectStoreLedgerProvider — zero-balance mapping
428+ #
429+ # A drained (0.0) account in a multi-account snapshot must be reported as 0.0,
430+ # not silently replaced by the top-level "balance" fallback. The barrier the
431+ # reconciler feeds, h(x) = cash - min_cash, relies on the real balance; a
432+ # masked zero inflates it and lets trades clear against an empty account.
433+ # ---------------------------------------------------------------------------
434+
435+
436+ class TestLedgerProviderZeroBalanceMapping :
437+ """Ledger providers must not treat a present 0.0 balance as absent."""
438+
439+ def _gcs_client_for (self , snapshot : dict ) -> MagicMock :
440+ blob = MagicMock ()
441+ blob .download_as_text .return_value = json .dumps (snapshot )
442+ bucket = MagicMock ()
443+ bucket .blob .return_value = blob
444+ client = MagicMock ()
445+ client .bucket .return_value = bucket
446+ return client
447+
448+ def _s3_provider_with_snapshot (self , mod , monkeypatch , snapshot : dict ):
449+ monkeypatch .setenv ("S3_RECONCILIATION_BUCKET" , "cage-ledger" )
450+ body = MagicMock ()
451+ body .read .return_value = json .dumps (snapshot ).encode ("utf-8" )
452+ client = MagicMock ()
453+ client .get_object .return_value = {"Body" : body }
454+ provider = mod .ObjectStoreLedgerProvider ()
455+ monkeypatch .setattr (provider , "_make_client" , lambda : client )
456+ return provider
457+
458+ def test_gcs_present_zero_balance_not_masked (self , monkeypatch ):
459+ """A 0.0 target balance must survive even when a top-level fallback exists."""
460+ mod = _get_module ()
461+ monkeypatch .setenv ("GCS_RECONCILIATION_BUCKET" , "cage-ledger" )
462+ snapshot = {"balances" : {"acct1" : 0.0 }, "balance" : 100_000.0 }
463+ client = self ._gcs_client_for (snapshot )
464+ provider = mod .GcsLedgerProvider ()
465+ with patch ("google.cloud.storage.Client" , return_value = client ):
466+ result = provider .fetch_balance ("acct1" )
467+ assert result .balance_usd == 0.0
468+
469+ def test_s3_present_zero_balance_not_masked (self , monkeypatch ):
470+ """Same containment for the S3-compatible provider."""
471+ mod = _get_module ()
472+ snapshot = {"balances" : {"acct1" : 0.0 }, "balance" : 100_000.0 }
473+ provider = self ._s3_provider_with_snapshot (mod , monkeypatch , snapshot )
474+
475+ result = provider .fetch_balance ("acct1" )
476+ assert result .balance_usd == 0.0
477+
478+ def test_gcs_nonzero_balance_preferred_over_fallback (self , monkeypatch ):
479+ """A real per-account balance still wins over the top-level fallback."""
480+ mod = _get_module ()
481+ monkeypatch .setenv ("GCS_RECONCILIATION_BUCKET" , "cage-ledger" )
482+ snapshot = {"balances" : {"acct1" : 4_200.0 }, "balance" : 100_000.0 }
483+ client = self ._gcs_client_for (snapshot )
484+ provider = mod .GcsLedgerProvider ()
485+ with patch ("google.cloud.storage.Client" , return_value = client ):
486+ result = provider .fetch_balance ("acct1" )
487+ assert result .balance_usd == 4_200.0
488+
489+ def test_gcs_absent_account_falls_back_to_top_level_balance (self , monkeypatch ):
490+ """When the account is absent, the top-level balance is still used."""
491+ mod = _get_module ()
492+ monkeypatch .setenv ("GCS_RECONCILIATION_BUCKET" , "cage-ledger" )
493+ snapshot = {"balances" : {}, "balance" : 100_000.0 }
494+ client = self ._gcs_client_for (snapshot )
495+ provider = mod .GcsLedgerProvider ()
496+ with patch ("google.cloud.storage.Client" , return_value = client ):
497+ result = provider .fetch_balance ("acct1" )
498+ assert result .balance_usd == 100_000.0
0 commit comments