feat: add Session.sandbox_json column and migration
Adds a nullable sandbox_json TEXT column to the sessions table via a new version-3 migration. Updates LATEST_VERSION to 3 and adjusts existing migration tests to account for the new column and version number. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,13 @@ def _migrate_v2(conn: Connection) -> None:
|
||||
conn.exec_driver_sql("ALTER TABLE sessions ADD COLUMN worktree_branch TEXT")
|
||||
|
||||
|
||||
def _migrate_v3(conn: Connection) -> None:
|
||||
conn.exec_driver_sql("ALTER TABLE sessions ADD COLUMN sandbox_json TEXT")
|
||||
|
||||
|
||||
MIGRATIONS: list[tuple[int, Callable[[Connection], None]]] = [
|
||||
(2, _migrate_v2),
|
||||
(3, _migrate_v3),
|
||||
]
|
||||
|
||||
BASELINE_VERSION = 1
|
||||
|
||||
@@ -38,6 +38,7 @@ class Session(Base):
|
||||
tmux_session_name: Mapped[str] = mapped_column(unique=True)
|
||||
harness_session_id: Mapped[str | None] = mapped_column(default=None)
|
||||
model: Mapped[str | None] = mapped_column(default=None)
|
||||
sandbox_json: Mapped[str | None] = mapped_column(default=None)
|
||||
worktree_path: Mapped[str | None] = mapped_column(default=None)
|
||||
worktree_branch: Mapped[str | None] = mapped_column(default=None)
|
||||
created_at: Mapped[datetime] = mapped_column(insert_default=func.now())
|
||||
|
||||
+54
-9
@@ -97,6 +97,7 @@ def test_unversioned_existing_db_stamped_latest(tmp_path):
|
||||
with engine.begin() as conn:
|
||||
conn.exec_driver_sql("ALTER TABLE sessions DROP COLUMN worktree_path")
|
||||
conn.exec_driver_sql("ALTER TABLE sessions DROP COLUMN worktree_branch")
|
||||
conn.exec_driver_sql("ALTER TABLE sessions DROP COLUMN sandbox_json")
|
||||
migrate(engine)
|
||||
assert _user_version(engine) == migrations.LATEST_VERSION
|
||||
|
||||
@@ -113,7 +114,7 @@ def test_db_from_newer_hqt_raises(tmp_path):
|
||||
|
||||
def test_pending_migrations_applied_in_order_once(tmp_path, monkeypatch):
|
||||
settings = _tmp_settings(tmp_path)
|
||||
ensure_db(settings) # stamps migrations.LATEST_VERSION (currently 2)
|
||||
ensure_db(settings) # stamps migrations.LATEST_VERSION (currently 3)
|
||||
applied: list[int] = []
|
||||
# Use versions above current LATEST_VERSION so none have been applied yet.
|
||||
monkeypatch.setattr(
|
||||
@@ -121,17 +122,17 @@ def test_pending_migrations_applied_in_order_once(tmp_path, monkeypatch):
|
||||
"MIGRATIONS",
|
||||
[
|
||||
*migrations.MIGRATIONS,
|
||||
(3, lambda conn: applied.append(3)),
|
||||
(4, lambda conn: applied.append(4)),
|
||||
(5, lambda conn: applied.append(5)),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(migrations, "LATEST_VERSION", 4)
|
||||
monkeypatch.setattr(migrations, "LATEST_VERSION", 5)
|
||||
engine = get_engine(settings)
|
||||
migrate(engine)
|
||||
assert applied == [3, 4]
|
||||
assert _user_version(engine) == 4
|
||||
assert applied == [4, 5]
|
||||
assert _user_version(engine) == 5
|
||||
migrate(engine) # re-run is a no-op
|
||||
assert applied == [3, 4]
|
||||
assert applied == [4, 5]
|
||||
|
||||
|
||||
def _session_column_names(engine) -> list[str]:
|
||||
@@ -147,11 +148,11 @@ def test_fresh_db_has_worktree_columns(tmp_path):
|
||||
cols = _session_column_names(engine)
|
||||
assert "worktree_path" in cols
|
||||
assert "worktree_branch" in cols
|
||||
assert _user_version(engine) == 2
|
||||
assert _user_version(engine) == 3
|
||||
|
||||
|
||||
def test_v1_db_upgraded_to_v2(tmp_path):
|
||||
# Build a v1 database: create all tables, drop the two new columns, stamp v1.
|
||||
# Build a v1 database: create all tables, drop v2+ columns, stamp v1.
|
||||
settings = _tmp_settings(tmp_path)
|
||||
settings.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
engine = get_engine(settings)
|
||||
@@ -159,6 +160,7 @@ def test_v1_db_upgraded_to_v2(tmp_path):
|
||||
with engine.begin() as conn:
|
||||
conn.exec_driver_sql("ALTER TABLE sessions DROP COLUMN worktree_path")
|
||||
conn.exec_driver_sql("ALTER TABLE sessions DROP COLUMN worktree_branch")
|
||||
conn.exec_driver_sql("ALTER TABLE sessions DROP COLUMN sandbox_json")
|
||||
conn.exec_driver_sql("PRAGMA user_version = 1")
|
||||
|
||||
migrate(engine)
|
||||
@@ -166,7 +168,8 @@ def test_v1_db_upgraded_to_v2(tmp_path):
|
||||
cols = _session_column_names(engine)
|
||||
assert "worktree_path" in cols
|
||||
assert "worktree_branch" in cols
|
||||
assert _user_version(engine) == 2
|
||||
assert "sandbox_json" in cols
|
||||
assert _user_version(engine) == 3
|
||||
|
||||
|
||||
def test_session_worktree_fields_default_none(tmp_path):
|
||||
@@ -215,3 +218,45 @@ def test_rows_stay_readable_after_session_closes(tmp_path):
|
||||
session.add(p)
|
||||
session.commit()
|
||||
assert p.name == "x" # would raise DetachedInstanceError without the flag
|
||||
|
||||
|
||||
def test_latest_version_is_three():
|
||||
assert migrations.LATEST_VERSION == 3
|
||||
|
||||
|
||||
def test_migration_adds_sandbox_json_column(tmp_path):
|
||||
# Simulate a baseline DB whose sessions table predates the column.
|
||||
settings = _tmp_settings(tmp_path)
|
||||
settings.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
engine = get_engine(settings)
|
||||
with engine.begin() as conn:
|
||||
conn.exec_driver_sql(
|
||||
"CREATE TABLE sessions ("
|
||||
"id INTEGER PRIMARY KEY, project_id INTEGER, harness_id INTEGER, "
|
||||
"tmux_session_name TEXT)"
|
||||
)
|
||||
conn.exec_driver_sql("PRAGMA user_version = 2")
|
||||
migrate(engine)
|
||||
cols = [c["name"] for c in inspect(engine).get_columns("sessions")]
|
||||
assert "sandbox_json" in cols
|
||||
assert _user_version(engine) == 3
|
||||
|
||||
|
||||
def test_session_round_trips_sandbox_json(tmp_path):
|
||||
settings = _tmp_settings(tmp_path)
|
||||
ensure_db(settings)
|
||||
factory = get_session_factory(get_engine(settings))
|
||||
with factory() as session:
|
||||
p = Project(name="proj", path="/tmp/proj-sandbox")
|
||||
h = Harness(name="claude-sandbox")
|
||||
session.add_all([p, h])
|
||||
session.commit()
|
||||
s = Session(
|
||||
project_id=p.id,
|
||||
harness_id=h.id,
|
||||
tmux_session_name="hqt-9",
|
||||
sandbox_json='{"fs": "rw", "net": true}',
|
||||
)
|
||||
session.add(s)
|
||||
session.commit()
|
||||
assert session.get(Session, s.id).sandbox_json == '{"fs": "rw", "net": true}'
|
||||
|
||||
Reference in New Issue
Block a user