Skip to content

fix: 14 verified bug fixes across sync, DB, monitor, and CLI#68

Merged
marcus merged 18 commits intomarcus:mainfrom
viktorius007:fix/verified-bugs
Mar 21, 2026
Merged

fix: 14 verified bug fixes across sync, DB, monitor, and CLI#68
marcus merged 18 commits intomarcus:mainfrom
viktorius007:fix/verified-bugs

Conversation

@viktorius007
Copy link

Summary

Systematic codebase audit found and fixed 14 genuine bugs across two sweep rounds, each personally verified by reading the source code and tracing logic paths. All fixes include matching tests. 30/30 packages passing.

Critical (3)

  • Nil validator panic in SSEApplyRemoteEvents called with nil validator, guaranteed panic on any non-empty SSE event batch (internal/serve/sse.go)
  • work_session_issues never syncs — missing from baseSyncableEntities, events silently dropped on push and failed on pull (cmd/sync.go)
  • Non-atomic undo of deleteRestoreIssue + LogAction as separate locked operations; crash between them loses the sync event (cmd/undo.go)

Data correctness (4)

  • Timestamp parse mismatchGetRecentConflicts used rigid format that fails on driver output, breaking td sync conflicts entirely (internal/db/sync_state.go)
  • rows.Err() unchecked in ~30 query functions — silent partial result sets (internal/db/)
  • Non-transactional migration — crash leaves partial data (internal/db/migrations.go)
  • Snapshot serve race — only copy of snapshot deleted on cache rename failure (internal/api/sync.go)

Concurrency (3)

  • StatusFilter data race — map reference captured in goroutine (pkg/monitor/commands.go)
  • Board editor data race — shared pointer mutated from goroutine (pkg/monitor/board_editor.go)
  • Stale syncState — push updates DB but pull uses stale LastSyncAt for conflict detection (cmd/autosync.go)

Behavioral (3)

  • CLI reject from wrong states — accepted in_progress/blocked/closed; HTTP API restricts to in_review only (cmd/review.go)
  • HelpFilter backspace UTF-8 — byte slicing splits multi-byte runes (pkg/monitor/commands.go)
  • Board editor preview count — shows capped "6" instead of "5+" (pkg/monitor/board_editor.go)

Infrastructure (1)

  • .gitignore + copyFile Sync — pre-commit hook blocked; backup durability gap (cmd/sync.go)

Test plan

  • go test ./... — 30/30 packages pass
  • go test -race ./pkg/monitor/... — no races
  • go vet ./... — clean
  • Manual: verify td sync conflicts displays stored conflicts
  • Manual: verify SSE live sync applies events without panic
  • Manual: verify td reject only works on in_review issues

🤖 Generated with Claude Code

Viktor and others added 18 commits March 21, 2026 19:44
The undo-delete path used two separate locked operations
(RestoreIssue + LogAction), creating a window where a crash
could restore the issue without creating the action_log entry.
This breaks sync push (restore never propagates to server) and
allows double-undo.

Replace with the existing RestoreIssueLogged which performs both
operations under a single withWriteLock call.

Also fix .gitignore to unblock pre-commit export hook.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…licts

GetRecentConflicts used a single rigid format ("2006-01-02 15:04:05")
that fails on timestamps stored by Go's modernc/sqlite driver, which
serializes time.Time via t.String() producing
"2006-01-02 15:04:05.999999999 +0000 UTC". This made td sync conflicts
non-functional.

Add the Go String() format and RFC3339Nano to parseTimestamp, and use
parseTimestamp in GetRecentConflicts instead of the rigid single format.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
fetchBoardIssues captured the StatusFilter map by reference (maps are
reference types in Go), not by value as the comment claimed. When
toggleBoardClosed mutates the map in-place and then launches a
fetchBoardIssues goroutine, the goroutine reads the same map that
future Update cycles can write to — a textbook data race.

Replace the shallow assignment with a deep copy so the goroutine
operates on an independent snapshot of the filter state.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
HelpFilter backspace used byte slicing ([:len-1]) which splits
multi-byte UTF-8 runes (e.g. é, €, emoji) mid-byte, producing
invalid UTF-8 strings. Switch to rune slicing.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
copyFile deferred out.Close() without calling out.Sync(), so data
could remain in OS write buffers. On crash between io.Copy and
Close, the backup file used by runBootstrap could be incomplete
or zero-length, with no way to recover the original database.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When copyFile uses os.Rename (fast path), the original tmpPath is
moved to tmpCachePath. If the subsequent rename to cachePath then
fails, the code deleted tmpCachePath (the only remaining copy) and
tried to serve from the now-nonexistent tmpPath — HTTP 500.

Fix: update servePath to tmpCachePath immediately after copyFile
succeeds, and don't delete tmpCachePath on rename failure since
it may be the only copy of the snapshot data.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The migration issued individual UPDATE and DELETE statements without
a transaction. A crash mid-migration left issue_files in a partially
migrated state with potential data loss (absolute-path rows deleted
before their relative-path replacements were committed).

Wrap all reads and writes in a single transaction so the migration
is atomic — either all paths are converted or none are.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Per the database/sql contract, rows.Next() can stop early due to
a driver error without the caller seeing an error from Scan. The
proper handling is to check rows.Err() after the loop exits.

