feat: add ServiceError and detached-safe session factory

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 19:05:40 -04:00
parent 8870b8f3ef
commit 5109609b8d
3 changed files with 25 additions and 1 deletions
+4 -1
View File
@@ -11,7 +11,10 @@ def get_engine(settings: Settings) -> Engine:
def get_session_factory(engine: Engine) -> sessionmaker: def get_session_factory(engine: Engine) -> sessionmaker:
return sessionmaker(bind=engine) # expire_on_commit=False: services open a session per operation and return
# ORM rows to the TUI after the session closes; loaded attributes must
# stay readable on those detached instances.
return sessionmaker(bind=engine, expire_on_commit=False)
def ensure_db(settings: Settings) -> None: def ensure_db(settings: Settings) -> None:
+8
View File
@@ -0,0 +1,8 @@
class ServiceError(Exception):
"""Expected, user-facing failure in a service operation.
Raised for recoverable conditions: missing DB rows (project/session
deleted underneath an action), harnesses no longer installed, duplicate
project paths. The TUI catches this and shows a notification instead of
letting the worker crash.
"""
+13
View File
@@ -123,3 +123,16 @@ def test_pending_migrations_applied_in_order_once(tmp_path, monkeypatch):
assert _user_version(engine) == 3 assert _user_version(engine) == 3
migrate(engine) # re-run is a no-op migrate(engine) # re-run is a no-op
assert applied == [2, 3] assert applied == [2, 3]
def test_rows_stay_readable_after_session_closes(tmp_path):
# Services use per-operation sessions; rows they return must remain
# readable after the originating session closes (expire_on_commit=False).
settings = _tmp_settings(tmp_path)
ensure_db(settings)
factory = get_session_factory(get_engine(settings))
with factory() as session:
p = Project(name="x", path="/x")
session.add(p)
session.commit()
assert p.name == "x" # would raise DetachedInstanceError without the flag