fix(kanban): one undecodable task field no longer breaks the whole board listing
A tasks row whose TEXT body holds invalid UTF-8 made sqlite3 raise "Could not decode to UTF-8 column 'body'" inside fetchall, so `hermes kanban list` (and `show`, and every other reader of that row) failed board-wide until the row was deleted by hand; a BLOB-typed body came back as bytes and crashed `--json` (#111743). Fix it once at the connection: every board connection (`_open_configured` and the read-only descendant path in `connect`) installs a lossy text_factory that substitutes U+FFFD, and `Task.from_row` runs BLOB cells through the same helper so a corrupt row renders with replacement characters instead of taking its neighbours down. Fixes #111743
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Invariant: one task row with an undecodable text field cannot take down the
|
||||
board listing (issue #111743).
|
||||
|
||||
A ``body`` stored as TEXT holding invalid UTF-8 made sqlite3 abort the whole
|
||||
``fetchall`` ("Could not decode to UTF-8 column 'body'"), so ``hermes kanban list``
|
||||
failed for every card until the row was deleted by hand; a BLOB-typed body came
|
||||
back as ``bytes`` and crashed ``--json``. Board connections now decode lossily
|
||||
(U+FFFD) and ``Task.from_row`` coerces BLOB cells the same way.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli import kanban_db as kb
|
||||
from hermes_cli import kanban_db_connect as kbc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def board(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
db_path = kb.kanban_db_path(board="default")
|
||||
kb._INITIALIZED_PATHS.discard(str(db_path.resolve()))
|
||||
kb.init_db()
|
||||
return db_path
|
||||
|
||||
|
||||
def test_list_survives_undecodable_and_blob_text_cells(board):
|
||||
with kbc.connect() as conn:
|
||||
good = kb.create_task(conn, title="good card", assignee="coder")
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (id, title, body, assignee, status, priority, created_by, created_at) "
|
||||
"VALUES ('t_text_bad', 'text invalid', CAST(X'FFFEABCD00' AS TEXT), 'coder', 'ready', 0, 'user', 1000)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (id, title, body, assignee, status, priority, created_by, created_at) "
|
||||
"VALUES ('t_blob_bad', 'blob body', X'FFFEABCD00', 'coder', 'ready', 0, 'user', 1000)"
|
||||
)
|
||||
|
||||
with kbc.connect() as conn:
|
||||
tasks = {t.id: t for t in kb.list_tasks(conn, status="ready")}
|
||||
assert {good, "t_text_bad", "t_blob_bad"} <= set(tasks)
|
||||
for tid in ("t_text_bad", "t_blob_bad"):
|
||||
body = tasks[tid].body
|
||||
assert isinstance(body, str) and "\ufffd" in body
|
||||
json.dumps(dataclasses.asdict(tasks[tid])) # the --json path
|
||||
assert kb.get_task(conn, "t_text_bad").title == "text invalid"
|
||||
Reference in New Issue
Block a user