fix: 14 verified bug fixes across sync, DB, monitor, and CLI#68
fix: 14 verified bug fixes across sync, DB, monitor, and CLI#68marcus merged 18 commits intomarcus:mainfrom
Conversation
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]>
|
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:
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. ✦ |
marcus
left a comment
There was a problem hiding this comment.
Full review + test run complete.
Code review: All 14 fixes verified correct. Highlights:
- SSE nil-validator: clean fix, passes
allowAllfunc instead of nil work_session_issuessync: one-liner addition tobaseSyncableEntities, test confirms it- Non-atomic undo: delegates to
RestoreIssueLoggedfor 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 = tmpCachePathupdate before second rename is exactly right - StatusFilter deep copy:
make + range copyis the correct map deep-copy idiom - Board editor goroutine:
boardCopy := *m.BoardEditorBoardvalue copy eliminates shared pointer - HelpFilter backspace:
[]runeconversion 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.
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)
ApplyRemoteEventscalled withnilvalidator, guaranteed panic on any non-empty SSE event batch (internal/serve/sse.go)work_session_issuesnever syncs — missing frombaseSyncableEntities, events silently dropped on push and failed on pull (cmd/sync.go)RestoreIssue+LogActionas separate locked operations; crash between them loses the sync event (cmd/undo.go)Data correctness (4)
GetRecentConflictsused rigid format that fails on driver output, breakingtd sync conflictsentirely (internal/db/sync_state.go)rows.Err()unchecked in ~30 query functions — silent partial result sets (internal/db/)internal/db/migrations.go)internal/api/sync.go)Concurrency (3)
pkg/monitor/commands.go)pkg/monitor/board_editor.go)cmd/autosync.go)Behavioral (3)
cmd/review.go)pkg/monitor/commands.go)pkg/monitor/board_editor.go)Infrastructure (1)
.gitignore+copyFileSync — pre-commit hook blocked; backup durability gap (cmd/sync.go)Test plan
go test ./...— 30/30 packages passgo test -race ./pkg/monitor/...— no racesgo vet ./...— cleantd sync conflictsdisplays stored conflictstd rejectonly works on in_review issues🤖 Generated with Claude Code