30 query functions across issues.go, issue_relations.go, activity.go,
boards.go, notes.go, and work_sessions.go were missing this check,
silently returning partial result sets on error.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The previous format "+0000 UTC" was a literal match, not a timezone
pattern — it only worked for UTC times. The modernc/sqlite driver
stores time.Time via t.String() which can include non-UTC offsets
(e.g. "+1000 AEST") and a monotonic clock suffix ("m=+0.000123").

Use Go's proper format pattern "-0700 MST" to handle any timezone,
and strip the monotonic suffix before parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- StatusFilter test: old test only checked the original was mutated
  (trivially true). New test verifies mutations to original don't
  affect the copy, and vice versa.

- Snapshot race test: old test only covered copyFile correctness.
  New test replicates the exact handleSyncSnapshot caching logic
  and verifies servePath is readable after a second-rename failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
HelpFilter test now calls handleKey() with a real KeyBackspace message
through the HelpFilterMode code path, rather than duplicating the fix
logic inline. If the rune-based slicing is reverted, the test fails.

StatusFilter test split into: one that calls fetchBoardIssues (exercises
real code, can't inspect closure), and one that verifies the copy
pattern's bidirectional independence.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The test verifies correctness (restore + action_log entry) but cannot
verify atomicity — the old non-atomic two-step code also passes.
Crash-safety is a structural guarantee from RestoreIssueLogged using
a single withWriteLock, not something a unit test can prove.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
work_session_issues was documented in sync specs, had backfill
support, and full test coverage, but was missing from the sync
entity validator. Events were silently dropped on push and failed
validation on pull (with sequence pointer advancing past them),
meaning work session issue tags never synced between devices.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
ApplyRemoteEvents was called with nil as the EntityValidator. Since
applyEventWithPrevious calls validator() unconditionally, this caused
a nil function pointer panic on any non-empty SSE event batch.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The CLI rejectCmd used IsValidTransition(status, StatusOpen) which
accepted transitions from in_progress, blocked, and closed — any
status with a path to open. The HTTP API correctly restricts reject
to in_review only. Rejecting a blocked or in_progress issue makes
no semantic sense and pollutes the action log.

Replace the state machine check with an explicit status guard
matching the HTTP API's validFrom constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
syncState was loaded once then passed to both push and pull.
After push updates last_sync_at in the DB, the in-memory struct
was stale. ApplyRemoteEvents uses LastSyncAt for conflict
detection, so rows modified between the old and new timestamps
were incorrectly flagged as conflicts during pull.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
executeBoardEditorSave captured m.BoardEditorBoard (a pointer) then
mutated the pointed-to struct from a tea.Cmd goroutine. The BubbleTea
Update loop could read the same struct concurrently — a data race.

Copy the struct value into the closure so the goroutine operates on
an independent copy. Same class of bug as the StatusFilter race fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The preview query fetched with Limit: 6 to detect "more than 5",
but Count: len(issues) reported the raw count (at most 6), making
"Matches: 6 issue(s)" appear for queries matching hundreds of issues.

Use a sentinel value (-1) when results exceed 5, rendering as
"Matches: 5+ issue(s)" and "... and more" in the preview.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@marcus
Copy link
Owner

marcus commented Mar 21, 2026

Hey @viktorius007! Starling here (AI assistant on the project). 👋

This is an extraordinary piece of work — a systematic, personally-verified audit that found and fixed 14 genuine bugs across sync, DB, monitor, and CLI. Two full sweep rounds, 30/30 packages passing, race detector clean. That's a high bar.

A few things that stand out:

  • The SSE nil-validator panic — guaranteed crash on any non-empty event batch. Good catch.
  • work_session_issues never syncing — silently dropped events are the worst kind of bug. Nice detective work tracing it to the missing entity registration.
  • Non-atomic undo of delete — the crash window between RestoreIssue + LogAction is exactly the kind of subtle correctness issue that's hard to spot without careful reading.
  • The rows.Err() audit across 30 query functions — this is the kind of unglamorous but genuinely important coverage work that keeps projects healthy.

This is flagged for Marcus's review before merge (as with all PRs). Given the scope, he'll want to walk through the concurrency fixes in particular.

Thanks for investing this level of care in the project. ✦

Copy link
Owner

@marcus marcus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full review + test run complete.

Code review: All 14 fixes verified correct. Highlights:

  • SSE nil-validator: clean fix, passes allowAll func instead of nil
  • work_session_issues sync: one-liner addition to baseSyncableEntities, test confirms it
  • Non-atomic undo: delegates to RestoreIssueLogged for single-lock guarantee
  • rows.Err() additions: systematic, correct placement in ~30 query functions
  • Migration tx: properly uses defer tx.Rollback() + tx.Commit() pattern
  • Snapshot race: servePath = tmpCachePath update before second rename is exactly right
  • StatusFilter deep copy: make + range copy is the correct map deep-copy idiom
  • Board editor goroutine: boardCopy := *m.BoardEditorBoard value copy eliminates shared pointer
  • HelpFilter backspace: []rune conversion correct for multi-byte safety
  • Timestamp parse: monotonic clock strip + additional formats handles Go time.Time.String() output
  • Stale syncState: reload after push is correct, prevents false conflict detection
  • CLI reject: aligns with HTTP API validFrom: [StatusInReview]

Tests: go test ./... -race -count=1 — 30/30 packages pass, no races detected.

LGTM. Merging.

@marcus marcus merged commit bbd5b5f into marcus:main Mar 21, 2026
@viktorius007 viktorius007 deleted the fix/verified-bugs branch March 21, 2026 19:39
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.

2 participants