Skip to content

fix: a card.json with null dates renders instead of crashing#30

Draft
ohad6k wants to merge 1 commit into
mainfrom
agents/fix-card-null-dates
Draft

fix: a card.json with null dates renders instead of crashing#30
ohad6k wants to merge 1 commit into
mainfrom
agents/fix-card-null-dates

Conversation

@ohad6k

@ohad6k ohad6k commented Jul 23, 2026

Copy link
Copy Markdown
Owner

The crash

emulo --card tracebacks on a card.json whose stats carry JSON nulls.

months_between() in emulo.py caught (ValueError, IndexError) but not TypeError. A reducer that writes "first_date": null instead of "" makes stats.get("first_date", "") return None, and None[:4] raises an uncaught TypeError that escapes both call sites — print_card() (emulo.py:2599) and render_card_html() (emulo.py:2879). Neither has its own guard, so the user gets a traceback instead of a card.

File "emulo.py", line 2368, in months_between
    y1, m1 = int(first_date[:4]), int(first_date[5:7])
TypeError: 'NoneType' object is not subscriptable

The fix

One line: except (TypeError, ValueError, IndexError).

Returning 0 is already the "unknown" value for this field — I checked both renderers rather than inventing a new sentinel. Each guards the months row with if months: and skips it when falsy (emulo.py:2621 and emulo.py:2888), which is exactly how they treat every other missing stat (sessions, tokens, messages are all if stats.get(...)). So a null date degrades to an omitted row, not a dash and not a bogus zero. The card renders, one row shorter. Non-string dates (an epoch int, a list) now degrade the same way instead of crashing.

Diff is 1 changed line plus a 3-line comment in months_between only. No caller guard was needed — the callers already handle 0 correctly.

Verification

New file: tests/test_card_null_dates.py — 8 tests covering months_between() directly, both renderers, and emulo --card end to end via subprocess on a card.json containing JSON nulls.

Before the fix (tests written first, run against unmodified emulo.py):

$ python -m unittest tests.test_card_null_dates
Ran 8 tests
FAILED (failures=1, errors=4)

The 4 errors were the raw TypeError escaping; the 1 failure was the end-to-end CLI test asserting returncode == 0 and getting 1 with a traceback on stderr. The 3 that passed are the controls: real dates still compute the span, empty/malformed strings already degraded via ValueError.

After the fix:

$ python -m unittest tests.test_card_null_dates
Ran 8 tests in 0.144s
OK

Full suite:

$ python -m unittest discover -s tests
Ran 404 tests in 43.370s
FAILED (failures=1, skipped=3)

The one remaining failure is a release pin, and I deliberately did not fix it

test_bootstrap.test_release_runtime_hashes_match_canonical_repository_bytes fails. It is not a behavior failure — it asserts that the sha256 in .agents/skills/emulo/runtime.json equals the canonical bytes of emulo.py, so any change to emulo.py trips it until the pin is regenerated.

I verified this is caused solely by my edit: at HEAD, emulo.py hashes to 22e305f9..., which is exactly the pinned value. With my change it hashes to ba8cd014....

Repo convention (commits 138252bb "Runtime SHA repinned", 99519743) is to repin in the same commit, leaving ref at the current version. I did not do it here because that file is runtime-integrity / release plumbing and was outside the files I was scoped to touch. It is a one-line, purely derived change for whoever merges:

".agents/skills/emulo/runtime.json" -> files."emulo.py".sha256 = "ba8cd0142e4f7d9bfbb2a4115a85aea622da8cc1d7fd5d6d01d0f6ce9515a2b3"

(ref stays v0.6.0; MINING_PROMPT.md is untouched.) With that one line applied, the suite is 404/404 green minus the 3 pre-existing skips. Every other test in the suite passes on this branch as-is.

Merge-order note: this breaks PR #28 if #28 merges second

PR #28 (agents/tests-coverage, not merged) contains two @unittest.expectedFailure tests for exactly this bug. Once this fix lands, those two will report "unexpected success" and fail that branch.

I did not touch, edit, or comment on #28. Whoever merges should either:

Order matters either way; the decorators have to come off.


Draft, opened unattended by an agent. Please read the release-pin section above before merging.

🤖 Generated with Claude Code

months_between() caught (ValueError, IndexError) but not TypeError. A
reducer that writes "first_date": null instead of "" makes
stats.get("first_date", "") return None, and None[:4] raises an uncaught
TypeError that escapes both print_card() and render_card_html(), so
`emulo --card` tracebacks instead of degrading.

Adding TypeError to the except tuple returns 0, which is already the
"unknown" value here: both renderers guard the months row with `if
months:` and skip it, exactly how they treat every other missing stat.
The card renders, minus one row. Non-string dates (an epoch int, a list)
degrade the same way.

tests/test_card_null_dates.py covers months_between directly, both
renderers, and `emulo --card` end to end on a card.json with JSON nulls.
On the unfixed code those tests produce 4 errors and 1 failure; after the
fix all 8 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ditto Ready Ready Preview, Comment Jul 23, 2026 1:22pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant