Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ never logs. Reusable for other Arc regressions via the same `e2e` log stream.
- **SourceKit cross-file diagnostics lie.** Editing a Swift file in this repo regularly produces `Cannot find type 'PickerViewModel'` / `Cannot find 'PrefsKey'` diagnostics for symbols defined in sibling files. They resolve at build time. Trust `xcodebuild`, not SourceKit, when verifying correctness.
- **Do not switch to ad-hoc signing (`-`).** The "Mojito Dev" self-signed identity is what keeps Accessibility / Input Monitoring TCC grants stable across rebuilds. Every ad-hoc rebuild gets a fresh cdhash and TCC treats it as a different app.
- **Debug builds have their own bundle ID (`ee.wells.Mojito.dev`).** This is intentional — it keeps the dev build's TCC grants, login-item registration, and ⌘-Tab presence fully separate from any released `Mojito.app` on the same Mac. You'll need to grant Accessibility + Input Monitoring once to `Mojito Dev.app` (separately from `Mojito.app`); they'll persist across rebuilds because the "Mojito Dev" code-signing identity is stable. Sparkle is disabled in Debug so the dev build doesn't try to update itself to the release.
- **Tests that need a `UserDefaults` suite must get it from `ScratchDefaults`** (`Tests/MojitoTests/ScratchDefaults.swift`), held as a stored property of the suite. Never `UserDefaults(suiteName: "mojito.tests.…")`: every run leaves a plist in `~/Library/Preferences` that cfprefsd keeps rewriting even after `removePersistentDomain` + deleting the file (W-1030 cleaned up ~5,800 of them).
- **Don't bump the emoji DB by re-running `build_emoji_db.py` alone.** The script pins SHA256 digests of upstream emojibase files in `EXPECTED_SHA256`. Bumping requires manual review + paste of new digests (the script explains the procedure in its header).
- **`SUPublicEDKey` in `project.yml` must be set before any release.** It's substituted into `Resources/Info.plist` at build time via the `info.properties` block, and `scripts/release.sh` aborts if it's empty. Generate via `./bin/generate_keys` (Sparkle stashes the private key in the login keychain, prints the public key; paste public into `project.yml`).

