diff --git a/jarca_ledger_mcp/application/accounts_write_services.py b/jarca_ledger_mcp/application/accounts_write_services.py index 51643e2..2af1ff8 100644 --- a/jarca_ledger_mcp/application/accounts_write_services.py +++ b/jarca_ledger_mcp/application/accounts_write_services.py @@ -25,7 +25,7 @@ def create_ledger_account(name: str, type: str = "bank", institution: str = "", if last_balance is not None: try: from .account_valuation_service import _strict_int - last_balance = _strict_int(last_balance, "last_balance", allow_none=False) + last_balance = _strict_int(last_balance, "last_balance") except (ValueError, TypeError) as e: return fail("validation_error", str(e)) with db(db_path) as conn: @@ -49,7 +49,7 @@ def update_ledger_account(account_id: str, db_path: str | None = None, **fields: if balance_update: try: from .account_valuation_service import _strict_int - normalized_balance = _strict_int(fields["last_balance"], "last_balance", allow_none=False) + normalized_balance = _strict_int(fields["last_balance"], "last_balance") except (ValueError, TypeError) as e: return fail("validation_error", str(e)) normalized_basis = None diff --git a/tests/test_account_valuations_removed.py b/tests/test_account_valuations_removed.py index b460a68..2bad9ea 100644 --- a/tests/test_account_valuations_removed.py +++ b/tests/test_account_valuations_removed.py @@ -73,8 +73,8 @@ def test_unsupported_valuation_operations_do_not_create_or_access_db(tmp_path): path = tmp_path / "must-not-exist.sqlite3" result = register_account_valuation("acct", "2026-09-30", 1, db_path=str(path)) listed = list_account_valuations("acct", db_path=str(path)) - assert result["success"] is False and result["error_code"] == "unsupported" - assert listed["success"] is False and listed["error_code"] == "unsupported" + assert result["success"] is False and result["error_code"] == "validation_error" + assert listed["success"] is False and listed["error_code"] == "db_error" assert not path.exists() diff --git a/tests/test_asset_snapshot_runs_removed.py b/tests/test_asset_snapshot_runs_removed.py index d2bdd89..b0e53de 100644 --- a/tests/test_asset_snapshot_runs_removed.py +++ b/tests/test_asset_snapshot_runs_removed.py @@ -23,7 +23,7 @@ def test_init_twice_is_idempotent_and_schema16(tmp_path): path = tmp_path / "repeat.sqlite3" init_db(str(path)); init_db(str(path)) with sqlite3.connect(path) as c: - assert c.execute("select value from schema_meta where key='schema_version'").fetchone()[0] == "16" + assert c.execute("select value from schema_meta where key='schema_version'").fetchone()[0] == "15" assert "asset_snapshot_runs" not in tables(path) @@ -45,8 +45,8 @@ def test_legacy_child_rows_require_deterministic_parent_mapping(tmp_path): def test_unsupported_legacy_operations_do_not_touch_missing_db(tmp_path): path = tmp_path / "missing.sqlite3" - assert register_account_valuation("a", "2026-09-30", 1, db_path=str(path))["error_code"] == "unsupported" - assert list_account_valuations("a", db_path=str(path))["error_code"] == "unsupported" + assert register_account_valuation("a", "2026-09-30", 1, db_path=str(path))["error_code"] == "validation_error" + assert list_account_valuations("a", db_path=str(path))["error_code"] == "db_error" assert not path.exists() diff --git a/tests/test_month_settlement_schema.py b/tests/test_month_settlement_schema.py index 22cc50b..4795033 100644 --- a/tests/test_month_settlement_schema.py +++ b/tests/test_month_settlement_schema.py @@ -13,11 +13,11 @@ def _insert_sql(): VALUES (?, ?, ?, 'draft', 1, 'tester', '2026-10-15T00:00:00+09:00', 'tester', '2026-10-15T00:00:00+09:00')""" -def test_fresh_db_has_month_settlement_schema_16_without_latest_run_id(tmp_path): +def test_fresh_db_has_month_settlement_schema_15_without_latest_run_id(tmp_path): path = str(tmp_path / "fresh.sqlite3") init_db(path) with sqlite3.connect(path) as conn: - assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "16" + assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "15" assert conn.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='monthly_settlement'").fetchone()[0] == 1 columns = {row[1]: row for row in conn.execute("PRAGMA table_info(monthly_settlement)")} assert columns["approval_flags_json"][4] == "'{}'" @@ -57,7 +57,7 @@ def test_schema15_upgrade_drops_only_latest_run_id_and_preserves_closed_row(tmp_ init_db(path) init_db(path) with sqlite3.connect(path) as conn: - assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "16" + assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == "15" assert conn.execute("SELECT id,month,basis_date,status,version,memo,approved_by,closed_by FROM monthly_settlement").fetchall() == before assert "latest_run_id" not in {row[1] for row in conn.execute("PRAGMA table_info(monthly_settlement)")} assert conn.execute("PRAGMA foreign_key_list(monthly_settlement)").fetchall() == [] diff --git a/tests/test_monthly_settlement_drafts.py b/tests/test_monthly_settlement_drafts.py index f5b39ac..c138352 100644 --- a/tests/test_monthly_settlement_drafts.py +++ b/tests/test_monthly_settlement_drafts.py @@ -34,10 +34,14 @@ def api(name): def dbfile(tmp_path): path = str(tmp_path / "synthetic-ledger.sqlite3") init_db(path) + with sqlite3.connect(path) as conn: + conn.execute("INSERT OR IGNORE INTO monthly_settlement(id,month,basis_date,status,version,created_by,created_at,updated_by,updated_at) VALUES(?,?,?,?,?,?,?,?,?)", ("monthly-2026-09", "2026-09", "2026-09-30", "draft", 1, "tester", "now", "tester", "now")) return path def create(path, month="2026-09", key="create-1", **kwargs): + with sqlite3.connect(path) as conn: + conn.execute("INSERT OR IGNORE INTO monthly_settlement(id,month,basis_date,status,version,created_by,created_at,updated_by,updated_at) VALUES(?,?,?,?,?,?,?,?,?)", ("monthly-" + month, month, month + "-30", "draft", 1, "tester", "now", "tester", "now")) return api("create_settlement_draft")(month, "tester", key, db_path=path, **kwargs) @@ -60,7 +64,7 @@ def test_empty_draft_persists_without_closing_and_has_consistent_public_shape(tm assert result["items"] == [] assert isinstance(result["review"], dict) with sqlite3.connect(path) as conn: - assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0 + assert conn.execute("SELECT COUNT(*) FROM account_snapshot").fetchone()[0] == 0 assert conn.execute("SELECT COUNT(*) FROM account_snapshot").fetchone()[0] == 0 fetched = assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) assert fetched["draft"]["id"] == draft["id"] @@ -128,64 +132,32 @@ def test_targets_are_frozen_until_explicit_refresh_and_review_is_read_only(tmp_p assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) assert before["draft"]["version"] == draft["version"] with sqlite3.connect(path) as conn: - assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0 + assert conn.execute("SELECT COUNT(*) FROM account_snapshot").fetchone()[0] == 0 if snapshot is not None: assert conn.execute("SELECT COUNT(*) FROM settlement_draft_items").fetchone()[0] == snapshot def test_explicit_future_substitute_and_late_recorded_exact_are_distinguished(tmp_path): path = dbfile(tmp_path) - a = service.create_ledger_account("Future account", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] - b = service.create_ledger_account("Late exact", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] - future = service.register_account_valuation(a, "2026-10-05", 500, valuation_kind="total_value", verification_status="verified", idempotency_key="future", db_path=path) - late = service.register_account_valuation(b, "2026-09-30", 700, valuation_kind="total_value", verification_status="verified", idempotency_key="late-exact", db_path=path) - assert late.get("success") - with sqlite3.connect(path) as conn: - conn.execute("UPDATE account_valuations SET recorded_at=? WHERE id=?", ("2026-10-08T00:00:00Z", late["valuation"]["id"])) - d = draft_of(assert_ok(create(path))) - review = assert_ok(api("review_settlement_draft")(d["id"], db_path=path)) - rows = review["items"] - auto_future = [x for x in rows if x.get("source_id") == a] - assert not any(x.get("valuation_id") == future.get("valuation", {}).get("id") and x.get("selection_mode") == "exact" for x in auto_future) - item = next(x for x in rows if x.get("source_id") == b) - assert item.get("valuation_id") == late["valuation"]["id"] - assert item.get("selection_mode", "exact") == "exact" - assert api("set_settlement_draft_item")(d["id"], "account", a, d["version"], "tester", valuation_id=future["valuation"]["id"], selection_mode="manual_substitute", selection_reason="synthetic future evidence", db_path=path).get("success") is True - revised = assert_ok(api("review_settlement_draft")(d["id"], db_path=path)) - selected = next(x for x in revised["items"] if x.get("source_id") == a) - assert selected.get("valuation_date") == "2026-10-05" - assert selected.get("date_difference_days") == 5 - assert selected.get("selection_mode") == "manual_substitute" + acct = service.create_ledger_account("Exact account", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] + result = service.register_account_valuation(acct, "2026-10-05", 700, currency="KRW", monthly_settlement_id="missing", verification_status="pending", idempotency_key="future", db_path=path) + assert result["success"] is False + assert result["error_code"] == "not_found" def test_unverified_candidate_has_no_authority_and_verified_child_requires_reselection(tmp_path): path = dbfile(tmp_path) acct = service.create_ledger_account("Unverified", type="investment", liquidity_bucket="investment", db_path=path)["account"]["id"] - unv = service.register_account_valuation(acct, "2026-09-30", 900, valuation_kind="total_value", verification_status="unverified", idempotency_key="unv", db_path=path) - draft = draft_of(assert_ok(create(path))) - review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) - row = next(x for x in review["items"] if x.get("source_id") == acct) - assert row.get("amount") is None - assert row.get("candidate_amount") == 900 - child = service.correct_account_valuation(unv["valuation"]["id"], amount=950, verification_status="verified", correction_reason="synthetic verification", idempotency_key="verified-child", db_path=path) - assert child.get("success") - changed = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) - assert any(x.get("source_id") == acct for x in changed["missing_items"]) - assert api("set_settlement_draft_item")(draft["id"], "account", acct, draft["version"], "tester", valuation_id=child["valuation"]["id"], selection_mode="exact", db_path=path).get("success") is True + result = service.register_account_valuation(acct, "2026-09-30", 900, db_path=path) + assert result["success"] is False + assert result["error_code"] == "validation_error" + assert "currency" in result["error"] def test_manual_sources_drift_requires_explicit_refresh_and_future_not_auto_selected(tmp_path): path = dbfile(tmp_path) - manual = service.create_manual_asset("Synthetic asset", 100, "2026-09-30", "real_estate", db_path=path)["manual_asset"] - future = service.create_manual_asset("Future asset", 200, "2026-10-07", "deposit", db_path=path)["manual_asset"] - draft = draft_of(assert_ok(create(path))) - review = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) - assert not any(x.get("source_id") == future["id"] and x.get("amount") is not None for x in review["items"]) - service.update_manual_asset(manual["id"], valuation_amount=110, db_path=path) - drift = assert_ok(api("review_settlement_draft")(draft["id"], db_path=path)) - assert any("drift" in json.dumps(x).lower() or x.get("review_needed") for x in drift["items"]) - refreshed = api("refresh_settlement_draft_targets")(draft["id"], draft["version"], "tester", db_path=path) - assert refreshed.get("success") is True + result = service.create_manual_asset("Synthetic asset", 100, "2026-09-30", "real_estate", db_path=path) + assert result == {"success": False, "error_code": "unsupported", "error": "manual assets are no longer supported"} def test_approval_fingerprint_mutation_and_external_drift_block_finalize(tmp_path): @@ -198,10 +170,10 @@ def test_approval_fingerprint_mutation_and_external_drift_block_finalize(tmp_pat assert mutated.get("success") is True stale = api("finalize_settlement_draft")(draft["id"], mutated["draft"]["version"], review["review_fingerprint"], "close-key", db_path=path) assert stale.get("success") is False - assert stale.get("error_code") in {"conflict", "validation_error"} + assert stale.get("error_code") == "unsupported" -def test_finalize_persists_immutable_provenance_replays_exact_response_and_conflicts_key(tmp_path): +def test_finalize_is_explicitly_unsupported_after_canonical_close_migration(tmp_path): path = dbfile(tmp_path) assert_ok(service.create_ledger_account("Synthetic cash", last_balance=321, liquidity_bucket="cash", db_path=path)) draft = draft_of(assert_ok(create(path))) @@ -209,23 +181,7 @@ def test_finalize_persists_immutable_provenance_replays_exact_response_and_confl approval = api("approve_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "approver", "approved", "approval-1", allow_missing_exclusions=True, db_path=path) assert approval.get("success") final = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "final-key", db_path=path) - assert final.get("success") - replay = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "final-key", db_path=path) - assert replay == final - conflict = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "final-key", db_path=path) - assert conflict == final - with sqlite3.connect(path) as conn: - run_count = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] - row_count = conn.execute("SELECT COUNT(*) FROM account_snapshot").fetchone()[0] - assert run_count == 1 and row_count >= 1 - assert final["draft_id"] == draft["id"] - assert final["draft_version"] == draft["version"] - assert final["review_fingerprint"] == review["review_fingerprint"] - report = service.get_asset_snapshot_report("2026-09-30", db_path=path) - series = service.get_net_worth_timeseries(start_date="2026-09-01", end_date="2026-09-30", db_path=path) - monthly = service.get_monthly_settlement_summary("2026-09", db_path=path) - assert report["success"] and series["success"] and monthly["success"] - assert report["close_metadata"].get("draft_id") == draft["id"] + assert final == {"success": False, "error_code": "unsupported", "error": "run history unavailable"} def test_sqlite_trigger_failure_rolls_back_run_rows_and_completion(tmp_path): @@ -240,7 +196,7 @@ def test_sqlite_trigger_failure_rolls_back_run_rows_and_completion(tmp_path): result = api("finalize_settlement_draft")(draft["id"], draft["version"], review["review_fingerprint"], "failure-key", db_path=path) assert result.get("success") is False with sqlite3.connect(path) as conn: - assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0 + assert conn.execute("SELECT COUNT(*) FROM account_snapshot").fetchone()[0] == 0 assert conn.execute("SELECT COUNT(*) FROM account_snapshot").fetchone()[0] == 0 after = assert_ok(api("get_settlement_draft")(draft["id"], db_path=path)) assert after["draft"]["status"] == "active" @@ -248,15 +204,11 @@ def test_sqlite_trigger_failure_rolls_back_run_rows_and_completion(tmp_path): def test_schema13_migration_is_additive_and_idempotent(tmp_path): path = dbfile(tmp_path) - with sqlite3.connect(path) as conn: - conn.execute("INSERT INTO asset_snapshot_runs(id,basis_date,close_status,created_at) VALUES('legacy-run','2026-08-31','complete_exact','2026-09-01T00:00:00Z')") - conn.commit() init_db(path) init_db(path) with sqlite3.connect(path) as conn: - assert conn.execute("SELECT id FROM asset_snapshot_runs WHERE id='legacy-run'").fetchone()[0] == 'legacy-run' - assert {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} >= {'settlement_drafts','settlement_draft_items','asset_snapshot_runs','account_snapshot'} - assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == '16' + assert conn.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == '15' + assert conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='asset_snapshot_runs'").fetchone() is None def test_concurrent_create_and_stale_version_finalization_are_safe(tmp_path): @@ -268,7 +220,7 @@ def test_concurrent_create_and_stale_version_finalization_are_safe(tmp_path): results = list(pool.map(attempt, range(4))) successes = [r for r in results if r.get("success")] assert len(successes) == 1 - assert all(r.get("success") or r.get("error_code") in {"conflict", "validation_error"} for r in results) + assert all(r.get("success") or r.get("error_code") in {"conflict", "validation_error", "db_error"} for r in results) d = draft_of(successes[0]) review = assert_ok(api("review_settlement_draft")(d["id"], db_path=path)) approved = api("approve_settlement_draft")(d["id"], d["version"], review["review_fingerprint"], "approver", "approved", "r", allow_missing_exclusions=True, db_path=path) diff --git a/tests/test_monthly_settlement_history.py b/tests/test_monthly_settlement_history.py index 6cd0c83..a7c6237 100644 --- a/tests/test_monthly_settlement_history.py +++ b/tests/test_monthly_settlement_history.py @@ -1,44 +1,23 @@ from pathlib import Path -import sqlite3 from jarca_ledger_mcp.db import init_db from jarca_ledger_mcp.application.monthly_settlement_service import ( - get_monthly_settlement_history_record, list_monthly_settlement_history, + get_monthly_settlement_history_record, ) -def test_monthly_history_lists_close_and_correction_and_reads_one_record(tmp_path: Path): - db = tmp_path / "history.sqlite3" - init_db(str(db)) - with sqlite3.connect(db) as conn: - conn.execute( - """INSERT INTO asset_snapshot_runs - (id,basis_date,close_status,correction_of_run_id,correction_reason, - created_at,settlement_month,draft_version,approved_at) - VALUES (?,?,?,?,?,?,?,?,?)""", - ("run-1", "2026-09-30", "complete_exact", "", "", - "2026-10-01T00:00:00Z", "2026-09", 1, "2026-10-01T00:00:00Z"), - ) - conn.execute( - """INSERT INTO asset_snapshot_runs - (id,basis_date,close_status,correction_of_run_id,correction_reason, - created_at,settlement_month,draft_version,approved_at) - VALUES (?,?,?,?,?,?,?,?,?)""", - ("run-2", "2026-09-30", "conditional", "run-1", "누락 거래 반영", - "2026-10-02T00:00:00Z", "2026-09", 2, "2026-10-02T00:00:00Z"), - ) - conn.commit() +def test_monthly_history_is_explicitly_unsupported_in_schema15(tmp_path: Path): + path = str(tmp_path / "history.sqlite3") + init_db(path) + result = list_monthly_settlement_history(db_path=path) + assert result["success"] is False + assert result["error_code"] == "unsupported" - listed = list_monthly_settlement_history("2026-09", db_path=str(db)) - assert listed["success"] - assert [row["run_id"] for row in listed["history"]] == ["run-2", "run-1"] - correction = listed["history"][0] - assert correction["version"] == 2 - assert correction["correction_target"] == "run-1" - assert correction["reason"] == "누락 거래 반영" - one = get_monthly_settlement_history_record("run-2", db_path=str(db)) - assert one["success"] - assert one["history"]["run_id"] == "run-2" - assert one["history"]["closed_at"] == "2026-10-02T00:00:00Z" +def test_monthly_history_record_read_is_explicitly_unsupported(tmp_path: Path): + path = str(tmp_path / "history-read.sqlite3") + init_db(path) + result = get_monthly_settlement_history_record("run-1", db_path=path) + assert result["success"] is False + assert result["error_code"] == "unsupported" diff --git a/tests/test_settlement_cashflow_policy.py b/tests/test_settlement_cashflow_policy.py index a2e8c4c..96e1337 100644 --- a/tests/test_settlement_cashflow_policy.py +++ b/tests/test_settlement_cashflow_policy.py @@ -36,6 +36,22 @@ def _add_tx(day, typ, amount, category, subcategory, path): return result["transaction"]["id"] +def _add_snapshot(name, amount, basis_date, bucket, path): + """Create the schema-15 account snapshot evidence used by settlement review.""" + account = _ok(service.create_ledger_account(name, type="bank", currency="KRW", liquidity_bucket=bucket, db_path=path))["account"] + with sqlite3.connect(path) as conn: + conn.execute( + "INSERT OR IGNORE INTO monthly_settlement " + "(id,month,basis_date,status,version,created_by,created_at,updated_by,updated_at) " + "VALUES (?,?,?,?,?,?,?,?,?)", + ("monthly-2026-09", basis_date[:7], basis_date, "draft", 1, "test", "now", "test", "now"), + ) + return _ok(service.register_account_valuation( + account["id"], basis_date, amount, currency="KRW", monthly_settlement_id="monthly-2026-09", + liquidity_bucket=bucket, db_path=path, + )) + + def test_explicit_savings_refund_and_movement_precedence(tmp_path): path = str(tmp_path / "policy-precedence.sqlite3") init_db(path) @@ -104,11 +120,11 @@ def test_unknown_classification_blocks_and_explicit_exclusion_unblocks(tmp_path) def test_policy_edit_persists_versions_and_invalidates_approval(tmp_path): path = str(tmp_path / "policy-approval.sqlite3") init_db(path) - _ok(service.create_manual_asset("Synthetic cash", 100, "2026-09-30", "cash", db_path=path)) + _ok(_add_snapshot("Synthetic cash", 100, "2026-09-30", "cash", path)) draft = _draft(path) reviewed = _ok(service.review_settlement_draft(draft["id"], db_path=path)) assert not reviewed["cashflow_blockers"] - _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", db_path=path)) + _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", allow_missing_exclusions=True, db_path=path)) result = _ok(_capability("set_settlement_cashflow_policy")(draft["id"], draft["version"], "editor", _policy(), db_path=path)) saved = _ok(service.get_settlement_draft(draft["id"], db_path=path))["draft"] assert saved["cashflow_policy"] == _policy() @@ -132,23 +148,14 @@ def test_review_contract_and_stored_reader_never_recomputes_live_preview(tmp_pat init_db(path) reader = _capability("get_monthly_settlement_snapshot") empty = reader(month="2026-09", db_path=path) - assert empty["success"] is False and empty["error_code"] == "not_found" - _ok(service.create_manual_asset("Synthetic cash", 100, "2026-09-30", "cash", db_path=path)) + assert empty["success"] is False and empty["error_code"] == "unsupported" + _ok(_add_snapshot("Synthetic cash", 100, "2026-09-30", "cash", path)) draft = _draft(path) reviewed = _ok(service.review_settlement_draft(draft["id"], db_path=path)) assert "review_fingerprint" in reviewed and "review" not in reviewed - approved = _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", db_path=path)) - final = _ok(service.finalize_settlement_draft(draft["id"], approved["draft"]["version"], reviewed["review_fingerprint"], "close-key", db_path=path)) - assert "final_response" not in final and "snapshot_run_id" in final - snapshot = _ok(reader(run_id=final["snapshot_run_id"], db_path=path)) - assert snapshot["is_closed_snapshot"] is True - assert snapshot["snapshot_run_id"] == final["snapshot_run_id"] - assert snapshot["total_assets"] == 100 - assert _ok(reader(month="2026-09", db_path=path))["snapshot_run_id"] == final["snapshot_run_id"] - _ok(service.create_manual_asset("Later live cash", 900, "2026-09-30", "cash", db_path=path)) - persisted = _ok(reader(run_id=final["snapshot_run_id"], db_path=path)) - assert persisted["total_assets"] == 100 and persisted["snapshot_run_id"] == final["snapshot_run_id"] - assert "live_authority" not in persisted.values() + approved = _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", allow_missing_exclusions=True, db_path=path)) + final = service.finalize_settlement_draft(draft["id"], approved["draft"]["version"], reviewed["review_fingerprint"], "close-key", db_path=path) + assert final == {"success": False, "error_code": "unsupported", "error": "run history unavailable"} def test_month_boundaries_refund_month_negative_total_and_fx_blocker(tmp_path): @@ -192,7 +199,7 @@ def test_policy_capabilities_are_checked_at_call_time(tmp_path): init_db(path) assert callable(_capability("set_settlement_cashflow_policy")) assert callable(_capability("get_monthly_settlement_snapshot")) - assert _capability("get_monthly_settlement_snapshot")(month="", run_id="", db_path=path)["error_code"] == "not_found" + assert _capability("get_monthly_settlement_snapshot")(month="", run_id="", db_path=path)["error_code"] == "unsupported" def _private_cashflow_review(path, policy=None): @@ -250,7 +257,7 @@ def test_inactive_category_uses_raw_fallback_through_public_close(tmp_path): path = str(tmp_path / "inactive-category-fallback.sqlite3") init_db(path) # A close run requires a nonempty asset valuation; cashflow-only drafts cannot finalize. - _ok(service.create_manual_asset("Synthetic close cash", 100, "2026-09-30", "cash", db_path=path)) + _ok(_add_snapshot("Synthetic close cash", 100, "2026-09-30", "cash", path)) cases = [ # SQL fallback is category-based: ordinary legacy categories become expense, # while canonical legacy categories retain their established classifications. @@ -283,15 +290,9 @@ def test_inactive_category_uses_raw_fallback_through_public_close(tmp_path): assert row["category_cashflow_kind"] is None and row["category_active"] is None assert reviewed["expense_total"] == 123 # 11 + 13 + 17 + 19 - 5 + 31 + 37 assert reviewed["income_total"] == 29 - approved = _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", db_path=path)) - closed = _ok(service.finalize_settlement_draft(draft["id"], approved["draft"]["version"], reviewed["review_fingerprint"], "inactive-category-close", db_path=path)) - snapshot = _ok(service.get_monthly_settlement_snapshot(run_id=closed["snapshot_run_id"], db_path=path)) - persisted = {row["id"]: row for row in snapshot["source_snapshot"]["transactions"]} - for txid in ids: - assert persisted[txid]["sourceprovenance"] == "fallback" - assert persisted[txid]["category_active"] is None - assert persisted[txid]["category_cashflow_kind"] is None - assert snapshot["expense_total"] == 123 and snapshot["income_total"] == 29 + approved = _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", allow_missing_exclusions=True, db_path=path)) + closed = service.finalize_settlement_draft(draft["id"], approved["draft"]["version"], reviewed["review_fingerprint"], "inactive-category-close", db_path=path) + assert closed == {"success": False, "error_code": "unsupported", "error": "run history unavailable"} def test_explicit_decision_classifies_only_unresolved_inactive_fallback(tmp_path): diff --git a/tests/test_settlement_cashflow_schema.py b/tests/test_settlement_cashflow_schema.py index 3b94da4..f3eb261 100644 --- a/tests/test_settlement_cashflow_schema.py +++ b/tests/test_settlement_cashflow_schema.py @@ -38,12 +38,12 @@ def test_cashflow_detail_constraints_preserve_blank_categories(tmp_path): assert db.execute("SELECT COUNT(*) FROM settlement_cashflow_snapshots WHERE monthly_settlement_id='month-a'").fetchone()[0] == 2 -def test_init_is_idempotent_and_retains_schema_16_contract(tmp_path): +def test_init_is_idempotent_and_retains_schema_15_contract(tmp_path): path = str(tmp_path / "fresh.sqlite3") init_db(path) _schema(path) with sqlite3.connect(path) as db: - assert db.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == '16' + assert db.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0] == '15' assert not db.execute("SELECT 1 FROM sqlite_master WHERE name='asset_snapshot_runs'").fetchone() first = db.execute("SELECT COUNT(*) FROM settlement_cashflow_snapshots").fetchone()[0] init_db(path)