fix(kanban): clear _INITIALIZED_PATHS in remove_board so recycled DBs re-init schema

Archiving or deleting a board via remove_board() leaves the path's
"schema already initialized" entry in the module-level cache. A
concurrent connect(board=<slug>) call (e.g. the dashboard event-stream
poll loop) then:

  1. resolves the same kanban.db path,
  2. recreates the directory + an empty sqlite file because
     connect() does mkdir(parents=True, exist_ok=True),
  3. skips the CREATE TABLE pass because the cache entry says the
     schema is already in place,
  4. errors on the next read with `no such table: task_events`.

Drop the cache entry before mutating the filesystem so the fresh file
gets a proper schema init on next connect(). Applies to both
archive=True (rename) and archive=False (rmtree) branches.

Fixes #23833.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
briandevans
2026-05-18 20:13:11 -07:00
committed by Teknium
parent 028bbc5425
commit d62964cdfa
2 changed files with 36 additions and 0 deletions
+5
View File
@@ -534,6 +534,11 @@ def remove_board(slug: str, *, archive: bool = True) -> dict:
if get_current_board() == normed:
clear_current_board()
# A concurrent connect(board=normed) after the rename/delete recreates
# an empty sqlite file via mkdir(exist_ok=True); the cache entry must be
# dropped first so the schema init pass re-runs on that fresh file.
_INITIALIZED_PATHS.discard(str((d / "kanban.db").resolve()))
if archive:
archive_root = boards_root() / "_archived"
archive_root.mkdir(parents=True, exist_ok=True)
+31
View File
@@ -258,6 +258,37 @@ class TestBoardCRUD:
kb.remove_board("pinned")
assert kb.get_current_board() == "default"
@pytest.mark.parametrize("archive", [True, False])
def test_remove_clears_init_cache_for_recreated_db(self, fresh_home, archive):
# Regression for #23833: poll loops that call connect(board=slug) right
# after remove_board() recreate an empty kanban.db at the same path
# (connect() does mkdir(exist_ok=True)). If _INITIALIZED_PATHS still
# contains the resolved path, the CREATE TABLE pass is skipped and
# downstream readers hit `no such table: task_events`.
kb.create_board("recycle")
# First connect populates _INITIALIZED_PATHS for this DB.
with kb.connect(board="recycle") as conn:
kb.create_task(conn, title="t1", assignee="dev")
db_path = kb.board_dir("recycle") / "kanban.db"
assert str(db_path.resolve()) in kb._INITIALIZED_PATHS
kb.remove_board("recycle", archive=archive)
# remove_board must drop the cache entry so a re-create through
# connect() gets a fresh schema-init pass.
assert str(db_path.resolve()) not in kb._INITIALIZED_PATHS
# Simulate the event-stream poll: re-open the same slug. connect()
# recreates the directory + empty .db; the schema must be re-applied.
with kb.connect(board="recycle") as conn:
tables = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
}
assert "task_events" in tables
assert "tasks" in tables
def test_rename_updates_metadata(self, fresh_home):
kb.create_board("slug-immutable")
kb.write_board_metadata("slug-immutable", name="New Display Name")