test(req-001): tighten AC coverage and remove markdown trailing whitespace

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-09 12:20:11 +00:00
committed by GitHub
co-authored by genichin
parent 64975033e6
commit 7aa3de524a
4 changed files with 186 additions and 50 deletions
+47 -12
View File
@@ -3,71 +3,106 @@
Test ID: TC-REQ-001-001 Test ID: TC-REQ-001-001
Verifies: Verifies:
- REQ-001.01-AC01 - REQ-001.01-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_tax_defaults_to_zero_when_omitted` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_tax_defaults_to_zero_when_omitted
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_tax_omitted_persists_zero_on_real_postgres
Automated: true Automated: true
Test ID: TC-REQ-001-002 Test ID: TC-REQ-001-002
Verifies: Verifies:
- REQ-001.01-AC02 - REQ-001.01-AC02
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_requires_executed_at_and_rejects_fee_field`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_sub_minimum_numeric_precision`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_amount_exceeding_numeric_20_8` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_requires_executed_at_and_rejects_fee_field
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_sub_minimum_numeric_precision
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_amount_exceeding_numeric_20_8
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_rejects_invalid_amount_tax_and_unrelated_fields_without_state_change
Automated: true Automated: true
Test ID: TC-REQ-001-003 Test ID: TC-REQ-001-003
Verifies: Verifies:
- REQ-001.02-AC01 - REQ-001.02-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_tax_defaults_to_zero_when_omitted`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_tax_defaults_to_zero_when_omitted
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres
Automated: true Automated: true
Test ID: TC-REQ-001-004 Test ID: TC-REQ-001-004
Verifies: Verifies:
- REQ-001.03-AC01 - REQ-001.03-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_executed_at_uses_utc_midnight_independent_of_server_timezone` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_executed_at_uses_utc_midnight_independent_of_server_timezone
Automated: true Automated: true
Test ID: TC-REQ-001-005 Test ID: TC-REQ-001-005
Verifies: Verifies:
- REQ-001.04-AC01 - REQ-001.04-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_includes_dividend_fields`, `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_openapi_contains_nullable_amount` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_includes_dividend_fields
- tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_openapi_contains_nullable_amount
Automated: true Automated: true
Test ID: TC-REQ-001-006 Test ID: TC-REQ-001-006
Verifies: Verifies:
- REQ-001.05-AC01 - REQ-001.05-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate
Automated: true Automated: true
Test ID: TC-REQ-001-007 Test ID: TC-REQ-001-007
Verifies: Verifies:
- REQ-001.05-AC02 - REQ-001.05-AC02
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation`, `tests/test_v1_portfolios_manual_ledger.py::test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_requires_confirmation
- tests/test_v1_portfolios_manual_ledger.py::test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching
Automated: true Automated: true
Test ID: TC-REQ-001-008 Test ID: TC-REQ-001-008
Verifies: Verifies:
- REQ-001.05-AC03 - REQ-001.05-AC03
Test reference: `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_cold_repositories_concurrent_dividend_requests_follow_duplicate_contract` Test reference:
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_cold_repositories_concurrent_dividend_requests_follow_duplicate_contract
Automated: true Automated: true
Test ID: TC-REQ-001-009 Test ID: TC-REQ-001-009
Verifies: Verifies:
- REQ-001.06-AC01 - REQ-001.06-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_decreases_profit_buffer`, `tests/test_v1_portfolios_manual_ledger.py::test_duplicate_reversal_is_rejected_without_state_change`, `tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_insufficient_buffer_is_rejected_without_state_change`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_decreases_profit_buffer
- tests/test_v1_portfolios_manual_ledger.py::test_duplicate_reversal_is_rejected_without_state_change
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_reversal_insufficient_buffer_is_rejected_without_state_change
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres
Automated: true Automated: true
Test ID: TC-REQ-001-010 Test ID: TC-REQ-001-010
Verifies: Verifies:
- REQ-001.07-AC01 - REQ-001.07-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_existing_buffer_plus_min_dividend_is_rejected_before_snapshot_divergence` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_duplicate_check_does_not_collapse_decimals_that_can_alias_in_float
- tests/test_v1_portfolios_manual_ledger.py::test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_precision_and_reversal_round_trip_on_real_postgres
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_existing_buffer_plus_min_dividend_is_rejected_before_snapshot_divergence
Automated: true Automated: true
Test ID: TC-REQ-001-011 Test ID: TC-REQ-001-011
Verifies: Verifies:
- REQ-001.08-AC01 - REQ-001.08-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_false_is_accepted_for_non_dividend_actions`, `tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_true_is_rejected_for_non_dividend_actions`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_existing_high_precision_snapshot_still_allows_legacy_manual_actions`, `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot_is_safe` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_false_is_accepted_for_non_dividend_actions
- tests/test_v1_portfolios_manual_ledger.py::test_confirm_duplicate_true_is_rejected_for_non_dividend_actions
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_existing_high_precision_snapshot_still_allows_legacy_manual_actions
- tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot_is_safe
Automated: true Automated: true
Test ID: TC-REQ-001-012 Test ID: TC-REQ-001-012
Verifies: Verifies:
- REQ-001.09-AC01 - REQ-001.09-AC01
Test reference: `tests/test_v1_portfolios_manual_ledger.py::test_manual_write_rejects_service_token`, `tests/test_v1_portfolios_manual_ledger.py::test_owner_mismatch_returns_403`, `tests/test_v1_portfolios_manual_ledger.py::test_daemon_managed_requires_confirmation_then_records_evidence`, `tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_message_is_sanitized`, `tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_secret_is_not_rendered_in_logs` Test reference:
- tests/test_v1_portfolios_manual_ledger.py::test_manual_write_rejects_service_token
- tests/test_v1_portfolios_manual_ledger.py::test_owner_mismatch_returns_403
- tests/test_v1_portfolios_manual_ledger.py::test_daemon_managed_requires_confirmation_then_records_evidence
- tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_message_is_sanitized
- tests/test_v1_portfolios_manual_ledger.py::test_manual_ledger_conflict_secret_is_not_rendered_in_logs
Automated: true Automated: true
+25 -7
View File
@@ -3,18 +3,18 @@
## Evidence Batch: local API regression run ## Evidence Batch: local API regression run
Requirement revision: `REQ-001@1.0` Requirement revision: `REQ-001@1.0`
Commit: `1015b465bb64a5cfe9271044d43d079152c81e20` Commit: `pending-local-commit`
Environment: `local sandbox / pytest / fake repository` Environment: `local sandbox / pytest / fake repository`
Executed at: `2026-09-09T12:05:39Z` Executed at: `2026-09-09T12:16:43Z`
Run selector: `pytest -q -o addopts='' tests/test_v1_portfolios_manual_ledger.py` Run selector: `pytest -q -o addopts='' tests/test_v1_portfolios_manual_ledger.py`
Run result: `33 passed` Run result: `35 passed`
Test ID: TC-REQ-001-001
Result: PASS
Test ID: TC-REQ-001-002 Test ID: TC-REQ-001-002
Result: PASS Result: PASS
Test ID: TC-REQ-001-003
Result: PASS
Test ID: TC-REQ-001-004 Test ID: TC-REQ-001-004
Result: PASS Result: PASS
@@ -27,6 +27,12 @@ Result: PASS
Test ID: TC-REQ-001-007 Test ID: TC-REQ-001-007
Result: PASS Result: PASS
Test ID: TC-REQ-001-009
Result: PASS
Test ID: TC-REQ-001-010
Result: PASS
Test ID: TC-REQ-001-012 Test ID: TC-REQ-001-012
Result: PASS Result: PASS
@@ -37,7 +43,7 @@ Commit: `f193aa6da33428783b91c1144fc8b9de5b3fb4bb`
Environment: `disposable PostgreSQL / independent re-review` Environment: `disposable PostgreSQL / independent re-review`
Executed at: `2026-09-09T12:05:39Z` Executed at: `2026-09-09T12:05:39Z`
Run selector: `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py (module)` Run selector: `tests/test_bat025_portfolio_ledger_manual_metadata_migration.py (module)`
Run result: `7 passed, 2 failed` (`2 failed`는 기존 stale migration-head baseline) Run result: `7 passed, 2 failed` (`2 failed` are pre-existing stale migration-head baseline)
Test ID: TC-REQ-001-003 Test ID: TC-REQ-001-003
Result: PASS Result: PASS
@@ -53,3 +59,15 @@ Result: PASS
Test ID: TC-REQ-001-011 Test ID: TC-REQ-001-011
Result: PASS Result: PASS
## Evidence Batch: local real PostgreSQL availability
Requirement revision: `REQ-001@1.0`
Commit: `pending-local-commit`
Environment: `local sandbox / PostgreSQL DSN not configured`
Executed at: `2026-09-09T12:16:43Z`
Run selector: `pytest -q -o addopts='' tests/test_bat025_portfolio_ledger_manual_metadata_migration.py::test_dividend_tax_omitted_persists_zero_on_real_postgres`
Run result: `1 skipped`
Test ID: TC-REQ-001-001
Result: BLOCKED
@@ -313,6 +313,28 @@ def test_dividend_precision_and_reversal_round_trip_on_real_postgres(
repo.close() repo.close()
def test_dividend_tax_omitted_persists_zero_on_real_postgres(
isolated_settings: Settings,
) -> None:
"""Omitted tax should persist as zero in PostgreSQL."""
upgrade_to_head(isolated_settings)
repo = _repo(isolated_settings)
portfolio_id = _seed_portfolio(repo, "dividend-tax-omitted")
dividend = repo.record_manual_ledger(
portfolio_id,
action="DIVIDEND",
amount=Decimal("1.00000000"),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
note="tax-omitted",
)
assert dividend.tax_amount == 0.0
with repo._connect().cursor() as cur:
cur.execute("SELECT tax_amount::text FROM portfolio_ledger WHERE ledger_entry_id = %s", (dividend.ledger_entry_id,))
assert cur.fetchone()[0] == "0.00000000"
repo.close()
def test_existing_high_precision_snapshot_still_allows_legacy_manual_actions( def test_existing_high_precision_snapshot_still_allows_legacy_manual_actions(
isolated_settings: Settings, isolated_settings: Settings,
) -> None: ) -> None:
+63 -2
View File
@@ -6,6 +6,7 @@ import os
import time import time
from contextlib import ExitStack from contextlib import ExitStack
from datetime import UTC, datetime from datetime import UTC, datetime
from decimal import Decimal
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@@ -71,6 +72,7 @@ class _FakeRepo:
self.principal = principal or _token_principal() self.principal = principal or _token_principal()
self.entries: list[LedgerEntry] = [] self.entries: list[LedgerEntry] = []
self._next_id = 1 self._next_id = 1
self._dividend_amounts: dict[int, Decimal] = {}
def close(self) -> None: def close(self) -> None:
pass pass
@@ -86,7 +88,9 @@ class _FakeRepo:
def record_manual_ledger(self, portfolio_id: str, **kwargs: Any) -> LedgerEntry: def record_manual_ledger(self, portfolio_id: str, **kwargs: Any) -> LedgerEntry:
self.get(portfolio_id) self.get(portfolio_id)
action = kwargs["action"] action = kwargs["action"]
amount = float(kwargs.get("amount") or 0.0) amount_raw = kwargs.get("amount")
amount_decimal = Decimal(str(amount_raw if amount_raw is not None else 0))
amount = float(amount_decimal)
quantity = float(kwargs.get("quantity") or 0.0) quantity = float(kwargs.get("quantity") or 0.0)
price = float(kwargs.get("price") or 0.0) price = float(kwargs.get("price") or 0.0)
fee = float(kwargs.get("fee") or 0.0) fee = float(kwargs.get("fee") or 0.0)
@@ -141,7 +145,7 @@ class _FakeRepo:
if e.action == "DIVIDEND" if e.action == "DIVIDEND"
and e.ledger_entry_id not in reversed_ids and e.ledger_entry_id not in reversed_ids
and e.executed_at == executed_at and e.executed_at == executed_at
and abs(float(e.price) - amount) < 1e-9 and self._dividend_amounts.get(e.ledger_entry_id, Decimal(str(e.price))) == amount_decimal
and e.note == note and e.note == note
), ),
None, None,
@@ -211,6 +215,8 @@ class _FakeRepo:
) )
self._next_id += 1 self._next_id += 1
self.entries.append(entry) self.entries.append(entry)
if action == "DIVIDEND":
self._dividend_amounts[entry.ledger_entry_id] = amount_decimal
return entry return entry
def list_manual_ledger_entries(self, portfolio_id: str, limit: int = 50) -> list[LedgerEntry]: def list_manual_ledger_entries(self, portfolio_id: str, limit: int = 50) -> list[LedgerEntry]:
@@ -576,6 +582,42 @@ def test_dividend_requires_executed_at_and_rejects_fee_field() -> None:
assert invalid_fee.status_code == 422 assert invalid_fee.status_code == 422
def test_dividend_rejects_invalid_amount_tax_and_unrelated_fields_without_state_change() -> None:
repo = _FakeRepo(_portfolio(cash=1000, quantity=10, price=10, buffer=500))
client = _make_client(repo)
baseline = (
repo.portfolio.cash_balance,
repo.portfolio.asset_quantity,
repo.portfolio.profit_buffer,
len(repo.entries),
)
invalid_payloads = [
{"action": "DIVIDEND", "amount": 0, "executed_at": "2026-09-01"},
{"action": "DIVIDEND", "amount": -1, "executed_at": "2026-09-01"},
{"action": "DIVIDEND", "amount": 1, "tax": -1, "executed_at": "2026-09-01"},
{"action": "DIVIDEND", "amount": 1, "tax": "0.000000001", "executed_at": "2026-09-01"},
{"action": "DIVIDEND", "amount": 1, "tax": "1000000000000.00000000", "executed_at": "2026-09-01"},
{"action": "DIVIDEND", "amount": 1, "executed_at": "2026-09-01", "quantity": 1},
{"action": "DIVIDEND", "amount": 1, "executed_at": "2026-09-01", "price": 1},
{"action": "DIVIDEND", "amount": 1, "executed_at": "2026-09-01", "target_ledger_entry_id": 1},
]
for payload in invalid_payloads:
resp = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
json=payload,
headers=_auth(),
)
assert resp.status_code == 422
assert (
repo.portfolio.cash_balance,
repo.portfolio.asset_quantity,
repo.portfolio.profit_buffer,
len(repo.entries),
) == baseline
def test_dividend_rejects_sub_minimum_numeric_precision() -> None: def test_dividend_rejects_sub_minimum_numeric_precision() -> None:
repo = _FakeRepo(_portfolio()) repo = _FakeRepo(_portfolio())
client = _make_client(repo) client = _make_client(repo)
@@ -653,6 +695,25 @@ def test_dividend_with_different_decimal_amount_is_not_treated_as_duplicate() ->
assert second.status_code == 201 assert second.status_code == 201
def test_dividend_duplicate_check_does_not_collapse_decimals_that_can_alias_in_float() -> None:
repo = _FakeRepo(_portfolio())
client = _make_client(repo)
first = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
json={"action": "DIVIDEND", "amount": "90000000.00000001", "executed_at": "2026-09-01", "note": "Q3"},
headers=_auth(),
)
assert first.status_code == 201
second = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
json={"action": "DIVIDEND", "amount": "90000000.00000002", "executed_at": "2026-09-01", "note": "Q3"},
headers=_auth(),
)
assert second.status_code == 201
def test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching() -> None: def test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching() -> None:
repo = _FakeRepo(_portfolio()) repo = _FakeRepo(_portfolio())
client = _make_client(repo) client = _make_client(repo)