diff --git a/jarca_ledger_mcp/application/assets_service.py b/jarca_ledger_mcp/application/assets_service.py index f067daa..f2ecb7b 100644 --- a/jarca_ledger_mcp/application/assets_service.py +++ b/jarca_ledger_mcp/application/assets_service.py @@ -694,7 +694,7 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str except (TypeError, json.JSONDecodeError): stored_fingerprint = {"authority": existing_run["request_fingerprint"], "request": ""} latest_basis_run = conn.execute( - "SELECT id, correction_of_run_id FROM asset_snapshot_runs WHERE basis_date=? ORDER BY rowid DESC LIMIT 1", + "SELECT id, correction_of_run_id, request_fingerprint FROM asset_snapshot_runs WHERE basis_date=? ORDER BY rowid DESC LIMIT 1", (basis,), ).fetchone() run_in_correction_chain = False @@ -714,9 +714,16 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str if parent_id else None ) + try: + latest_authority_fingerprint = json.loads( + latest_basis_run["request_fingerprint"] + ).get("authority") if latest_basis_run else None + except (TypeError, json.JSONDecodeError, AttributeError): + latest_authority_fingerprint = None immutable_replay_run = bool( run_in_correction_chain and latest_basis_run + and latest_authority_fingerprint == authority_fingerprint and ( latest_basis_run["id"] != existing_run["id"] or existing_run["correction_of_run_id"] diff --git a/tests/test_issue_12_review63_assets.py b/tests/test_issue_12_review63_assets.py index a2ad2d4..7ad8ba4 100644 --- a/tests/test_issue_12_review63_assets.py +++ b/tests/test_issue_12_review63_assets.py @@ -97,22 +97,28 @@ def test_f002_correction_replay_preserves_full_initial_response(tmp_path): def test_f006_initial_replay_survives_multiple_later_corrections(tmp_path): db_path = str(tmp_path / "ledger.sqlite3") service.create_manual_asset( - "현금", 100, "2026-06-30", "cash", db_path=db_path + "현금 A", 100, "2026-06-30", "cash", db_path=db_path ) - asset_id = service.list_manual_assets(db_path=db_path)["manual_assets"][0]["id"] + service.create_manual_asset( + "현금 B", 200, "2026-06-30", "cash", db_path=db_path + ) + assets = { + item["name"]: item["id"] + for item in service.list_manual_assets(db_path=db_path)["manual_assets"] + } first = service.close_asset_snapshot( "2026-06-30", idempotency_key="multi-initial", db_path=db_path ) assert first["success"] is True - service.update_manual_asset(asset_id, valuation_amount=110, db_path=db_path) + service.update_manual_asset(assets["현금 A"], valuation_amount=110, db_path=db_path) first_correction = service.close_asset_snapshot( "2026-06-30", correction_reason="first correction", idempotency_key="multi-correction-1", db_path=db_path, ) assert first_correction["success"] is True - service.update_manual_asset(asset_id, valuation_amount=120, db_path=db_path) + service.update_manual_asset(assets["현금 A"], valuation_amount=120, db_path=db_path) second_correction = service.close_asset_snapshot( "2026-06-30", correction_reason="second correction", idempotency_key="multi-correction-2", db_path=db_path, @@ -124,6 +130,13 @@ def test_f006_initial_replay_survives_multiple_later_corrections(tmp_path): ) assert replay == first + service.update_manual_asset(assets["현금 B"], valuation_amount=220, db_path=db_path) + drifted_replay = service.close_asset_snapshot( + "2026-06-30", idempotency_key="multi-initial", db_path=db_path + ) + assert drifted_replay["success"] is False + assert drifted_replay["error_code"] == "idempotency_conflict" + def test_f003_zero_row_conditional_runs_are_timeseries_authority_with_aliases(tmp_path): db_path = str(tmp_path / "ledger.sqlite3")