Expand Down
4 changes: 4 additions & 0 deletions Mojito.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
D57C0BA3F93C45531A439D89 /* v07.bin in Resources */ = {isa = PBXBuildFile; fileRef = 7CCBA41F4C17CC47E155A9BE /* v07.bin */; };
D69A33630F4FD6D6EB950C67 /* UpdaterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 702AC2870CD686AB17CC518E /* UpdaterCoordinator.swift */; };
D7FD72169491380D742F69BC /* ChooChooSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = C060B27E6970531BEBA3673E /* ChooChooSound.swift */; };
D8CD1BAA49BB0D0B96A7C2CC /* ScratchDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8D795EF2026F5349717D49 /* ScratchDefaults.swift */; };
DADDDB9ED8378048DF84B51A /* EggSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1732A13138B365E5C880F62 /* EggSound.swift */; };
DCE52DEE8C9CEC354443E7DB /* SettingsRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC142D2205F1C089F7524D2 /* SettingsRoot.swift */; };
DD501615C4A6623E5BA03369 /* EmojiDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B842CBBD6C7466F9BA2786 /* EmojiDatabaseTests.swift */; };
Expand Down Expand Up @@ -262,6 +263,7 @@
561D5F53503A9DDE7BE03430 /* KeyMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyMonitor.swift; sourceTree = "<group>"; };
5A80F3946EA93F8F8A25774E /* TriggerStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerStateMachine.swift; sourceTree = "<group>"; };
5A8851906565F70B7815640F /* FuzzyMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcher.swift; sourceTree = "<group>"; };
5A8D795EF2026F5349717D49 /* ScratchDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScratchDefaults.swift; sourceTree = "<group>"; };
5B9E4C2183154C36F1883004 /* PickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerViewModel.swift; sourceTree = "<group>"; };
5C9E01A82260E8A38DF8D7EA /* DebugReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugReport.swift; sourceTree = "<group>"; };
5FB7AB3DB0E63AF30880FE66 /* BrowserURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserURL.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -426,6 +428,7 @@
AEB65C36C1B1D1A3ECADC6FE /* KlipyAPITests.swift */,
12F71260B63547A6DF1B01AD /* PickerViewModelTests.swift */,
6401923F7190AFC65A7B6959 /* QueryStemmerTests.swift */,
5A8D795EF2026F5349717D49 /* ScratchDefaults.swift */,
9E0C646543028E968FB6287E /* SeasonalGatesTests.swift */,
D74BDC060E2EAACF7A71A3A6 /* SkinToneTests.swift */,
E25EC50F4D58E03E218947DF /* SymbolsDatabaseTests.swift */,
Expand Down Expand Up @@ -1102,6 +1105,7 @@
98422299C980042CFA642B78 /* KlipyAPITests.swift in Sources */,
2C2FD54F6E35CCD0374213BB /* PickerViewModelTests.swift in Sources */,
A11509496D8522DC13BF31F3 /* QueryStemmerTests.swift in Sources */,
D8CD1BAA49BB0D0B96A7C2CC /* ScratchDefaults.swift in Sources */,
C025CD4AE30320C3FE69028A /* SeasonalGatesTests.swift in Sources */,
917B8512B30F3B6E8C9D2B8C /* SkinToneTests.swift in Sources */,
91A5F2A800EA1FC0700394EA /* SymbolsDatabaseTests.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Tests/MojitoTests/AliasTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import Foundation
/// Custom shortcut (alias) storage + validation.
@MainActor
struct AliasStoreTests {
private let scratch = ScratchDefaults()

private func makeStore() -> AliasStore {
let suite = UserDefaults(suiteName: "mojito.tests.alias.\(UUID().uuidString)")!
return AliasStore(defaults: suite)
AliasStore(defaults: scratch.make())
}

@Test func startsEmpty() {
Expand Down Expand Up @@ -61,8 +62,7 @@ struct AliasStoreTests {
}

@Test func persistsAcrossInstances() {
let suiteName = "mojito.tests.alias.persist.\(UUID().uuidString)"
let suite = UserDefaults(suiteName: suiteName)!
let suite = scratch.make()
let first = AliasStore(defaults: suite)
first.add(alias: "check", hexcode: "2705")
let second = AliasStore(defaults: suite)
Expand Down
8 changes: 4 additions & 4 deletions Tests/MojitoTests/FavoritesBrowseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,10 @@ struct TriggerStateMachineBrowseTests {

@MainActor
struct QuickAccessStoreTests {
private let scratch = ScratchDefaults()

private func makeStore() -> QuickAccessStore {
let suite = UserDefaults(suiteName: "mojito.tests.quickaccess.\(UUID().uuidString)")!
return QuickAccessStore(defaults: suite)
QuickAccessStore(defaults: scratch.make())
}

@Test func startsAllAuto() {
Expand Down Expand Up @@ -254,8 +255,7 @@ struct QuickAccessStoreTests {
}

@Test func persistsAcrossInstances() {
let suiteName = "mojito.tests.quickaccess.persist.\(UUID().uuidString)"
let suite = UserDefaults(suiteName: suiteName)!
let suite = scratch.make()
let first = QuickAccessStore(defaults: suite)
first.pin("2764", at: 2)
let second = QuickAccessStore(defaults: suite)
Expand Down
55 changes: 55 additions & 0 deletions Tests/MojitoTests/ScratchDefaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import Testing

/// Hands out throwaway `UserDefaults` suites for store tests and deletes them
/// on deinit.
///
/// Suites are opened by absolute path inside a private temp directory, so they
/// never land in `~/Library/Preferences`. A named suite
/// (`UserDefaults(suiteName: "mojito.tests.…")`) does, and cfprefsd rewrites
/// an empty plist there even after `removePersistentDomain` + deleting the
/// file — that's how thousands of `mojito.tests.*.plist` files piled up (W-1030).
///
/// Keep one as a stored property of the test suite. Swift Testing builds a
/// fresh suite instance per test, so every domain it made is torn down when
/// that test finishes.
final class ScratchDefaults {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("mojito-tests-\(UUID().uuidString)", isDirectory: true)
private(set) var suiteNames: [String] = []
private var suites: [UserDefaults] = []

func make() -> UserDefaults {
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let name = directory.appendingPathComponent(UUID().uuidString).path
let suite = UserDefaults(suiteName: name)!
suiteNames.append(name)
suites.append(suite)
return suite
}

deinit {
// Clear cfprefsd's copy first so it has nothing to flush back to disk.
for (name, suite) in zip(suiteNames, suites) {
suite.removePersistentDomain(forName: name)
}
try? FileManager.default.removeItem(at: directory)
}
}

struct ScratchDefaultsTests {
@Test func suitesLiveInTempAndAreRemovedOnDeinit() throws {
var scratch: ScratchDefaults? = ScratchDefaults()
let suite = try #require(scratch).make()
suite.set("x", forKey: "k")
let name = try #require(scratch?.suiteNames.first)
let directory = try #require(scratch).directory
#expect(name.hasPrefix(FileManager.default.temporaryDirectory.path))
#expect(FileManager.default.fileExists(atPath: directory.path))

scratch = nil

#expect(!FileManager.default.fileExists(atPath: directory.path))
#expect(suite.string(forKey: "k") == nil)
}
}
4 changes: 3 additions & 1 deletion Tests/MojitoTests/TriggerConfigStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import Testing
/// Round-trip persistence and one-time legacy migration of `TriggerConfig`.
struct TriggerConfigStoreTests {

private let scratch = ScratchDefaults()

private func freshSuite() -> UserDefaults {
UserDefaults(suiteName: "mojito.tests.triggers.\(UUID().uuidString)")!
scratch.make()
}

// MARK: Codable round-trip
Expand Down
Loading