Skip to content

Make ServerStore instance-based so tests can't poison production servers.json#34

Merged
buggerman merged 1 commit into
mainfrom
serverstore-injection
Apr 26, 2026
Merged

Make ServerStore instance-based so tests can't poison production servers.json#34
buggerman merged 1 commit into
mainfrom
serverstore-injection

Conversation

@buggerman
Copy link
Copy Markdown
Owner

Summary

  • Fix a serious test-pollution bug: ServerStore was a static-method holder that always wrote to ~/Library/Application Support/Brygga/servers.json. Any test that constructed AppState() and triggered persist() (e.g. via closePrivateMessage) overwrote the user's real server config with the test fixtures (ServerA @ a.example.org, ServerB @ b.example.org). Subsequent app launches restored the test fixtures, the user saw their real servers gone, and the scrollback for those servers became orphaned under their original UUIDs.
  • Refactor ServerStore from enum of static methods to final class with instance methods + init(root:). Production code uses ServerStore.shared; tests construct their own ServerStore(root: tempDir).
  • AppState.init(store:) requires a store explicitly — no default. A test that forgets to inject a temp store is now a compile error rather than a silent prod overwrite.

Changes

  • Sources/BryggaCore/Persistence/ServerStore.swift — converted from enum to final class: Sendable. Removed static fileURL(), load(), save(). Added init(root: URL? = nil), instance load() and save(_:), public fileURL: URL. Added static let shared = ServerStore() for the prod default. Snapshot and ServerConfig nested types are unchanged so existing ServerStore.ServerConfig references in AppState keep working.
  • Sources/BryggaCore/Models/AppState.swift — added private let store: ServerStore field. Removed init(), replaced with init(store: ServerStore). restoreFromStore() now calls store.load(); persist() calls store.save(...).
  • Sources/Brygga/BryggaApp.swiftAppState()AppState(store: .shared). Sole production call site.
  • Tests/AppStateNavigationTests.swift — added makeStore() helper that builds a ServerStore rooted at a unique tempdir. All AppState(...) constructors now use it. Added testPersistWritesToInjectedStoreNotProduction regression test that asserts a persist()-triggering mutation lands under the injected store's path, not the prod path.

Test plan

  • swift build — passes
  • swift test — 132 tests pass (+1 regression test)
  • swiftformat --lint . — clean
  • Manual verification that swift test no longer touches the prod path:
    stat -f '%Sm  %z bytes' ~/Library/Application\ Support/Brygga/servers.json
    swift test > /dev/null
    stat -f '%Sm  %z bytes' ~/Library/Application\ Support/Brygga/servers.json
    mtime is unchanged across the test run. Verified locally.

Risk / rollback

  • API change: AppState.init() is removed. Only call site (BryggaApp.swift) is updated in this PR. Any external consumer of AppState() would need to pass ServerStore.shared.
  • The default init(root: nil) on ServerStore keeps prod behavior identical — same JSON file path, same schema, no migration needed.
  • Revert: this single commit. Reverting reintroduces the bug.

Follow-ups (separate PRs)

  1. Recovery utility — small script to walk scrollback/<UUID>/ orphans, print channels found in each, let user rebind one to a fresh Server entry by editing servers.json's id field.
  2. Same injection treatment for ScrollbackStore.shared and ScrollbackIndex.shared — those also leaked into production from IRCSessionTests, though the impact is tiny appended-row pollution rather than data overwrite.

@buggerman buggerman merged commit 3483a2e into main Apr 26, 2026
1 check passed
@buggerman buggerman deleted the serverstore-injection branch April 26, 2026 21:09
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