Skip to content

Commit 5c1399a

Browse files
authored
fix(core): stop AppMeta trapping when there is no app bundle (#762)
AppMeta force-unwrapped both Info.plist lookups, so reading version or build outside an app bundle trapped. The FlipcashCore package test host has no bundle, so UserAgentClientInterceptorTests took the whole process down with SIGTRAP and no suite in the package could run under `swift test`. Only filtered runs that excluded that test worked. Both lookups now fall back to AppMeta.unknown. The stand-in is deliberately non-numeric: SessionAuthenticator.requiresUpgrade parses build and treats an unparseable value as "allow access", whereas "0" would parse as a build below any server minimum and gate the app behind a forced upgrade it can never satisfy. The package already compiled for macOS, so the trap was the only thing in the way: `swift test` now runs all 933 tests. That makes the testing doc's blanket "packages are iOS-only" rule wrong for FlipcashCore and FlipcashCoreVectors, which declare .macOS, so it is narrowed to the packages where it still holds. UserAgentClientInterceptorTests is unchanged. The new AppMetaTests assert only what is true under both hosts, since the app target does have a bundle.
1 parent c7b18c4 commit 5c1399a

3 files changed

Lines changed: 46 additions & 3 deletions

File tree

‎.claude/docs/testing.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ snapshots. The crash is inside the sanitizer, not app code, in both cases. Detai
5353
repro steps in [2026-08-25-tsan-ui-test-crash.md](../plans/2026-08-25-tsan-ui-test-crash.md).
5454
Don't move TSan back onto `AllTargets` without re-checking that analysis against the current Xcode.
5555

56-
**Never run `swift test` in a package directory** (`FlipcashCore`, `FlipcashUI`, etc.). Packages are iOS-only; `swift test` targets the macOS host and fails with code-signing errors. Always go through `./Scripts/test.sh` (which routes through the `Flipcash` scheme on the iOS Simulator).
56+
**`swift test` works in `FlipcashCore` only.** It and `FlipcashCoreVectors` declare `.macOS`, so `swift test` builds and runs their suites on the macOS host. Every other package (`FlipcashUI`, `FlipcashAPI`, `CodeScanner`, `CrossPlatformVectors`) is iOS-only and cannot be tested that way.
57+
58+
A macOS run has no app bundle, so anything reading `Bundle.main` sees nil — `AppMeta` returns `AppMeta.unknown` instead of trapping. A test that needs a real bundle value belongs in a simulator run.
59+
60+
`./Scripts/test.sh` (the `Flipcash` scheme on the iOS Simulator) stays the check that counts, because it runs on the platform we ship. Treat `swift test` as a fast inner loop for pure-logic suites, not a substitute for it.
5761

5862
For paired-device builds, see [Xcode MCP Server](quick-reference.md#xcode-mcp-server).
5963

‎FlipcashCore/Sources/FlipcashCore/Clients/AppMeta.swift‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66
import Foundation
77

88
public enum AppMeta {
9+
10+
/// Stand-in returned when the host has no Info.plist value, as in a package test host.
11+
public static let unknown = "unknown"
12+
13+
/// The app's marketing version, or ``unknown`` when the host bundle doesn't declare one.
914
public static var version: String {
10-
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
15+
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? unknown
1116
}
1217

18+
/// The app's build number, or ``unknown`` when the host bundle doesn't declare one.
19+
///
20+
/// Deliberately not a numeric stand-in: `SessionAuthenticator.requiresUpgrade` parses this and
21+
/// treats an unparseable value as "allow access", whereas `"0"` would parse and read as a build
22+
/// older than any server minimum, gating the app behind a forced upgrade it can never satisfy.
1323
public static var build: String {
14-
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
24+
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? unknown
1525
}
1626
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// AppMetaTests.swift
3+
// FlipcashCoreTests
4+
//
5+
6+
import Testing
7+
@testable import FlipcashCore
8+
9+
@Suite("AppMeta — Info.plist lookups degrade instead of trapping")
10+
struct AppMetaTests {
11+
12+
// Assertions hold under both hosts: the package test host has no Info.plist and gets the
13+
// stand-in, the app target has one and gets the real value.
14+
15+
@Test("Version reads without trapping and is never empty")
16+
func versionDegrades() {
17+
#expect(!AppMeta.version.isEmpty)
18+
}
19+
20+
@Test("Build reads without trapping and is never empty")
21+
func buildDegrades() {
22+
#expect(!AppMeta.build.isEmpty)
23+
}
24+
25+
@Test("The build stand-in stays unparseable, so requiresUpgrade allows access")
26+
func buildStandInIsNotNumeric() {
27+
#expect(UInt32(AppMeta.unknown) == nil)
28+
}
29+
}

0 commit comments

Comments
 (0)