fix(kanban): BLOB cells in comment/event/run rows degrade like a BLOB task body
Only Task.from_row coerced BLOB-typed cells; a BLOB task_comments.body (or event payload / run summary) still came back as bytes and crashed `hermes kanban show <id> --json` with "Object of type bytes is not JSON serializable". Apply _lossy_text in the other from_row constructors. Test: BLOB comment body and event payload -> str with U+FFFD and JSON-serialisable (red before).
This commit is contained in:
@@ -38,7 +38,8 @@ def _lossy_text(value: Any) -> Any:
|
||||
|
||||
Installed as every board connection's ``text_factory`` (a TEXT cell holding
|
||||
invalid UTF-8 otherwise aborts the whole ``fetchall`` with "Could not decode
|
||||
to UTF-8") and applied to BLOB-typed cells in :meth:`Task.from_row`, so one
|
||||
to UTF-8") and applied to BLOB-typed cells in the ``from_row`` constructors
|
||||
(task, comment, event, run), so one
|
||||
corrupt row degrades to replacement characters instead of taking the board
|
||||
listing down."""
|
||||
if isinstance(value, bytes):
|
||||
@@ -789,7 +790,7 @@ class Run:
|
||||
def from_row(cls, row: sqlite3.Row) -> "Run":
|
||||
return cls(
|
||||
**{
|
||||
col: row[col] for col in (
|
||||
col: _lossy_text(row[col]) for col in (
|
||||
"task_id", "profile", "step_key", "status", "claim_lock", "claim_expires",
|
||||
"worker_pid", "max_runtime_seconds", "last_heartbeat_at", "outcome", "summary", "error",
|
||||
)
|
||||
@@ -797,7 +798,7 @@ class Run:
|
||||
id=int(row["id"]),
|
||||
started_at=int(row["started_at"]),
|
||||
ended_at=_opt_int(row["ended_at"]),
|
||||
metadata=_json_or(row["metadata"]),
|
||||
metadata=_json_or(_lossy_text(row["metadata"])),
|
||||
)
|
||||
|
||||
|
||||
@@ -812,8 +813,8 @@ class Comment:
|
||||
@classmethod
|
||||
def from_row(cls, r: sqlite3.Row) -> "Comment":
|
||||
return cls(
|
||||
id=r["id"], task_id=r["task_id"], author=r["author"],
|
||||
body=r["body"], created_at=r["created_at"],
|
||||
id=r["id"], task_id=r["task_id"], author=_lossy_text(r["author"]),
|
||||
body=_lossy_text(r["body"]), created_at=r["created_at"],
|
||||
)
|
||||
|
||||
|
||||
@@ -852,8 +853,8 @@ class Event:
|
||||
def from_row(cls, row: sqlite3.Row) -> "Event":
|
||||
run_id = _row_get(row, "run_id")
|
||||
return cls(
|
||||
id=row["id"], task_id=row["task_id"], kind=row["kind"],
|
||||
payload=_json_or(row["payload"]), created_at=row["created_at"], run_id=_opt_int(run_id),
|
||||
id=row["id"], task_id=row["task_id"], kind=_lossy_text(row["kind"]),
|
||||
payload=_json_or(_lossy_text(row["payload"])), created_at=row["created_at"], run_id=_opt_int(run_id),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ 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.
|
||||
(U+FFFD) and the ``from_row`` constructors (task, comment, event, run) coerce
|
||||
BLOB cells the same way, so ``show --json`` survives a BLOB comment too.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -52,3 +53,22 @@ def test_list_survives_undecodable_and_blob_text_cells(board):
|
||||
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"
|
||||
|
||||
|
||||
def test_show_json_survives_blob_cells_in_sibling_tables(board):
|
||||
with kbc.connect() as conn:
|
||||
tid = kb.create_task(conn, title="card", assignee="coder")
|
||||
conn.execute(
|
||||
"INSERT INTO task_comments (task_id, author, body, created_at) "
|
||||
"VALUES (?, 'user', X'FFFEABCD00', 1000)", (tid,),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO task_events (task_id, kind, payload, created_at) "
|
||||
"VALUES (?, 'note', X'FFFEABCD00', 1000)", (tid,),
|
||||
)
|
||||
|
||||
with kbc.connect() as conn:
|
||||
comments = kb.list_comments(conn, tid)
|
||||
assert isinstance(comments[-1].body, str) and "\ufffd" in comments[-1].body
|
||||
events = kb.list_events(conn, tid)
|
||||
json.dumps([dataclasses.asdict(c) for c in comments] + [dataclasses.asdict(e) for e in events])
|
||||
|
||||
Reference in New Issue
Block a user