from __future__ import annotations import json import sqlite3 import tempfile import unittest from pathlib import Path from project_bus.db import Database from project_bus.operations import OperationsError, backup_database, bootstrap_project, load_manifest class RecordingClient: def __init__(self): self.calls = [] def call(self, name, arguments): self.calls.append((name, arguments)) return {"name": name, "idempotency_key": arguments["idempotency_key"]} class OperationsTests(unittest.TestCase): def manifest(self): return { "project": { "project_id": "sandbox", "name": "Sandbox", "description": "test", }, "actors": [ { "actor_id": "po", "auth_subject": "po", "display_name": "PO", "role": "PO", }, { "actor_id": "reviewer", "auth_subject": "reviewer", "display_name": "Reviewer", "role": "REVIEWER", }, ], } def test_bootstrap_uses_stable_idempotency_keys(self): first = RecordingClient() second = RecordingClient() bootstrap_project(first, self.manifest()) bootstrap_project(second, self.manifest()) self.assertEqual(first.calls, second.calls) self.assertEqual(first.calls[0][0], "create_project") self.assertEqual([call[0] for call in first.calls[1:]], ["register_actor", "register_actor"]) def test_manifest_rejects_system_and_duplicate_subjects(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "manifest.json" invalid = self.manifest() invalid["actors"][0]["role"] = "SYSTEM" path.write_text(json.dumps(invalid), encoding="utf-8") with self.assertRaises(OperationsError): load_manifest(path) invalid = self.manifest() invalid["actors"][1]["auth_subject"] = "po" path.write_text(json.dumps(invalid), encoding="utf-8") with self.assertRaises(OperationsError): load_manifest(path) def test_backup_is_consistent_and_integrity_checked(self): with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "source.db" destination = Path(directory) / "backup.db" Database(source).migrate() with sqlite3.connect(source) as connection: connection.execute( """ INSERT INTO projects(project_id, name, description, created_at) VALUES ('sandbox', 'Sandbox', '', '2026-07-30T00:00:00Z') """ ) connection.execute( """ INSERT INTO actors(actor_id, auth_subject, display_name, created_at) VALUES ('system', 'system', 'System', '2026-07-30T00:00:00Z') """ ) connection.execute( """ INSERT INTO event_log( event_id, project_id, event_type, actor_id, actor_role, aggregate_type, aggregate_id, payload_json, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( "evt_test", "sandbox", "test.created", "system", "SYSTEM", "test", "test-1", "{}", "2026-07-30T00:00:00Z", ), ) receipt = backup_database(source, destination) self.assertEqual(receipt["integrity"], "ok") self.assertEqual(receipt["event_cursor"], 1) with sqlite3.connect(destination) as connection: self.assertEqual( connection.execute("SELECT COUNT(*) FROM event_log").fetchone()[0], 1, ) def test_backup_refuses_in_place_destination(self): with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "source.db" source.touch() with self.assertRaises(OperationsError): backup_database(source, source) if __name__ == "__main__": unittest.main()