Files
jarca-ledger-mcp/tests/test_schema_14_to_15_actual.py
T

122 lines
7.1 KiB
Python

from __future__ import annotations
import sqlite3
from pathlib import Path
import pytest
from jarca_ledger_mcp.infrastructure.sqlite import migrations
from jarca_ledger_mcp.infrastructure.sqlite.migrations import init_db
FIXTURE = Path(__file__).parent / "fixtures" / "schema_v14.sql"
def _v14_db(path: Path) -> None:
with sqlite3.connect(path) as db:
db.executescript(FIXTURE.read_text())
db.execute("INSERT INTO accounts VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ("a1", "Main", "bank", "Bank", "KRW", 1, "memo", "1234", "User", 100000, "2026-01-31", "", "ok", "cash", "2026-01-01", "2026-01-31"))
db.execute("INSERT INTO transactions (id,transaction_date,type,amount,account_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)", ("t1", "2026-01-15", "expense", -1200, "a1", "2026-01-15", "2026-01-15"))
db.execute("INSERT INTO asset_snapshots (id,basis_date,source_type,source_id,name,amount,closed_at,created_at) VALUES (?,?,?,?,?,?,?,?)", ("s1", "2026-01-31", "account", "a1", "Main", 100000, "2026-01-31T10:00:00", "2026-01-31T10:00:00"))
db.execute("INSERT INTO asset_snapshot_runs (id,basis_date,close_status,created_at) VALUES (?,?,?,?)", ("run1", "2026-01-31", "closed", "2026-01-31T10:00:00"))
db.execute("INSERT OR REPLACE INTO schema_meta VALUES ('schema_version','14')")
db.commit()
def _rows(path: Path, table: str):
with sqlite3.connect(path) as db:
return db.execute(f"SELECT * FROM {table} ORDER BY 1").fetchall()
def test_actual_v14_fixture_is_tag_ddl_and_migrates_preserving_rows(tmp_path):
path = tmp_path / "ledger.sqlite3"
_v14_db(path)
before_accounts, before_tx = _rows(path, "accounts"), _rows(path, "transactions")
init_db(str(path))
with sqlite3.connect(path) as db:
assert db.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "15"
assert db.execute("SELECT amount, closed_at FROM asset_snapshots WHERE id='s1'").fetchone() == (100000, "2026-01-31T10:00:00")
assert db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='asset_snapshot_runs'").fetchone() is not None
assert db.execute("PRAGMA foreign_key_check").fetchall() == []
assert db.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
assert _rows(path, "accounts") == before_accounts
assert _rows(path, "transactions") == before_tx
def test_actual_v14_migration_is_idempotent_and_snapshot_unchanged(tmp_path):
path = tmp_path / "ledger.sqlite3"
_v14_db(path)
init_db(str(path))
first = _rows(path, "asset_snapshots")
init_db(str(path))
assert _rows(path, "asset_snapshots") == first
def test_migration_failure_rolls_back_legacy_renames_and_metadata(tmp_path, monkeypatch):
path = tmp_path / "ledger.sqlite3"
_v14_db(path)
original = migrations.migrate_db
def fail(conn):
raise RuntimeError("injected")
monkeypatch.setattr(migrations, "migrate_db", fail)
with pytest.raises(RuntimeError, match="injected"):
init_db(str(path))
with sqlite3.connect(path) as db:
assert db.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "14"
assert db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='asset_snapshots'").fetchone()
assert not db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='account_snapshot'").fetchone()
monkeypatch.setattr(migrations, "migrate_db", original)
def test_new_init_has_same_contract(tmp_path):
path = tmp_path / "new.sqlite3"
init_db(str(path))
with sqlite3.connect(path) as db:
assert db.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "15"
cols = {r[1] for r in db.execute("PRAGMA table_info(account_snapshot)")}
assert {"monthly_settlement_id", "verification_status", "evidence_kind"} <= cols
def test_populated_v14_upgrade_preserves_evidence_corrections_and_cashflow(tmp_path):
path = tmp_path / "populated.sqlite3"
_v14_db(path)
with sqlite3.connect(path) as db:
db.execute("INSERT INTO manual_assets(id,name,valuation_amount,created_at,updated_at) VALUES('manual','House',500,'t','t')")
db.execute("INSERT INTO account_valuations(id,account_id,basis_date,amount,recorded_at) VALUES('v1','a1','2026-01-31',100,'t')")
db.execute("INSERT INTO account_valuations(id,account_id,basis_date,amount,recorded_at,supersedes_id,correction_reason) VALUES('v2','a1','2026-01-31',110,'t','v1','correct')")
db.execute("UPDATE asset_snapshots SET snapshot_run_id='run1' WHERE id='s1'")
# A direct asset close can have no settlement_month. A correction shares
# its basis date, but its frozen cashflow must never merge with run1.
db.execute("INSERT INTO asset_snapshot_runs(id,basis_date,close_status,created_at,correction_of_run_id,correction_reason) VALUES('run2','2026-01-31','conditional','t','run1','correct')")
for run, amount in [("run1", 10), ("run2", 20)]:
db.execute("INSERT INTO settlement_cashflow_snapshots VALUES(?,?,'income','','',?,1,'KRW')", ("cf-" + run, run, amount))
db.execute("CREATE INDEX custom_valuation_amount ON account_valuations(amount)")
tables = ("accounts", "transactions", "manual_assets", "account_valuations", "asset_snapshots", "asset_snapshot_runs")
with sqlite3.connect(path) as db:
columns = {table: ",".join(row[1] for row in db.execute(f"PRAGMA table_info({table})")) for table in tables}
before = {table: db.execute(f"SELECT {columns[table]} FROM {table} ORDER BY id").fetchall() for table in tables}
init_db(str(path))
init_db(str(path))
with sqlite3.connect(path) as db:
assert {table: db.execute(f"SELECT {columns[table]} FROM {table} ORDER BY id").fetchall() for table in tables} == before
with sqlite3.connect(path) as db:
assert db.execute("SELECT settlement_run_id,monthly_settlement_id,amount FROM settlement_cashflow_snapshots ORDER BY id").fetchall() == [("run1", None, 10), ("run2", None, 20)]
assert db.execute("SELECT COUNT(*) FROM monthly_settlement").fetchone() == (0,)
assert db.execute("SELECT COUNT(*) FROM account_snapshot").fetchone() == (0,)
assert db.execute("SELECT name FROM sqlite_master WHERE name='custom_valuation_amount'").fetchone()
assert db.execute("PRAGMA integrity_check").fetchone() == ("ok",)
assert db.execute("PRAGMA foreign_key_check").fetchall() == []
def test_orphan_cashflow_migration_rolls_back_without_deleting_legacy_data(tmp_path):
path = tmp_path / "orphan.sqlite3"
_v14_db(path)
with sqlite3.connect(path) as db:
db.execute("INSERT INTO settlement_cashflow_snapshots VALUES('orphan','missing','income','','',1,1,'KRW')")
before = _rows(path, "settlement_cashflow_snapshots")
with pytest.raises(sqlite3.IntegrityError):
init_db(str(path))
assert _rows(path, "settlement_cashflow_snapshots") == before
with sqlite3.connect(path) as db:
assert db.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone() == ("14",)
assert not db.execute("SELECT name FROM sqlite_master WHERE name='account_snapshot'").fetchone()