From 5f6065344f800c2c5ad6f802e95e9e8ff5cc7b26 Mon Sep 17 00:00:00 2001 From: Godopu Date: Sun, 25 Oct 2026 22:15:02 +0000 Subject: [PATCH] fix(lib.sh): load_state_json strict treats a missing DB table as unreadable (Issue #71) A DB without its sessions table (or without both tables) was read as an empty registry in strict mode, so stop closed a registered session row-less and exited 0. Strict mode now fails on any OperationalError; the non-strict reader is unchanged. Refs #71 Co-Authored-By: Claude Opus 5.5 --- .agents/skills/lib.sh | 12 ++++++------ tests/test_issue71_herdr_names.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index e873ff3..0faacb9 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -1592,8 +1592,8 @@ herdr() { # NOTE: Retained as small inline PYEOF block (39 lines) per Plan Rev.2 (Job 98393a97). # Small blocks have minimal overhead and high local cohesion; extraction into lib_py is reserved for large blocks (>200 lines). # `load_state_json strict` (Issue #71) fails (stderr, exit 1) when the YAML or -# DB cannot be read, instead of printing {} - for a caller that acts on a -# session being absent. +# DB cannot be read, a DB table missing included, instead of printing {} - for +# a caller that acts on a session being absent. load_state_json() { env_python "$AGENT_SESSIONS_YAML" "MAM_STATE_READ=${1:-}" <<'PYEOF' import os, sys, sqlite3, json, yaml @@ -1610,16 +1610,16 @@ try: row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() if row: d = json.loads(row[0]) - except sqlite3.OperationalError as e: - if strict and 'no such table' not in str(e): + except sqlite3.OperationalError: + if strict: raise try: cursor = conn.execute('SELECT data FROM sessions') for r in cursor.fetchall(): db_sessions.append(json.loads(r[0])) d['herdr_sessions'] = db_sessions - except sqlite3.OperationalError as e: - if strict and 'no such table' not in str(e): + except sqlite3.OperationalError: + if strict: raise conn.close() elif os.path.exists(yaml_path): diff --git a/tests/test_issue71_herdr_names.py b/tests/test_issue71_herdr_names.py index cf25803..22c6197 100644 --- a/tests/test_issue71_herdr_names.py +++ b/tests/test_issue71_herdr_names.py @@ -511,7 +511,7 @@ def test_stop_does_not_take_an_unreadable_row_for_no_row(mam_sandbox, mock_herdr assert "orphan74" in json.loads(Path(mock_herdr).read_text())["agents"] -@pytest.mark.parametrize("broken", ["yaml-without-db", "db"]) +@pytest.mark.parametrize("broken", ["yaml-without-db", "db", "no-sessions-table", "no-tables"]) def test_stop_does_not_take_an_unreadable_registry_for_no_row(mam_sandbox, mock_herdr, broken): """load_state_json's {} on a read error is not "no row": a registered session is not closed row-less.""" name = "orphan75-creator-claude" @@ -522,10 +522,18 @@ def test_stop_does_not_take_an_unreadable_registry_for_no_row(mam_sandbox, mock_ for f in mam.glob("agent-sessions.db*"): f.unlink() (mam / "agent-sessions.yaml").write_text("herdr_sessions: [\n - {name: " + name + "\n") - else: + elif broken == "db": for f in mam.glob("agent-sessions.db-*"): f.unlink() (mam / "agent-sessions.db").write_bytes(b"this is not a database" * 64) + else: + # a missing table is not an empty registry + conn = sqlite3.connect(str(mam / "agent-sessions.db")) + conn.execute("DROP TABLE sessions") + if broken == "no-tables": + conn.execute("DROP TABLE state") + conn.commit() + conn.close() _set_mock(mock_herdr, agents={name: _agent(mam_sandbox, "w1:p76")}) res = _stop(mam_sandbox, name) assert res.returncode == 1, res.stdout + res.stderr