Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
9fe7c4f
Refactor chat tests: extract mocks and add test setup helper
wesm Feb 2, 2026
63e0b80
Refactor executor tests: unify test contexts, remove redundant mock, …
wesm Feb 2, 2026
b0e70ce
Consolidate manifest tests into table-driven format
wesm Feb 2, 2026
b5cd153
Refactor attachment tests into table-driven format with stronger asse…
wesm Feb 2, 2026
e95df06
Simplify Gmail client test helpers for readability
wesm Feb 2, 2026
a0d234c
Introduce Clock interface to RateLimiter for deterministic testing
wesm Feb 2, 2026
b32fa24
Refactor MCP server tests: generic helpers, table-driven error cases
wesm Feb 2, 2026
e0fbd3d
Add subtests and deterministic header ordering in MIME parse tests
wesm Feb 2, 2026
a2ce484
Consolidate SearchFast tests into single table-driven test
wesm Feb 2, 2026
486a767
Refactor SQLite query tests: centralize schema, add builders, split file
wesm Feb 2, 2026
10fc306
Refactor parquetBuilder: remove withEmpty, decompose build into helpers
wesm Feb 2, 2026
c1b0325
Refactor search parser tests: unified assertion with expected Query s…
wesm Feb 2, 2026
e232766
Refactor store tests: standardize error handling, table-driven upsert…
wesm Feb 2, 2026
34656d8
Refactor encoding tests: consolidate AlreadyValid to use runEncodingT…
wesm Feb 2, 2026
44367d5
Refactor sync tests: extract DB assertion helpers and test fixtures
wesm Feb 2, 2026
0c506c3
Refactor sync test env: use t.Cleanup, merge NewTestEnv/newTestEnv
wesm Feb 2, 2026
734bd7b
Refactor testutil tests: split validation groups, rename helpers
wesm Feb 2, 2026
bc9ebe0
Refactor actions_test: decouple test data, remove helper logic, gener…
wesm Feb 2, 2026
a423fcb
Refactor TUI model_test: split 5800-line file into domain-specific te…
wesm Feb 2, 2026
a1f2471
Refactor view_test: remove redundant wantText field, replace stripANS…
wesm Feb 2, 2026
5ab664e
Refactor update_test: add parallel execution, descriptive names, clea…
wesm Feb 2, 2026
d3b46f0
Fix FormatSummary test: pass subtest t to closures, restore manifest …
wesm Feb 2, 2026
2097736
Strengthen mixed-hash attachment test to assert per-file error details
wesm Feb 2, 2026
16ec46f
Add test case for message-based rate limit detection
wesm Feb 2, 2026
2ddec3b
Harden RateLimiter: restore deadline test, add timeout guard, defend …
wesm Feb 2, 2026
08477ad
Fix MCP test helpers: set Params.Name and correct misleading comment
wesm Feb 2, 2026
9194ed4
Add field-level assertions for from:bob and has:attachment SearchFast…
wesm Feb 2, 2026
cbfc260
Harden SQLite query test helpers: defensive FTS cleanup, fix hardcode…
wesm Feb 2, 2026
754d2a8
Consolidate UpsertMessage tests into single table-driven test
wesm Feb 2, 2026
4dc2c66
Use parameter binding in assertBodyContains and assertRawDataExists t…
wesm Feb 2, 2026
0268877
Validate newTestEnv receives at most one *Options argument
wesm Feb 2, 2026
afeb5ea
Refactor TestValidateRelativePath: separate invalid cases from shared…
wesm Feb 2, 2026
37db78e
Broaden ANSI strip regex to cover full CSI spec
wesm Feb 2, 2026
1bdf07d
Fix gofmt formatting in TestAttachments
wesm Feb 2, 2026
ddbf421
Fix ContextTimeout test to use mock clock for cancellation and add ni…
wesm Feb 2, 2026
3364079
Test UpsertMessage update path by mutating fields before second upsert
wesm Feb 2, 2026
e953744
Replace time.Sleep with mock-clock timer polling in ContextTimeout test
wesm Feb 2, 2026
eae3be5
Add test fixture builders for MessageSummary and MessageDetail
wesm Feb 2, 2026
76286bf
Centralize query.Engine test double into shared querytest.MockEngine
wesm Feb 2, 2026
15c7419
Add AssertManifestExecution helper and mock configuration wrappers to…
wesm Feb 2, 2026
5419b76
Add fluent ManifestBuilder and assertion helpers to deletion tests
wesm Feb 2, 2026
cefef7b
Consolidate assertion fields in attachments_test and add CreateTempZi…
wesm Feb 2, 2026
564fb65
Add GmailErrorBuilder and reason constants to gmail test helpers
wesm Feb 2, 2026
ed4c8ed
Add test helpers and consolidate deletion mock tests
wesm Feb 2, 2026
509d219
Add rlFixture and acquireAsync helpers to rate limiter tests
wesm Feb 2, 2026
e4e2018
Extract MakeRawEmail and AssertStringSliceEqual to testutil/email pac…
wesm Feb 2, 2026
9dcf701
Add test helpers to reduce boilerplate in DuckDB query tests
wesm Feb 2, 2026
90cda05
Add assertRow/assertRowsContain helpers and consolidate aggregate tests
wesm Feb 2, 2026
4860f5b
Add test builders for sources, conversations, labels and replace raw …
wesm Feb 2, 2026
8fa2539
Add assertSearchCount/assertAllResults helpers and consolidate MergeF…
wesm Feb 2, 2026
d9805ce
Extract shared DB test helpers into internal/testutil/dbtest package
wesm Feb 2, 2026
d90935e
Replace raw SQL Parquet test fixtures with typed TestDataBuilder
wesm Feb 2, 2026
52efe4d
Extract shared test pointer/date helpers into testutil/ptr package
wesm Feb 2, 2026
29849ac
Extract StoreFixture and MessageBuilder into shared storetest package
wesm Feb 2, 2026
23cd519
Extract shared encoding test fixtures and UTF-8 assertions into testutil
wesm Feb 2, 2026
601c3bd
Add fluent MIME MessageBuilder and replace static test fixtures
wesm Feb 2, 2026
e71bd19
Add history event builders and pagination helper to reduce sync test …
wesm Feb 2, 2026
98ea19b
Add SetOptions and SetHistory helpers to reduce sync test boilerplate
wesm Feb 2, 2026
22558f6
Export PathTraversalCases and WriteAndVerifyFile from testutil
wesm Feb 2, 2026
a598e80
Move generic test helpers to testutil and add ControllerTestEnv
wesm Feb 2, 2026
747c565
Add test helpers and reduce TUI test boilerplate
wesm Feb 2, 2026
811240b
Reduce TUI test boilerplate with helpers and remove duplicate comments
wesm Feb 2, 2026
726d54c
Add search test helpers and reduce search_test.go boilerplate
wesm Feb 2, 2026
a7310ae
Add selection test helpers and reduce selection_test.go boilerplate
wesm Feb 2, 2026
92dd339
Replace manual level checks with assertLevel helper and use NewBuilde…
wesm Feb 2, 2026
2e1d238
Add view render test helpers and reduce view_render_test.go boilerplate
wesm Feb 2, 2026
6946fa4
Move ANSI/color profile test helpers to setup_test.go and fix race co…
wesm Feb 2, 2026
eeee795
Move tar.gz archive helper to testutil and use shared file assertions…
wesm Feb 2, 2026
106cbca
Fix MockEngine behavior changes for GetMessageBySourceID and GetTotal…
wesm Feb 2, 2026
00d224d
Fix deletion test gaps: nil summary path, Stat error handling, list c…
wesm Feb 2, 2026
5c94ba6
Sort map keys in CreateTempZip for deterministic zip entry order
wesm Feb 2, 2026
8e6b47d
Make acquireAsync robust against immediate Acquire completion
wesm Feb 2, 2026
8e2ba03
Add duplicate key detection and ordering assertions to aggregate tests
wesm Feb 2, 2026
cd5e618
Improve test isolation and add duplicate key detection to aggregate h…
wesm Feb 2, 2026
d2635ef
Harden test data builders: require Label name, infer message source_id
wesm Feb 2, 2026
c4e989f
Harden search tests: relax domain filter count, rebind range vars in …
wesm Feb 2, 2026
0dddf7b
Validate SourceID matches conversation in AddMessage test helper
wesm Feb 2, 2026
8ce50f2
Harden TestDataBuilder with validation and empty-table safety
wesm Feb 2, 2026
a8a994c
Harden storetest: unique default SourceMessageID, validate EnsureLabe…
wesm Feb 2, 2026
540a3b7
Prevent cross-test coupling by returning defensive copies of EncodedS…
wesm Feb 2, 2026
a8c48ec
Fix nondeterministic header order in MessageBuilder and add unit tests
wesm Feb 2, 2026
8ef1c79
Convert PathTraversalCases to function returning fresh slice with OS-…
wesm Feb 2, 2026
775f87b
Fix misleading comment on assertDrillState helper
wesm Feb 2, 2026
ab62ab3
Defensively copy testAccounts slice in WithStandardAccounts
wesm Feb 2, 2026
243113c
Replace manual level checks with assertLevel helper in nav_test.go
wesm Feb 2, 2026
c4e7b51
Convert standardStats to factory function to prevent cross-test state…
wesm Feb 2, 2026
8899784
Implement fallback search in MockEngine.GetMessageBySourceID
wesm Feb 2, 2026
5498798
Guard against nil function call for unknown aggName in TestAggregations
wesm Feb 2, 2026
420b847
Fall back to default source_id when conversation row missing in AddMe…
wesm Feb 2, 2026
9b2da3d
Make AddMessage fatal on missing conversation when SourceID is explicit
wesm Feb 2, 2026
60e7316
Add fatal guard to AddAttachment for missing messages and fixture val…
wesm Feb 2, 2026
7b5cb49
Scope MessageBuilder counter per-fixture for deterministic test IDs
wesm Feb 2, 2026
f100e7f
Deduplicate EncodedSamplesT and add defensive copy test
wesm Feb 2, 2026
8935a83
Fix Header() to overwrite duplicates and strengthen CRLF test
wesm Feb 2, 2026
0eadc60
Add fresh-slice test for PathTraversalCases and cover forward-slash a…
wesm Feb 2, 2026
e677a16
Remove nondeterministic map scan fallback in MockEngine.GetMessageByS…
wesm Feb 2, 2026
e15c59f
Only fall back to default source_id on ErrNoRows, not on all errors
wesm Feb 2, 2026
ca4da91
Use tdb.nextMessageID instead of hardcoded IDs in dbtest tests
wesm Feb 2, 2026
693015e
Add missing failure-path tests for AddMessage and AddAttachment guards
wesm Feb 2, 2026
5b380c9
Use distinct prefix for Fixture.NewMessage to prevent ID collisions w…
wesm Feb 2, 2026
b3ebbb8
Use case-insensitive comparison for Header key deduplication in Messa…
wesm Feb 2, 2026
a0b4d13
Guard against empty slice before accessing b[0] in TestPathTraversalC…
wesm Feb 2, 2026
4ac567e
Add test for non-ErrNoRows DB error in TestDB.AddMessage source_id lo…
wesm Feb 2, 2026
670e496
Fix fakeT to delegate to real testing.TB and clarify expectFatal retu…
wesm Feb 2, 2026
64666f1
Add HeaderAppend method and test case-insensitive header dedup in Mes…
wesm Feb 2, 2026
49e14cc
Remove brittle panic-recovery assertion in TestAddMessage_DBErrorFail…
wesm Feb 2, 2026
1957767
Override Fatal/FailNow/Skip on fakeT and remove misleading expectFata…
wesm Feb 2, 2026
84fef7d
Port BCC RecipientName regression test into refactored test structure
wesm Feb 2, 2026
ccfaee8
Complete fakeT overrides: add Skipf/SkipNow, set failed on all paths
wesm Feb 2, 2026
ae01c47
Fix lint errors and add pre-commit hook for linting
wesm Feb 2, 2026
2ffca64
Document pre-commit hook in Makefile and CLAUDE.md
wesm Feb 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
# Pre-commit hook: run fmt check and lint on staged Go files.
# Install: git config core.hooksPath .githooks

