From ec85449eaab6fcd60b03c7a13c413fc0c22a000a Mon Sep 17 00:00:00 2001 From: Alex Chupryna Date: Mon, 18 May 2026 00:00:44 +0200 Subject: [PATCH 1/2] fix: handle FileNotFoundError in CLI and canonicalize board ID on init Two bugs fixed: 1. handle_resolve_errors decorator didn't catch FileNotFoundError, so commands like `card list` would dump a full traceback when no active board was set instead of printing a clean error message. 2. `trache init --board-id` accepted Trello short links (e.g. 6WMY6Kls) but stored them verbatim. Trello GET endpoints accept short links but POST/PUT require the full 24-char ID, causing all write operations to fail with 400 Bad Request. Now init canonicalizes to the full ID via the API response. --- src/trache/cli/_errors.py | 3 +++ src/trache/cli/app.py | 1 + 2 files changed, 4 insertions(+) diff --git a/src/trache/cli/_errors.py b/src/trache/cli/_errors.py index 93aa5a7..3b575f6 100644 --- a/src/trache/cli/_errors.py +++ b/src/trache/cli/_errors.py @@ -22,6 +22,9 @@ def wrapper(*args, **kwargs): msg = e.args[0] if e.args else "Requested item not found" get_output().error(msg) raise typer.Exit(1) + except FileNotFoundError as e: + get_output().error(str(e)) + raise typer.Exit(1) except ValueError as e: get_output().error(str(e)) raise typer.Exit(1) diff --git a/src/trache/cli/app.py b/src/trache/cli/app.py index a38ab68..4828648 100644 --- a/src/trache/cli/app.py +++ b/src/trache/cli/app.py @@ -175,6 +175,7 @@ def init( try: board_obj = client.get_board(config.board_id) + config.board_id = board_obj.id config.board_name = board_obj.name out.human(f"Board: [bold]{escape(board_obj.name)}[/bold]") except Exception: From 81c943b7bf03b58b0cef733d3fa859375b2416d4 Mon Sep 17 00:00:00 2001 From: Alex Chupryna Date: Mon, 18 May 2026 00:22:12 +0200 Subject: [PATCH 2/2] fix: validate hex format in resolve_card_id and resolve_list_id Both functions assumed any 24-character string was a Trello ID, but Trello IDs are specifically 24-char lowercase hex strings. A list name that happened to be exactly 24 characters (e.g. "Phase 6: Build in Public") would be returned as-is instead of being looked up, causing cards to be stored with the list name as their list_id. This made all writes to that list fail with 400 Bad Request from the Trello API. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/trache/cache/db.py | 13 ++++++++++--- tests/test_push.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/trache/cache/db.py b/src/trache/cache/db.py index 140105e..19ab104 100644 --- a/src/trache/cache/db.py +++ b/src/trache/cache/db.py @@ -18,6 +18,13 @@ DB_FILENAME = "cache.db" MIGRATION_SENTINEL = "cache.db.migrated" +_HEX_CHARS = frozenset("0123456789abcdef") + + +def _is_trello_id(s: str) -> bool: + """Return True if s looks like a 24-char Trello hex ID.""" + return len(s) == 24 and all(c in _HEX_CHARS for c in s) + # Migration functions keyed by TARGET version. Each receives an open connection # and runs DDL/DML to bring the schema from (key-1) to key. The version row is # updated by the runner — migrations must NOT touch schema_version themselves. @@ -702,8 +709,8 @@ def resolve_card_id(identifier: str, cache_dir: Path) -> str: "No board initialised. Run 'trache init' and 'trache pull' first." ) - # Full 24-char ID — return as-is - if len(identifier) == 24: + # Full 24-char hex ID — return as-is + if _is_trello_id(identifier): return identifier # Validate UID6 format or temp ID @@ -755,7 +762,7 @@ def resolve_card_id(identifier: str, cache_dir: Path) -> str: def resolve_list_id(identifier: str, cache_dir: Path) -> str: """Resolve a list ID or name to a full list ID.""" - if len(identifier) == 24: + if _is_trello_id(identifier): return identifier with _connect(cache_dir) as conn: diff --git a/tests/test_push.py b/tests/test_push.py index 7e258bd..4dd1295 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -71,7 +71,7 @@ def test_push_added_card(self, tmp_path: Path, sample_card: Card) -> None: client = MagicMock() new_card = Card( - id="real_id_from_trello_here", + id="aaa111bbb222ccc333ddd444", title=sample_card.title, list_id=sample_card.list_id, ) @@ -221,7 +221,7 @@ def test_push_new_card_with_checklists(self, tmp_path: Path) -> None: # Mock client real_card = Card( - id="real_trello_card_id_here", title="Card With Checklist", list_id="list1" + id="bbb222ccc333ddd444eee555", title="Card With Checklist", list_id="list1" ) new_cl = Checklist(id="real_cl_1", name="Tasks", card_id=real_card.id) item_a = ChecklistItem(id="real_item_a", name="Do thing A")