From 33ad255cccd62e6f3717bed93809cbce7550f33e Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Sun, 2 Aug 2026 06:32:10 -0500 Subject: [PATCH 1/2] test: reduce Windows CI fixture overhead The plain Windows Go test job is now the CI critical path, with test-only SQLite commits and a probabilistic git stress loop consuming minutes without adding proportional coverage. Keep the real scale and page boundaries, but seed large fixtures transactionally and exercise the close/reopen stress case only under the race detector while retaining a deterministic normal-suite fallback assertion. This reduces serial I/O without increasing test parallelism or weakening the production paths under test. --- internal/db/session_stats_race_test.go | 61 +++++++++++++++++++ internal/db/session_stats_test.go | 52 ++++------------ internal/parser/hermes_provider_test.go | 10 ++- internal/parser/opencode_provider_test.go | 16 ++--- internal/parser/opencode_test.go | 36 ++++++++--- internal/sync/aider_reconciliation_test.go | 5 +- internal/sync/opencode_container_perf_test.go | 32 +++++----- internal/sync/test_helpers_test.go | 24 +++++++- 8 files changed, 164 insertions(+), 72 deletions(-) create mode 100644 internal/db/session_stats_race_test.go diff --git a/internal/db/session_stats_race_test.go b/internal/db/session_stats_race_test.go new file mode 100644 index 000000000..54d01e836 --- /dev/null +++ b/internal/db/session_stats_race_test.go @@ -0,0 +1,61 @@ +//go:build race + +package db + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestOutcomeStatsWriterCloseRaceDoesNotPanic exercises the maintenance +// writer transition under the race detector. The normal suite covers the +// closed-writer state deterministically without paying for hundreds of git +// scans on every platform. +func TestOutcomeStatsWriterCloseRaceDoesNotPanic(t *testing.T) { + skipIfNoGit(t) + d := testDB(t) + repo := statsOutcomeRepo(t) + insertSessionFixture(t, d, sessionFixture{ + id: "writer-race", agent: "claude", userMsgs: 5, + startedAt: hoursAgo(5), cwd: repo, + }) + + done := make(chan struct{}) + toggleErr := make(chan error, 1) + var wg sync.WaitGroup + wg.Go(func() { + for { + select { + case <-done: + return + default: + } + if err := d.CloseWriter(); err != nil { + toggleErr <- err + return + } + if err := d.ReopenWriter(); err != nil { + toggleErr <- err + return + } + } + }) + + for range 300 { + // The race detector validates synchronization; transient errors while + // the writer is closed are acceptable for this stress case. + _, _ = d.GetSessionStats(context.Background(), StatsFilter{ + Since: "28d", IncludeGitOutcomes: true, + }) + } + close(done) + wg.Wait() + select { + case err := <-toggleErr: + require.NoError(t, err, "writer toggling failed") + default: + } +} diff --git a/internal/db/session_stats_test.go b/internal/db/session_stats_test.go index f29598a87..68b1ae932 100644 --- a/internal/db/session_stats_test.go +++ b/internal/db/session_stats_test.go @@ -2612,54 +2612,28 @@ func TestGetSessionStats_OutcomeStats_Happy(t *testing.T) { assert.Nil(t, out.PRsMerged, "PRsMerged want nil (no GHToken)") } -// TestOutcomeStatsWriterCloseRaceDoesNotPanic guards the writer snapshot in -// computeOutcomeStats: closing the writer for a maintenance pass concurrently -// with the git outcome-stats path must fall back to the read-only cache and -// never hand git.NewCache a nil writer pool (which would panic on first use). -func TestOutcomeStatsWriterCloseRaceDoesNotPanic(t *testing.T) { +// TestOutcomeStatsClosedWriterUsesReadOnlyCache guards the writer snapshot in +// computeOutcomeStats: when a maintenance pass has closed the writer, the git +// outcome-stats path must use the read-only cache and return the same result. +// The concurrent close/reopen stress case lives in session_stats_race_test.go. +func TestOutcomeStatsClosedWriterUsesReadOnlyCache(t *testing.T) { skipIfNoGit(t) d := testDB(t) ctx := context.Background() repo := statsOutcomeRepo(t) insertSessionFixture(t, d, sessionFixture{ - id: "race1", agent: "claude", userMsgs: 5, + id: "closed-writer", agent: "claude", userMsgs: 5, startedAt: hoursAgo(5), cwd: repo, }) - done := make(chan struct{}) - toggleErr := make(chan error, 1) - var wg sync.WaitGroup - wg.Go(func() { - for { - select { - case <-done: - return - default: - } - if err := d.CloseWriter(); err != nil { - toggleErr <- err - return - } - if err := d.ReopenWriter(); err != nil { - toggleErr <- err - return - } - } + require.NoError(t, d.CloseWriter(), "close writer") + stats, err := d.GetSessionStats(ctx, StatsFilter{ + Since: "28d", IncludeGitOutcomes: true, }) - - for range 300 { - // Must never panic; a transient error while the writer is closed is fine. - _, _ = d.GetSessionStats(ctx, StatsFilter{ - Since: "28d", IncludeGitOutcomes: true, - }) - } - close(done) - wg.Wait() - select { - case err := <-toggleErr: - require.NoError(t, err, "writer toggling failed") - default: - } + require.NoError(t, err, "GetSessionStats with closed writer") + require.NotNil(t, stats.OutcomeStats, "OutcomeStats") + assert.Equal(t, 1, stats.OutcomeStats.ReposActive, "ReposActive") + assert.Equal(t, 3, stats.OutcomeStats.Commits, "Commits") } // TestGetSessionStats_OutcomeStats_NoCwd verifies that sessions without diff --git a/internal/parser/hermes_provider_test.go b/internal/parser/hermes_provider_test.go index 6eb50844b..9a47b8ced 100644 --- a/internal/parser/hermes_provider_test.go +++ b/internal/parser/hermes_provider_test.go @@ -842,19 +842,23 @@ func TestHermesMemberCoreSeedRetainedIDBytesStayBounded(t *testing.T) { stateDB := filepath.Join(root, "state.db") conn, err := sql.Open("sqlite3", stateDB) require.NoError(t, err) - _, err = conn.Exec("DELETE FROM messages; DELETE FROM sessions") + tx, err := conn.Begin() + require.NoError(t, err) + defer func() { _ = tx.Rollback() }() + _, err = tx.Exec("DELETE FROM messages; DELETE FROM sessions") require.NoError(t, err) for i := range sessionCount { id := fmt.Sprintf("member-%06d", i) - _, err = conn.Exec(`INSERT INTO sessions + _, err = tx.Exec(`INSERT INTO sessions (id, source, started_at, estimated_cost_usd, actual_cost_usd) VALUES (?, 'cli', ?, 0, 0)`, id, i) require.NoError(t, err) - _, err = conn.Exec(`INSERT INTO messages + _, err = tx.Exec(`INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, 'user', 'hello', ?)`, id, i) require.NoError(t, err) } + require.NoError(t, tx.Commit()) require.NoError(t, conn.Close()) var retained, peak int64 diff --git a/internal/parser/opencode_provider_test.go b/internal/parser/opencode_provider_test.go index a5b2ede33..ef2d0e4bb 100644 --- a/internal/parser/opencode_provider_test.go +++ b/internal/parser/opencode_provider_test.go @@ -1386,13 +1386,15 @@ func TestOpenCodeChangedPathWatermarkMergeMaterializesOnlyChangedBatch( seeder.AddProject("proj", "/home/user/app") const base = int64(1779012000000) var stored []StoredMemberFreshness - for i := range sessions { - id := fmt.Sprintf("ses-%06d", i) - seeder.AddSession(id, "proj", "", id, base, base) - stored = append(stored, StoredMemberFreshness{ - Path: dbPath + "#" + id, CoveredThroughNS: base * 1_000_000, - }) - } + seeder.InTransaction(func(seeder *OpenCodeSeeder) { + for i := range sessions { + id := fmt.Sprintf("ses-%06d", i) + seeder.AddSession(id, "proj", "", id, base, base) + stored = append(stored, StoredMemberFreshness{ + Path: dbPath + "#" + id, CoveredThroughNS: base * 1_000_000, + }) + } + }) // One session advances past its stored coverage. changed := fmt.Sprintf("ses-%06d", sessions/2) _, err := seeder.db.ExecContext(t.Context(), diff --git a/internal/parser/opencode_test.go b/internal/parser/opencode_test.go index ce31f9523..dd8a14fd3 100644 --- a/internal/parser/opencode_test.go +++ b/internal/parser/opencode_test.go @@ -91,14 +91,36 @@ func assertEq[T comparable](t *testing.T, name string, got, want T) { assert.Equal(t, want, got, name) } +type openCodeSeedExecer interface { + Exec(query string, args ...any) (sql.Result, error) +} + type OpenCodeSeeder struct { - db *sql.DB - t *testing.T + db *sql.DB + exec openCodeSeedExecer + t *testing.T +} + +func (s *OpenCodeSeeder) executor() openCodeSeedExecer { + if s.exec != nil { + return s.exec + } + return s.db +} + +func (s *OpenCodeSeeder) InTransaction(seed func(*OpenCodeSeeder)) { + s.t.Helper() + tx, err := s.db.Begin() + require.NoError(s.t, err, "begin seed transaction") + defer func() { _ = tx.Rollback() }() + + seed(&OpenCodeSeeder{db: s.db, exec: tx, t: s.t}) + require.NoError(s.t, tx.Commit(), "commit seed transaction") } func (s *OpenCodeSeeder) AddProject(id, worktree string) { s.t.Helper() - _, err := s.db.Exec(`INSERT INTO project (id, worktree) VALUES (?, ?)`, id, worktree) + _, err := s.executor().Exec(`INSERT INTO project (id, worktree) VALUES (?, ?)`, id, worktree) require.NoError(s.t, err, "add project") } @@ -115,7 +137,7 @@ func (s *OpenCodeSeeder) AddSession(id, projectID, parentID, title string, timeC // Omit directory so the same helper works on legacy schemas that // lack the column; modern fixtures default directory to ''. - _, err := s.db.Exec( + _, err := s.executor().Exec( `INSERT INTO session (id, project_id, parent_id, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)`, @@ -138,7 +160,7 @@ func (s *OpenCodeSeeder) AddSessionDirectory( tStr = title } - _, err := s.db.Exec( + _, err := s.executor().Exec( `INSERT INTO session (id, project_id, parent_id, title, directory, time_created, time_updated) @@ -150,14 +172,14 @@ func (s *OpenCodeSeeder) AddSessionDirectory( func (s *OpenCodeSeeder) AddMessage(id, sessionID string, timeCreated, timeUpdated int64, data string) { s.t.Helper() - _, err := s.db.Exec(`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)`, + _, err := s.executor().Exec(`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)`, id, sessionID, timeCreated, timeUpdated, data) require.NoError(s.t, err, "add message") } func (s *OpenCodeSeeder) AddPart(id, messageID, sessionID string, timeCreated, timeUpdated int64, data string) { s.t.Helper() - _, err := s.db.Exec(`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)`, + _, err := s.executor().Exec(`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)`, id, messageID, sessionID, timeCreated, timeUpdated, data) require.NoError(s.t, err, "add part") } diff --git a/internal/sync/aider_reconciliation_test.go b/internal/sync/aider_reconciliation_test.go index 79f10e7fd..ad0626cf0 100644 --- a/internal/sync/aider_reconciliation_test.go +++ b/internal/sync/aider_reconciliation_test.go @@ -60,7 +60,10 @@ func TestReconcileWatchRootsAiderScansOneLargeContainerOnce(t *testing.T) { repo := filepath.Join(root, "repo") require.NoError(t, os.MkdirAll(repo, 0o755)) var history strings.Builder - for i := range 600 { + // Cross the reconciliation page boundary so the fixture exercises a + // multi-page container without paying to archive hundreds of redundant + // rows beyond the boundary. + for i := range reconciliationPageSize + 1 { history.WriteString("# aider chat started at 2026-06-09 14:01:00\n") history.WriteString("#### prompt ") history.WriteString(strings.Repeat("x", i%17+1)) diff --git a/internal/sync/opencode_container_perf_test.go b/internal/sync/opencode_container_perf_test.go index 261dfd67f..2993060a0 100644 --- a/internal/sync/opencode_container_perf_test.go +++ b/internal/sync/opencode_container_perf_test.go @@ -34,13 +34,15 @@ func TestOpenCodeSharedContainerChangeIsPerSessionBounded(t *testing.T) { env := setupSingleAgentTestEnv(t, parser.AgentOpenCode) oc := createOpenCodeDB(t, env.opencodeDir) oc.addProject(t, "proj", "/home/user/code/app") - for i := range n { - seedOpenCodeSQLiteTextSession( - t, oc, "proj", fmt.Sprintf("ses%05d", i), - 1779012000000, 1779012030000, - "prompt", "answer", - ) - } + oc.inTransaction(t, func(oc *openCodeTestDB) { + for i := range n { + seedOpenCodeSQLiteTextSession( + t, oc, "proj", fmt.Sprintf("ses%05d", i), + 1779012000000, 1779012030000, + "prompt", "answer", + ) + } + }) require.Equal(t, n, env.engine.SyncAll(context.Background(), nil).Synced) @@ -88,13 +90,15 @@ func TestOpenCodeWatcherEventIsWatermarkBounded(t *testing.T) { env := setupSingleAgentTestEnv(t, parser.AgentOpenCode) oc := createOpenCodeDB(t, env.opencodeDir) oc.addProject(t, "proj", "/home/user/code/app") - for i := range n { - seedOpenCodeSQLiteTextSession( - t, oc, "proj", fmt.Sprintf("ses%05d", i), - 1779012000000, 1779012030000, - "prompt", "answer", - ) - } + oc.inTransaction(t, func(oc *openCodeTestDB) { + for i := range n { + seedOpenCodeSQLiteTextSession( + t, oc, "proj", fmt.Sprintf("ses%05d", i), + 1779012000000, 1779012030000, + "prompt", "answer", + ) + } + }) require.Equal(t, n, env.engine.SyncAll(context.Background(), nil).Synced) diff --git a/internal/sync/test_helpers_test.go b/internal/sync/test_helpers_test.go index fa9067b0f..cf167cb53 100644 --- a/internal/sync/test_helpers_test.go +++ b/internal/sync/test_helpers_test.go @@ -164,9 +164,14 @@ func (e *testEnv) updateSessionProject( } // openCodeTestDB manages an OpenCode SQLite database for tests. +type openCodeTestExecer interface { + Exec(query string, args ...any) (sql.Result, error) +} + type openCodeTestDB struct { path string db *sql.DB + exec openCodeTestExecer } type kiroSQLiteTestDB struct { @@ -433,10 +438,27 @@ func writeLegacyKiroSession( func (oc *openCodeTestDB) mustExec(t *testing.T, msg, query string, args ...any) { t.Helper() - _, err := oc.db.Exec(query, args...) + executor := oc.exec + if executor == nil { + executor = oc.db + } + _, err := executor.Exec(query, args...) require.NoError(t, err, msg) } +func (oc *openCodeTestDB) inTransaction( + t *testing.T, + seed func(*openCodeTestDB), +) { + t.Helper() + tx, err := oc.db.Begin() + require.NoError(t, err, "begin OpenCode seed transaction") + defer func() { _ = tx.Rollback() }() + + seed(&openCodeTestDB{path: oc.path, db: oc.db, exec: tx}) + require.NoError(t, tx.Commit(), "commit OpenCode seed transaction") +} + func (oc *openCodeTestDB) addProject( t *testing.T, id, worktree string, ) { From aac8b760ca01d7047f8a36e573e842ca8dd5b1e5 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Sun, 2 Aug 2026 08:42:05 -0500 Subject: [PATCH 2/2] ci: shorten Windows Go test critical path The four-core Windows runner spends several minutes with a package worker occupied by the CGo-heavy DuckDB suite while other long SQLite packages compete for the remaining workers. Run DuckDB on an independent Windows runner so both test sets advance concurrently, and keep routine Windows output non-verbose to avoid unnecessary log I/O.\n\nArtifact export boundary tests also created thousands of deliberately bare rows through individual commits. Seed those rows transactionally while retaining the real queue triggers, 1,024-row claim boundary, and observable export assertions; the affected tests dropped from 17.1 to 11.0 seconds locally. --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++- internal/artifact/export_test.go | 67 ++++++++++++++++++++------------ 2 files changed, 102 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f395b8100..df120d79d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,8 +164,24 @@ jobs: - name: Restore pricing snapshot run: go run ./internal/pricing/cmd/litellm-snapshot -restore - - name: Run Go tests - run: go test -tags "fts5" ${{ runner.os == 'Linux' && '-coverprofile=coverage.out' || '' }} ./... -v -count=1 -timeout=20m + - name: Run Go tests (Linux) + if: runner.os == 'Linux' + run: go test -tags "fts5" -coverprofile=coverage.out ./... -v -count=1 -timeout=20m + env: + CGO_ENABLED: "1" + + - name: Run Go tests (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + # DuckDB runs in its own job below so its CGo-heavy package does not + # occupy one of this four-core runner's package workers for minutes. + $packages = @(go list ./...) + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $packages = $packages | Where-Object { $_ -ne 'go.kenn.io/agentsview/internal/duckdb' } + go test -tags "fts5" $packages -count=1 -timeout=20m env: CGO_ENABLED: "1" @@ -185,6 +201,47 @@ jobs: if: runner.os == 'Linux' && steps.codecov.outcome == 'failure' run: echo "::warning::Codecov upload failed" + # Keep the derived-mirror suite concurrent with the main Windows package set. + test-duckdb-windows: + name: Go Test (windows-latest, DuckDB) + runs-on: windows-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + + - name: Setup MinGW + id: setup_mingw + continue-on-error: true + uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 + with: + msystem: MINGW64 + update: false + install: mingw-w64-x86_64-gcc + path-type: inherit + + - name: Setup MinGW (retry on transient failure) + if: steps.setup_mingw.outcome == 'failure' + uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 + with: + msystem: MINGW64 + update: false + install: mingw-w64-x86_64-gcc + path-type: inherit + + - name: Restore pricing snapshot + run: go run ./internal/pricing/cmd/litellm-snapshot -restore + + - name: Run DuckDB tests + run: go test -tags "fts5" ./internal/duckdb -count=1 -timeout=20m + env: + CGO_ENABLED: "1" + test-race: name: Go Test (race detector) runs-on: ${{ github.repository == 'kenn-io/agentsview' && (github.event_name == 'push' && github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.base.repo.full_name == github.repository)) && 'kenn-linux-x64-public' || 'ubuntu-latest' }} diff --git a/internal/artifact/export_test.go b/internal/artifact/export_test.go index e7278fcdc..a36e21ae1 100644 --- a/internal/artifact/export_test.go +++ b/internal/artifact/export_test.go @@ -2,6 +2,7 @@ package artifact import ( "context" + "database/sql" "encoding/json" "errors" "fmt" @@ -45,6 +46,38 @@ func testExportDB(t *testing.T) *db.DB { return database } +// seedBareExportSessions inserts the minimal rows these export cardinality +// tests need in one transaction. The real sessions-table export triggers still +// enqueue every local row; parser validation and message replacement are not +// part of the behavior under test here. +func seedBareExportSessions( + t *testing.T, + database *db.DB, + count int, + idFormat string, + machine string, +) { + t.Helper() + ctx := t.Context() + require.NoError(t, database.Update(func(tx *sql.Tx) error { + stmt, err := tx.PrepareContext(ctx, `INSERT INTO sessions + (id, project, machine, agent, created_at) + VALUES (?, 'project', ?, 'claude', '2026-06-14T01:02:03Z')`) + if err != nil { + return err + } + defer func() { _ = stmt.Close() }() + for i := range count { + if _, err := stmt.ExecContext( + ctx, fmt.Sprintf(idFormat, i), machine, + ); err != nil { + return err + } + } + return nil + }), "seed bare export sessions") +} + // latestStoreCheckpointForTest locates the highest-sequence checkpoint for // origin and decodes its full session map. It stands in for peer.go's // latestStoreCheckpointSummary (out of PR2 scope; see plan hazard 6) so @@ -1364,12 +1397,9 @@ func TestArtifactExportCardinalityLoadsOnlyDirtyBatch(t *testing.T) { for _, archiveSize := range []int{20, 2000} { t.Run(fmt.Sprintf("archive-%d", archiveSize), func(t *testing.T) { database := testExportDB(t) - for i := range archiveSize { - require.NoError(t, database.UpsertSession(db.Session{ - ID: fmt.Sprintf("peer-%04d", i), Project: "project", - Machine: "peer-a1b2c3", Agent: "claude", - })) - } + seedBareExportSessions( + t, database, archiveSize, "peer-%04d", "peer-a1b2c3", + ) require.NoError(t, database.UpsertSession(db.Session{ ID: "dirty", Project: "project", Machine: "local", Agent: "claude", })) @@ -1404,12 +1434,9 @@ func TestExportToStoreCardinalityIgnoresUnrelatedArchiveBodies(t *testing.T) { for _, archiveSize := range []int{20, 2000} { t.Run(fmt.Sprintf("archive-%d", archiveSize), func(t *testing.T) { database := testExportDB(t) - for i := range archiveSize { - require.NoError(t, database.UpsertSession(db.Session{ - ID: fmt.Sprintf("peer-%04d", i), Project: "project", - Machine: "peer-a1b2c3", Agent: "claude", - })) - } + seedBareExportSessions( + t, database, archiveSize, "peer-%04d", "peer-a1b2c3", + ) seedSession(t, database, "dirty", "project") counted := &countingCanonicalExportDB{DB: database} filesystem, err := newProtocolTestStore(t.TempDir()) @@ -1473,12 +1500,9 @@ func TestExportToStoreIncrementalBatchIsBoundedAndFullStreamsAllBodies(t *testin func TestExportToStoreFullDrainsMoreThanOneClaimPage(t *testing.T) { database := testExportDB(t) const total = 1025 - for i := range total { - require.NoError(t, database.UpsertSession(db.Session{ - ID: fmt.Sprintf("session-%04d", i), Project: "project", - Machine: "local", Agent: "claude", CreatedAt: "2026-06-14T01:02:03Z", - })) - } + seedBareExportSessions( + t, database, total, "session-%04d", "local", + ) counted := &countingCanonicalExportDB{DB: database} filesystem, err := newProtocolTestStore(t.TempDir()) require.NoError(t, err) @@ -1512,12 +1536,7 @@ func TestExportToStoreFullDrainsMoreThanOneClaimPage(t *testing.T) { func TestExportToStoreExplicitSessionIDsClaimBeyondOldestQueuePage(t *testing.T) { database := testExportDB(t) const total = 1025 - for i := range total { - require.NoError(t, database.UpsertSession(db.Session{ - ID: fmt.Sprintf("session-%04d", i), Project: "project", - Machine: "local", Agent: "claude", - })) - } + seedBareExportSessions(t, database, total, "session-%04d", "local") filesystem, err := newProtocolTestStore(t.TempDir()) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, filesystem.Close()) })