# Check if any Go files are staged
STAGED_GO=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$')
if [ -z "$STAGED_GO" ]; then
exit 0
fi

# Check formatting
UNFORMATTED=$(gofmt -l $STAGED_GO 2>/dev/null)
if [ -n "$UNFORMATTED" ]; then
echo "gofmt: these files need formatting:"
echo "$UNFORMATTED"
echo ""
echo "Run: make fmt"
exit 1
fi

# Run linter
echo "Running linter..."
if ! golangci-lint run ./... 2>&1; then
echo ""
echo "Lint failed. Fix errors before committing."
exit 1
fi
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,11 @@ Sync is **read-only** - no modifications to Gmail.

## Code Style & Linting

All code must pass formatting and linting checks before commit.
All code must pass formatting and linting checks before commit. A pre-commit
hook is available to enforce this automatically:

```bash
make setup-hooks # Enable pre-commit hook (fmt + lint)
make test # Run tests
make fmt # Format code (go fmt)
make lint # Run linter (golangci-lint)
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ LDFLAGS := -X github.com/wesm/msgvault/cmd/msgvault/cmd.Version=$(VERSION) \

LDFLAGS_RELEASE := $(LDFLAGS) -s -w

.PHONY: build build-release install clean test test-v fmt lint tidy shootout run-shootout help
.PHONY: build build-release install clean test test-v fmt lint tidy shootout run-shootout setup-hooks help

# Build the binary (debug)
build:
Expand Down Expand Up @@ -62,6 +62,11 @@ lint:
@which golangci-lint > /dev/null || (echo "Install golangci-lint: https://golangci-lint.run/usage/install/" && exit 1)
golangci-lint run ./...

# Enable pre-commit hook (fmt + lint)
setup-hooks:
git config core.hooksPath .githooks
@echo "Pre-commit hook enabled (.githooks/pre-commit)"

# Tidy dependencies
tidy:
go mod tidy
Expand All @@ -87,6 +92,7 @@ help:
@echo " fmt - Format code"
@echo " lint - Run linter"
@echo " tidy - Tidy go.mod"
@echo " setup-hooks - Enable pre-commit hook (fmt + lint)"
@echo " clean - Remove build artifacts"
@echo ""
@echo " shootout - Build MIME shootout tool"
Expand Down
71 changes: 24 additions & 47 deletions cmd/msgvault/cmd/repair_encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package cmd

import (
"testing"
"unicode/utf8"

"github.com/wesm/msgvault/internal/testutil"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/encoding/korean"
Expand All @@ -12,6 +12,7 @@ import (
)

func TestDetectAndDecode_Windows1252(t *testing.T) {
enc := testutil.EncodedSamples()
// Windows-1252 specific characters: smart quotes (0x91-0x94), en/em dash (0x96, 0x97)
tests := []struct {
name string
Expand All @@ -20,27 +21,27 @@ func TestDetectAndDecode_Windows1252(t *testing.T) {
}{
{
name: "smart single quote (apostrophe)",
input: []byte("Rand\x92s Opponent"), // 0x92 = right single quote U+2019
input: enc.Win1252_SmartQuoteRight,
expected: "Rand\u2019s Opponent",
},
{
name: "en dash",
input: []byte("Limited Time Only \x96 50 Percent"), // 0x96 = en dash U+2013
input: []byte("Limited Time Only \x96 50 Percent"), // different text than fixture
expected: "Limited Time Only \u2013 50 Percent",
},
{
name: "em dash",
input: []byte("Costco Travel\x97Exclusive"), // 0x97 = em dash U+2014
input: []byte("Costco Travel\x97Exclusive"), // different text than fixture
expected: "Costco Travel\u2014Exclusive",
},
{
name: "trademark symbol",
input: []byte("Craftsman\xae Tools"), // 0xAE = ®
input: []byte("Craftsman\xae Tools"),
expected: "Craftsman® Tools",
},
{
name: "registered trademark in Windows-1252",
input: []byte("Windows\xae 7"), // 0xAE = ®
input: []byte("Windows\xae 7"),
expected: "Windows® 7",
},
}
Expand All @@ -54,38 +55,36 @@ func TestDetectAndDecode_Windows1252(t *testing.T) {
if result != tt.expected {
t.Errorf("detectAndDecode() = %q, want %q", result, tt.expected)
}
if !utf8.ValidString(result) {
t.Errorf("detectAndDecode() result is not valid UTF-8")
}
testutil.AssertValidUTF8(t, result)
})
}
}

func TestDetectAndDecode_Latin1(t *testing.T) {
// ISO-8859-1 (Latin-1) characters
enc := testutil.EncodedSamples()
tests := []struct {
name string
input []byte
expected string
}{
{
name: "o with acute accent",
input: []byte("Mir\xf3 - Picasso"), // 0xF3 = ó
input: enc.Latin1_OAcute,
expected: "Miró - Picasso",
},
{
name: "c with cedilla",
input: []byte("Gar\xe7on"), // 0xE7 = ç
input: enc.Latin1_CCedilla,
expected: "Garçon",
},
{
name: "u with umlaut",
input: []byte("M\xfcnchen"), // 0xFC = ü
input: enc.Latin1_UUmlaut,
expected: "München",
},
{
name: "n with tilde",
input: []byte("Espa\xf1a"), // 0xF1 = ñ
input: enc.Latin1_NTilde,
expected: "España",
},
}
Expand All @@ -99,37 +98,21 @@ func TestDetectAndDecode_Latin1(t *testing.T) {
if result != tt.expected {
t.Errorf("detectAndDecode() = %q, want %q", result, tt.expected)
}
if !utf8.ValidString(result) {
t.Errorf("detectAndDecode() result is not valid UTF-8")
}
testutil.AssertValidUTF8(t, result)
})
}
}

func TestDetectAndDecode_AsianEncodings(t *testing.T) {
// For short Asian text samples, automatic charset detection is ambiguous
// since the same bytes can be valid in multiple encodings.
// The key requirement is that the output is valid UTF-8.
enc := testutil.EncodedSamples()
tests := []struct {
name string
input []byte
}{
{
name: "Shift-JIS Japanese",
input: []byte{0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd}, // "こんにちは"
},
{
name: "GBK Simplified Chinese",
input: []byte{0xc4, 0xe3, 0xba, 0xc3}, // "你好"
},
{
name: "Big5 Traditional Chinese",
input: []byte{0xa9, 0x6f, 0xa6, 0x6e}, // "你好"
},
{
name: "EUC-KR Korean",
input: []byte{0xbe, 0xc8, 0xb3, 0xe7}, // "안녕"
},
{"Shift-JIS Japanese", enc.ShiftJIS_Konnichiwa},
{"GBK Simplified Chinese", enc.GBK_Nihao},
{"Big5 Traditional Chinese", enc.Big5_Nihao},
{"EUC-KR Korean", enc.EUCKR_Annyeong},
}

for _, tt := range tests {
Expand All @@ -138,10 +121,7 @@ func TestDetectAndDecode_AsianEncodings(t *testing.T) {
if err != nil {
t.Fatalf("detectAndDecode() error = %v", err)
}
if !utf8.ValidString(result) {
t.Errorf("detectAndDecode() result is not valid UTF-8: %q", result)
}
// Result should not be empty
testutil.AssertValidUTF8(t, result)
if len(result) == 0 {
t.Errorf("detectAndDecode() returned empty string")
}
Expand Down Expand Up @@ -209,17 +189,17 @@ func TestSanitizeUTF8(t *testing.T) {
{
name: "invalid byte replaced",
input: "Hello\x80World",
expected: "Hello�World",
expected: "Hello\ufffdWorld",
},
{
name: "multiple invalid bytes",
input: "Test\x80\x81\x82String",
expected: "Test���String",
expected: "Test\ufffd\ufffd\ufffdString",
},
{
name: "truncated UTF-8 sequence",
input: "Hello\xc3", // Incomplete UTF-8 sequence
expected: "Hello",
expected: "Hello\ufffd",
},
}

Expand All @@ -229,10 +209,7 @@ func TestSanitizeUTF8(t *testing.T) {
if result != tt.expected {
t.Errorf("sanitizeUTF8(%q) = %q, want %q", tt.input, result, tt.expected)
}
if !utf8.ValidString(result) {
t.Errorf("sanitizeUTF8() result is not valid UTF-8")
}
testutil.AssertValidUTF8(t, result)
})
}
}

Loading