refactor: remove obsolete code and redundant tests
Remove legacy Commerce types, unused helpers, duplicate SFTP progress code, and 28 unreferenced strings. Drop fixture-only tests and arbitrary screenshot size assertions while retaining behavioral coverage. Validation: 2,592 macOS tests, 40 focused macOS tests, and 53 iOS tests passed. Swift formatting, AppStrings, and Chinese localization checks passed.
This commit is contained in:
1 parent
8450234179
commit
3708a7c54f
48 files changed
+24
-787
No files matched your search
@@ -52,11 +52,13 @@ Follow `.swift-format`: four spaces and 120 columns. Use PascalCase types, camel
|
||||
|
||||
Use Swift Testing (`@Suite`, `@Test`, `#expect`) in the three test directories. Add regression coverage and run the default test suite before submission. Web changes must pass `npm run build`; verify English and `/zh/` routes visually.
|
||||
|
||||
Assert observable behavior and regressions. Avoid assertions that only repeat fixture values, compare identical constructions, or impose arbitrary PNG byte-count thresholds. Keep screenshot export helpers opt-in for visual review.
|
||||
|
||||
Test SwiftData migrations by opening historical stores and verifying retained data after reopening. Do not assert that the latest schema equals a hard-coded version number; adding a migration must not invalidate unrelated tests.
|
||||
|
||||
macOS tests that present windows or activate the app must use `.requiresVisibleWindow` on their suite or individual test. They are skipped by default; keep this opt-in off for routine verification. To opt in from the command line, prefix the test command with `TEST_RUNNER_TERMIND_WINDOW_TESTS=1` (Xcode forwards it as `TERMIND_WINDOW_TESTS=1` to the test host), preferably with `-only-testing:TFMTests/<Suite>` to limit the run. In Xcode, set `TERMIND_WINDOW_TESTS=1` in the test action's environment. Screenshot tests with existing dedicated opt-in variables keep those gates. Offscreen layout and logic tests remain enabled by default.
|
||||
|
||||
StoreKit tests require manual authorization and are also skipped by default. Run them explicitly with `TERMIND_STOREKIT_TESTS=1`, preferably with `-only-testing:TFMTests/StoreKitTrialTests`.
|
||||
Commerce access and paywall-state behavior are covered by `RevenueCatCommerceTests`, `ProPaywallStateTests`, and feature-access tests. `StoreKit/Termind.storekit` is a manual development fixture; live products, trial eligibility, and purchase/restore behavior require RevenueCat and App Store Sandbox validation.
|
||||
|
||||
Mobile key-window integration suites are skipped by default because they share UIKit's process-wide presentation state. Run them explicitly with `TEST_RUNNER_TERMIND_MOBILE_UI_TESTS=1` (Xcode forwards it as `TERMIND_MOBILE_UI_TESTS=1`), `-parallel-testing-enabled NO`, and narrow `-only-testing:TFITests/<Suite>` filters.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ struct RevenueCatCommerceTests {
|
||||
@Test
|
||||
func onlyTheConfiguredEntitlementUnlocksPro() throws {
|
||||
let info = try customerInfo(entitlementID: "another_entitlement", expiration: now.addingTimeInterval(60))
|
||||
#expect(resolve(info) == .free(trialAvailable: false))
|
||||
#expect(resolve(info) == .free)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,7 +85,7 @@ struct RevenueCatCommerceTests {
|
||||
#expect(resolve(try customerInfo(expiration: nil, verification: 2)) == .verificationFailed)
|
||||
}
|
||||
|
||||
@Test(arguments: [EntitlementStatus.purchased(.purchased), .free(trialAvailable: false)])
|
||||
@Test(arguments: [EntitlementStatus.purchased(.purchased), .free])
|
||||
func remotePaywallCompletionUpdatesAccessWithoutClosingItsAccountVariant(status: EntitlementStatus) async {
|
||||
let service = FakeService()
|
||||
let controller = ProAccessController(purchaseService: service, observeUpdates: false)
|
||||
@@ -97,7 +97,7 @@ struct RevenueCatCommerceTests {
|
||||
#expect(controller.isPaywallPresented)
|
||||
#expect(!controller.isProcessing)
|
||||
|
||||
service.status = .free(trialAvailable: false)
|
||||
service.status = .free
|
||||
await controller.refresh()
|
||||
#expect(!controller.hasProAccess)
|
||||
#expect(controller.isPaywallPresented)
|
||||
@@ -111,7 +111,7 @@ struct RevenueCatCommerceTests {
|
||||
service.continuation.yield(.purchased(.purchased))
|
||||
for _ in 0..<100 where !controller.hasProAccess { await Task.yield() }
|
||||
#expect(controller.hasProAccess)
|
||||
service.continuation.yield(.free(trialAvailable: false))
|
||||
service.continuation.yield(.free)
|
||||
for _ in 0..<100 where controller.hasProAccess { await Task.yield() }
|
||||
#expect(!controller.hasProAccess)
|
||||
}
|
||||
@@ -127,7 +127,7 @@ struct RevenueCatCommerceTests {
|
||||
await controller.refresh()
|
||||
service.status = .purchased(.familyShared)
|
||||
await controller.refresh()
|
||||
service.status = .free(trialAvailable: false)
|
||||
service.status = .free
|
||||
await controller.refresh()
|
||||
#expect(observed == [true, false])
|
||||
#expect(!controller.isPaywallPresented)
|
||||
@@ -145,7 +145,7 @@ struct RevenueCatCommerceTests {
|
||||
service.continuation.yield(.purchased(.purchased))
|
||||
for _ in 0..<100 where !controller.hasProAccess { await Task.yield() }
|
||||
#expect(controller.hasProAccess)
|
||||
service.refreshContinuation?.resume(returning: .free(trialAvailable: false))
|
||||
service.refreshContinuation?.resume(returning: .free)
|
||||
service.refreshContinuation = nil
|
||||
await refresh.value
|
||||
#expect(controller.hasProAccess)
|
||||
@@ -170,7 +170,7 @@ struct RevenueCatCommerceTests {
|
||||
let newerResponse = try #require(service.refreshContinuation)
|
||||
service.refreshContinuation = nil
|
||||
|
||||
olderResponse.resume(returning: .free(trialAvailable: false))
|
||||
olderResponse.resume(returning: .free)
|
||||
await olderRefresh.value
|
||||
#expect(controller.hasProAccess)
|
||||
newerResponse.resume(returning: .purchased(.familyShared))
|
||||
@@ -267,7 +267,7 @@ struct RevenueCatCommerceTests {
|
||||
private final class FakeService: ProPurchaseService {
|
||||
let updates: AsyncStream<EntitlementStatus>
|
||||
let continuation: AsyncStream<EntitlementStatus>.Continuation
|
||||
var status: EntitlementStatus = .free(trialAvailable: false)
|
||||
var status: EntitlementStatus = .free
|
||||
var refreshes = 0
|
||||
var refreshFails = false
|
||||
var suspendsRefresh = false
|
||||
|
||||
@@ -211,57 +211,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ every %@. Auto-renews until canceled." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$@每%2$@。除非取消,否则会自动续订。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$@每%2$@。除非取消,否則會自動續訂。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ free" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "免费%@"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "免費%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ free, then %@ per %@. Auto-renews until canceled." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "免费%1$@,之后每%3$@收费%2$@。除非取消,否则会自动续订。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "免費%1$@,之後每%3$@收費%2$@。除非取消,否則會自動續訂。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ on %@" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -285,23 +234,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ per %@. Auto-renews until canceled." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "每%2$@收费%1$@。除非取消,否则会自动续订。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "每%2$@收費%1$@。除非取消,否則會自動續訂。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ provider value" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -2042,25 +1974,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AI provider usage and charges are separate from Termind Pro." : {
|
||||
"comment" : "A description of the difference between Termind Pro and the AI provider.",
|
||||
"extractionState" : "manual",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "AI提供方的用量与费用不包含在Termind Pro中。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "AI供應商的用量與費用不包含在Termind Pro中。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"API Key" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -2131,23 +2044,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Access Ends" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "权益结束时间"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "權益結束時間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Action" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -3359,23 +3255,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Annual · Family Shared" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "年度订阅·家庭共享"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "年度訂閱·家庭共享"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Another MCP server already uses this ID." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -4574,40 +4453,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Buy Lifetime Access" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "购买永久版"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "購買永久版"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Buy Lifetime Access — %@" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "购买永久版—%@"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "購買永久版—%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Buy Termind Pro" : {
|
||||
"comment" : "A button that purchases Termind Pro.",
|
||||
"extractionState" : "manual",
|
||||
@@ -9455,25 +9300,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Development Access" : {
|
||||
"comment" : "A label for a user with access to all features.",
|
||||
"extractionState" : "manual",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "开发环境权限"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "開發環境權限"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Device" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -11505,23 +11331,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Expired" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "过期时间"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "過期時間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Export" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -12593,23 +12402,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Free for %@, then %@ every %@. Auto-renews until canceled." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "免费%1$@,之后每%3$@收费%2$@。除非取消,否则会自动续订。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "免費%1$@,之後每%3$@收費%2$@。除非取消,否則會自動續訂。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Free includes up to 3 monitoring cards. Upgrade to Pro for more." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -12984,23 +12776,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Grace Period Ends" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "宽限期结束时间"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "寬限期結束時間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Green" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -18068,25 +17843,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"No subscription or automatic renewal" : {
|
||||
"comment" : "A description of a user's subscription status.",
|
||||
"extractionState" : "manual",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无订阅或自动续费"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "無訂閱或自動續費"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"No supported private key in this file." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -18663,23 +18419,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"One payment. No subscription or automatic renewal." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "一次付费,无订阅或自动续订。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "一次付費,無訂閱或自動續訂。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"One purchase for AI, more monitored servers, remote MCP tools, and iCloud sync." : {
|
||||
"comment" : "A summary of Termind Pro features available on iOS.",
|
||||
"extractionState" : "manual",
|
||||
@@ -21899,23 +21638,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Received text is not valid %@." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "收到的文本不是有效的%@。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "收到的文字不是有效的%@。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Recent Workspaces" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -22667,23 +22389,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Renews" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "续订时间"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "續訂時間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reopen Closed Workspace" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -26447,23 +26152,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start Free Trial" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "开始免费试用"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "開始免費試用"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start a Conversation" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -26957,40 +26645,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Subscribe Annually" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "订阅年度方案"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "訂閱年度方案"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Subscribe Annually — %@" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "订阅年度方案—%@"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "訂閱年度方案—%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Succeeded · %@" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -27683,42 +27337,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text cannot be represented using %@." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "文本无法使用%@表示。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "文字無法使用%@表示。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"The App Store is temporarily unavailable. You can keep using already unlocked features." : {
|
||||
"comment" : "Error message displayed when the App Store is temporarily unavailable.",
|
||||
"extractionState" : "manual",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "App Store暂时不可用,已经解锁的功能仍可继续使用。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "App Store暫時無法使用,已經解鎖的功能仍可繼續使用。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"The Chat model has no available credential." : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -29274,25 +28892,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"The purchase is waiting for App Store approval." : {
|
||||
"comment" : "Message displayed when a purchase is pending approval.",
|
||||
"extractionState" : "manual",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "购买正在等待App Store批准。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "購買正在等待App Store核准。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"The remote file changed or could not be verified. Replace it with your saved local changes?" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -31610,25 +31209,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Trial Ends" : {
|
||||
"comment" : "A label that displays the date when a user's trial of Termind Pro will end.",
|
||||
"extractionState" : "manual",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "试用结束时间"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "試用結束時間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Trust and Continue" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -34745,23 +34325,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"day" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "天"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "天"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -35153,23 +34716,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"month" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "月"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "月"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"move" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -35696,23 +35242,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"week" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "周"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "週"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"what to emphasize" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -35815,23 +35344,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"year" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "年"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "年"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"~" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
@@ -15,11 +15,6 @@ public nonisolated enum ProOwnership: Equatable, Sendable {
|
||||
case familyShared
|
||||
}
|
||||
|
||||
public nonisolated enum ProPurchasePlan: Hashable, Sendable {
|
||||
case annual
|
||||
case lifetime
|
||||
}
|
||||
|
||||
public nonisolated enum ProSubscriptionPhase: Equatable, Sendable {
|
||||
case introductoryTrial
|
||||
case subscribed
|
||||
@@ -57,7 +52,7 @@ public nonisolated struct ProSubscriptionStatus: Equatable, Sendable {
|
||||
|
||||
public nonisolated enum EntitlementStatus: Equatable, Sendable {
|
||||
case loading
|
||||
case free(trialAvailable: Bool)
|
||||
case free
|
||||
case subscription(ProSubscriptionStatus)
|
||||
case purchased(ProOwnership)
|
||||
case verificationFailed
|
||||
@@ -70,12 +65,4 @@ public nonisolated enum EntitlementStatus: Equatable, Sendable {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
public var canPurchase: Bool {
|
||||
switch self {
|
||||
case .purchased, .unrestrictedForTesting: false
|
||||
case .subscription(let subscription): !subscription.phase.hasProAccess
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import TermindDomain
|
||||
nonisolated enum RevenueCatEntitlementResolver {
|
||||
static func resolve(_ info: CustomerInfo, entitlementID: String, now: Date) -> EntitlementStatus {
|
||||
guard info.entitlements.verification != .failed else { return .verificationFailed }
|
||||
guard let entitlement = info.entitlements.all[entitlementID] else { return .free(trialAvailable: false) }
|
||||
guard let entitlement = info.entitlements.all[entitlementID] else { return .free }
|
||||
guard entitlement.verification != .failed else { return .verificationFailed }
|
||||
let ownership: ProOwnership = entitlement.ownershipType == .familyShared ? .familyShared : .purchased
|
||||
if entitlement.expirationDate == nil {
|
||||
return entitlement.isActive ? .purchased(ownership) : .free(trialAvailable: false)
|
||||
return entitlement.isActive ? .purchased(ownership) : .free
|
||||
}
|
||||
let subscription = info.subscriptionsByProductIdentifier[entitlement.productIdentifier]
|
||||
let expiration = entitlement.expirationDate
|
||||
|
||||
@@ -84,11 +84,6 @@ public struct SSHAuthenticationForm: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var needsPasswordEntry: Bool {
|
||||
guard let selectedIdentity else { return true }
|
||||
return library.passwordAvailability(for: selectedIdentity) != .available
|
||||
}
|
||||
|
||||
private var title: LocalizedStringResource {
|
||||
if needsPassphrase { return "Passphrase required" }
|
||||
if prompt.isProxy { return "Proxy authentication required" }
|
||||
|
||||
@@ -26,10 +26,6 @@ extension AppStrings {
|
||||
"Abilities"
|
||||
}
|
||||
|
||||
public static var accessEnds: LocalizedStringResource {
|
||||
"Access Ends"
|
||||
}
|
||||
|
||||
public static func add(_ count: Int) -> LocalizedStringResource {
|
||||
"Add \(count)"
|
||||
}
|
||||
@@ -78,14 +74,6 @@ extension AppStrings {
|
||||
"AI"
|
||||
}
|
||||
|
||||
public static var aiProviderUsageAndChargesAreSeparateFromTermindPro: LocalizedStringResource {
|
||||
"AI provider usage and charges are separate from Termind Pro."
|
||||
}
|
||||
|
||||
public static var annualFamilyShared: LocalizedStringResource {
|
||||
"Annual · Family Shared"
|
||||
}
|
||||
|
||||
public static var anotherMcpServerAlreadyUsesThisId: LocalizedStringResource {
|
||||
"Another MCP server already uses this ID."
|
||||
}
|
||||
@@ -250,10 +238,6 @@ extension AppStrings {
|
||||
"Description"
|
||||
}
|
||||
|
||||
public static var developmentAccess: LocalizedStringResource {
|
||||
"Development Access"
|
||||
}
|
||||
|
||||
public static var disabled: LocalizedStringResource {
|
||||
"Disabled"
|
||||
}
|
||||
@@ -306,10 +290,6 @@ extension AppStrings {
|
||||
"Exact model ID, such as gpt-5.4"
|
||||
}
|
||||
|
||||
public static var expired: LocalizedStringResource {
|
||||
"Expired"
|
||||
}
|
||||
|
||||
public static var exposedTools: LocalizedStringResource {
|
||||
"Exposed Tools"
|
||||
}
|
||||
@@ -354,10 +334,6 @@ extension AppStrings {
|
||||
"Ghostty"
|
||||
}
|
||||
|
||||
public static var gracePeriodEnds: LocalizedStringResource {
|
||||
"Grace Period Ends"
|
||||
}
|
||||
|
||||
public static var headerName: LocalizedStringResource {
|
||||
"Header name"
|
||||
}
|
||||
@@ -658,10 +634,6 @@ extension AppStrings {
|
||||
"Removes the selected models. You can fetch them again."
|
||||
}
|
||||
|
||||
public static var renews: LocalizedStringResource {
|
||||
"Renews"
|
||||
}
|
||||
|
||||
public static var replaceKey: LocalizedStringResource {
|
||||
"Replace Key"
|
||||
}
|
||||
@@ -814,10 +786,6 @@ extension AppStrings {
|
||||
"Transport"
|
||||
}
|
||||
|
||||
public static var trialEnds: LocalizedStringResource {
|
||||
"Trial Ends"
|
||||
}
|
||||
|
||||
public static var url: LocalizedStringResource {
|
||||
"URL"
|
||||
}
|
||||
|
||||
@@ -17,27 +17,6 @@ nonisolated extension RemoteCharset {
|
||||
}
|
||||
}
|
||||
|
||||
public nonisolated struct RemoteCharsetConversionError: LocalizedError, Sendable {
|
||||
public enum Direction: Sendable { case encoding, decoding }
|
||||
|
||||
public var charset: RemoteCharset
|
||||
public var direction: Direction
|
||||
|
||||
public init(charset: RemoteCharset, direction: Direction) {
|
||||
self.charset = charset
|
||||
self.direction = direction
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch direction {
|
||||
case .encoding:
|
||||
String(localized: "Text cannot be represented using \(charset.displayName).")
|
||||
case .decoding:
|
||||
String(localized: "Received text is not valid \(charset.displayName).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stateful iconv adapter that preserves unconvertible source bytes verbatim.
|
||||
/// Valid input is converted while malformed, incomplete, or unrepresentable bytes
|
||||
/// remain in place so Charset conversion never interrupts a byte stream.
|
||||
|
||||
@@ -48,48 +48,6 @@ public nonisolated struct SFTPProgress: Sendable {
|
||||
|
||||
}
|
||||
|
||||
nonisolated private struct SFTPProgressMeter {
|
||||
public let total: Int64
|
||||
public let report: @Sendable (SFTPProgress) -> Void
|
||||
public private(set) var completed: Int64 = 0
|
||||
private var sampledBytes: Int64 = 0
|
||||
private var sampledAt = ContinuousClock.now
|
||||
|
||||
public nonisolated init(
|
||||
total: Int64, offset: Int64 = 0, report: @escaping @Sendable (SFTPProgress) -> Void
|
||||
) {
|
||||
self.total = total
|
||||
self.completed = offset
|
||||
self.report = report
|
||||
}
|
||||
|
||||
public nonisolated mutating func add(_ bytes: Int) {
|
||||
completed += Int64(bytes)
|
||||
sampledBytes += Int64(bytes)
|
||||
let elapsed = sampledAt.duration(to: .now)
|
||||
guard elapsed >= .milliseconds(250) else { return }
|
||||
let components = elapsed.components
|
||||
let seconds = max(
|
||||
0.001,
|
||||
Double(components.seconds) + Double(components.attoseconds) / 1e18
|
||||
)
|
||||
report(
|
||||
SFTPProgress(
|
||||
completedBytes: completed, totalBytes: total,
|
||||
bytesPerSecond: Int64(Double(sampledBytes) / seconds)
|
||||
))
|
||||
sampledBytes = 0
|
||||
sampledAt = .now
|
||||
}
|
||||
|
||||
public nonisolated func finish() {
|
||||
report(
|
||||
SFTPProgress(
|
||||
completedBytes: completed, totalBytes: total, bytesPerSecond: 0
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// One initialized SFTP subsystem running over an SSH channel.
|
||||
public nonisolated protocol SFTPChannelProtocol: RemoteFileSystem, Sendable {
|
||||
/// Opens another SFTP subsystem channel on the same authenticated SSH
|
||||
|
||||
@@ -18,7 +18,6 @@ nonisolated final class SSHAgentChannels: @unchecked Sendable {
|
||||
private let wake: @Sendable () -> Void
|
||||
private var channels: [Channel] = []
|
||||
private var stopped = false
|
||||
var hasChannels: Bool { !channels.isEmpty }
|
||||
|
||||
init(provider: any SSHAgentForwarding, queue: DispatchQueue, wake: @escaping @Sendable () -> Void) {
|
||||
self.provider = provider
|
||||
|
||||
@@ -49,10 +49,6 @@ public final class SFTPTransferContext {
|
||||
if let primary { Task { await primary.disconnect() } }
|
||||
}
|
||||
|
||||
/// Admission is owned by `SFTPTransferCoordinator`; a channel consumes the
|
||||
/// same slot as a connection, so fallback never changes capacity.
|
||||
var canStartTransfer: Bool { true }
|
||||
|
||||
func configureClientFactory(_ factory: @escaping SFTPTransferCoordinator.ClientFactory) {
|
||||
clientFactory = factory
|
||||
}
|
||||
|
||||
@@ -350,16 +350,6 @@ final class MobileWorkspace: Identifiable {
|
||||
|
||||
var hostKeyPrompt: SSHHostKeyPrompt? { connection.pendingHostKey }
|
||||
|
||||
private func preparation(
|
||||
for host: Host, submissions: [UUID: SSHAuthenticationSubmission]
|
||||
) throws -> SSHConnectionPreparation {
|
||||
try hosts.prepareSSHConnection(
|
||||
for: host,
|
||||
submissions: submissions,
|
||||
temporaryHostKeys: temporaryKeys
|
||||
)
|
||||
}
|
||||
|
||||
func submitAuthentication(
|
||||
identityID: UUID?, username: String, password: String?, passphrase: String?, keyID: UUID?, save: Bool
|
||||
) {
|
||||
|
||||
@@ -241,7 +241,6 @@ struct MobileAISettingsRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10000)
|
||||
let directory = FileManager.default.temporaryDirectory.appending(path: "ai-settings-screenshots")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
|
||||
|
||||
@@ -476,7 +476,6 @@ struct MobileAssistantRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10000)
|
||||
let directory = FileManager.default.temporaryDirectory.appending(path: "assistant-screenshots")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
|
||||
|
||||
@@ -218,7 +218,6 @@ struct MobileCardRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10000)
|
||||
let directory = FileManager.default.temporaryDirectory.appending(path: "card-screenshots")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
|
||||
|
||||
@@ -110,7 +110,6 @@ struct MobileConnectionJourneyRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10_000)
|
||||
Attachment.record(data, named: "\(name)-\(language)-\(scheme == .dark ? "dark" : "light").png")
|
||||
let directory = FileManager.default.temporaryDirectory.appending(path: "journey-screenshots")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
|
||||
@@ -68,7 +68,6 @@ struct MobileHostIntegrationRenderingTests {
|
||||
#expect(window.drawHierarchy(in: window.bounds, afterScreenUpdates: true))
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 5_000)
|
||||
Attachment.record(data, named: "host-integration-mobile-\(language).png")
|
||||
try data.write(
|
||||
to: FileManager.default.temporaryDirectory.appending(path: "host-integration-mobile-\(language).png"))
|
||||
|
||||
@@ -268,7 +268,6 @@ struct MobileIconRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10000)
|
||||
let directory = FileManager.default.temporaryDirectory.appending(path: "icon-screenshots")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
|
||||
|
||||
@@ -77,7 +77,6 @@ struct MobileKeyExportRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10000)
|
||||
let directory = FileManager.default.temporaryDirectory.appending(path: "key-export-screenshots")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
|
||||
|
||||
@@ -266,7 +266,6 @@ struct MobileMarketingScreenshotTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 100_000)
|
||||
|
||||
let outputDirectory = try #require(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first)
|
||||
.appending(path: "marketing-screenshots", directoryHint: .isDirectory)
|
||||
|
||||
@@ -46,7 +46,6 @@ struct MobileMemorySettingsRenderingTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10_000)
|
||||
Attachment.record(data, named: "mobile-memory-\(language)-\(enabled ? "enabled" : "disabled").png")
|
||||
await model.destroy()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ struct MobileMultiConnectionAccessTests {
|
||||
service.status = .purchased(.purchased)
|
||||
await pro.refresh()
|
||||
#expect(app.transfers.strategy == strategy)
|
||||
service.status = .free(trialAvailable: false)
|
||||
service.status = .free
|
||||
await pro.refresh()
|
||||
#expect(app.transfers.strategy == .channelsOnly)
|
||||
#expect(app.sftpSettings.preferences.strategy == strategy)
|
||||
@@ -32,6 +32,6 @@ struct MobileMultiConnectionAccessTests {
|
||||
@MainActor
|
||||
private final class MobileConnectionPurchaseService: ProPurchaseService {
|
||||
let updates = AsyncStream<EntitlementStatus> { $0.finish() }
|
||||
var status = EntitlementStatus.free(trialAvailable: false)
|
||||
var status = EntitlementStatus.free
|
||||
func currentStatus() async throws -> EntitlementStatus { status }
|
||||
}
|
||||
@@ -64,7 +64,6 @@ struct MobilePassphraseFormLayoutTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10000)
|
||||
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
|
||||
"termind-passphrase-ios-\(language)-\(showKeys ? "keys" : "form").png")
|
||||
try data.write(to: url)
|
||||
|
||||
@@ -36,7 +36,6 @@ struct MobileSFTPSettingsRenderingTests {
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
Attachment.record(data, named: "mobile-connection-settings-\(language)-\(value.globalLimit).png")
|
||||
#expect(data.count > 10_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,6 @@ struct MobileTerminalFontTests {
|
||||
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
#expect(data.count > 10_000)
|
||||
Attachment.record(data, named: "\(name).png")
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ struct MobileTerminalHighlightingTests {
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
Attachment.record(data, named: "mobile-keyword-\(name)-\(language)-\(Int(window.bounds.width)).png")
|
||||
#expect(data.count > 20000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ struct MobileTerminalThemeTests {
|
||||
}
|
||||
let data = try #require(image.pngData())
|
||||
Attachment.record(data, named: "mobile-themes-\(language)-\(isDark ? "dark" : "light").png")
|
||||
#expect(data.count > 10_000)
|
||||
#expect(model.preferences == initial)
|
||||
let directory = FileManager.default.temporaryDirectory.appendingPathComponent("termind-theme-previews")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
|
||||
@@ -39,7 +39,6 @@ protocol SerialPortConnection: Sendable {
|
||||
}
|
||||
|
||||
protocol SerialPortServing: Sendable {
|
||||
func availablePorts() async -> [SerialPortDescriptor]
|
||||
func portUpdates() -> AsyncStream<[SerialPortDescriptor]>
|
||||
func open(
|
||||
_ configuration: SerialConnectionConfiguration,
|
||||
|
||||
@@ -78,8 +78,6 @@ final class VaultSelectionState {
|
||||
set(items, for: page)
|
||||
}
|
||||
|
||||
func clear(_ page: VaultPage) { selections[page] = [] }
|
||||
|
||||
func prune(available: Set<VaultSelectionID>, pages: Set<VaultPage> = Set(VaultPage.allCases)) {
|
||||
for page in pages {
|
||||
let valid = selection(for: page).filter(available.contains)
|
||||
|
||||
@@ -174,10 +174,6 @@ final class WindowModel {
|
||||
vaultPages.selection.toggle(detail, on: page)
|
||||
}
|
||||
|
||||
func clearVaultSelection(for page: VaultPage) {
|
||||
vaultPages.selection.clear(page)
|
||||
}
|
||||
|
||||
/// Removes records deleted out from under a remembered selection.
|
||||
func pruneVaultSelections(in store: HostLibraryModel) {
|
||||
vaultPages.selection.prune(available: Self.availableVaultSelections(in: store))
|
||||
@@ -1615,13 +1611,6 @@ nonisolated enum DragPayload: Hashable, Sendable {
|
||||
case workspace(UUID)
|
||||
case pane(UUID)
|
||||
case assistantConversation(UUID)
|
||||
|
||||
nonisolated var isLayoutPayload: Bool {
|
||||
switch self {
|
||||
case .workspace, .pane: true
|
||||
case .assistantConversation: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sidebar rows
|
||||
|
||||
@@ -9,7 +9,6 @@ nonisolated final class SystemSerialPortService: SerialPortServing, @unchecked S
|
||||
private let openQueue = DispatchQueue(
|
||||
label: "com.akinokaede.termind.serial.open", attributes: .concurrent
|
||||
)
|
||||
func availablePorts() async -> [SerialPortDescriptor] { Self.scanPorts() }
|
||||
|
||||
func portUpdates() -> AsyncStream<[SerialPortDescriptor]> {
|
||||
AsyncStream { continuation in
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import Foundation
|
||||
import TermindApplication
|
||||
import TermindDomain
|
||||
import TermindInfrastructure
|
||||
import TermindPersistence
|
||||
import TermindPresentation
|
||||
import TermindRemoteInfrastructure
|
||||
import Testing
|
||||
|
||||
@testable import Termind
|
||||
|
||||
struct CommerceConfigurationTests {
|
||||
@Test
|
||||
func localStoreKitFixtureContainsAnnualAndLifetimeProducts() throws {
|
||||
let configurationFile = URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appending(path: "StoreKit/Termind.storekit")
|
||||
let data = try Data(contentsOf: configurationFile)
|
||||
let configuration = try #require(
|
||||
JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
)
|
||||
let products = try #require(configuration["products"] as? [[String: Any]])
|
||||
let productIDs = products.compactMap { $0["productID"] as? String }
|
||||
|
||||
#expect(Set(productIDs) == ["com.akinokaede.termind.pro"])
|
||||
let pro = try #require(
|
||||
products.first { $0["productID"] as? String == "com.akinokaede.termind.pro" })
|
||||
#expect(pro["displayPrice"] as? String == "9.99")
|
||||
#expect(pro["familyShareable"] as? Bool == true)
|
||||
|
||||
let groups = try #require(configuration["subscriptionGroups"] as? [[String: Any]])
|
||||
let group = try #require(groups.first)
|
||||
let subscriptions = try #require(group["subscriptions"] as? [[String: Any]])
|
||||
let annual = try #require(
|
||||
subscriptions.first { $0["productID"] as? String == "com.akinokaede.termind.pro.annual" })
|
||||
#expect(annual["displayPrice"] as? String == "3.99")
|
||||
#expect(annual["familyShareable"] as? Bool == true)
|
||||
#expect(annual["recurringSubscriptionPeriod"] as? String == "P1Y")
|
||||
#expect(annual["type"] as? String == "RecurringSubscription")
|
||||
let offer = try #require(annual["introductoryOffer"] as? [String: Any])
|
||||
#expect(offer["paymentMode"] as? String == "free")
|
||||
#expect(offer["subscriptionPeriod"] as? String == "P2W")
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ struct DiagnosisConsentRenderingTests {
|
||||
let bitmap = try #require(host.view.bitmapImageRepForCachingDisplay(in: host.view.bounds))
|
||||
host.view.cacheDisplay(in: host.view.bounds, to: bitmap)
|
||||
let png = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
#expect(png.count > 5_000)
|
||||
#expect(controller.needsConsent)
|
||||
#expect(defaults.object(forKey: SettingsKey.diagnosisConsent) == nil)
|
||||
Attachment.record(png, named: "diagnosis-\(language)-\(Int(width)).png")
|
||||
|
||||
@@ -41,22 +41,6 @@ struct ListViewMigrationTests {
|
||||
#expect(normal != selected)
|
||||
}
|
||||
|
||||
/// Folding a section must not change the item's *value*, or ListViewKit
|
||||
/// reconfigures the header's hosting view from inside the click that asked
|
||||
/// for the fold — which crashed. Only the count is data; expansion is read
|
||||
/// by the row itself. See `AssistantSectionHeaderRow`.
|
||||
@Test
|
||||
func foldingASectionDoesNotChangeItsListItem() {
|
||||
let expanded = AssistantConversationListItem.section(.pinned, count: 3)
|
||||
let collapsed = AssistantConversationListItem.section(.pinned, count: 3)
|
||||
|
||||
#expect(expanded == collapsed)
|
||||
#expect(
|
||||
AssistantConversationListItem.Section.pinned.expansionKey
|
||||
!= AssistantConversationListItem.Section.chats.expansionKey
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func assistantSectionIdentityIsSeparateFromConversationIdentity() {
|
||||
let pinned = AssistantConversationListItem.section(.pinned)
|
||||
|
||||
@@ -161,7 +161,7 @@ struct MetricsLimitTests {
|
||||
|
||||
@Test
|
||||
func productionWiringUsesTheExistingProPaywallAndIncludesSubscriptionAccess() {
|
||||
let free = ProAccessController.testing(status: .free(trialAvailable: true))
|
||||
let free = ProAccessController.testing(status: .free)
|
||||
let app = AppModel(hosts: HostLibraryModel(), proAccess: free)
|
||||
defer { app.shutdownForTermination() }
|
||||
#expect(app.serverMetrics.remainingCardSlots == 3)
|
||||
|
||||
@@ -137,6 +137,5 @@ struct ModelProviderRenderingTests {
|
||||
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
|
||||
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
Attachment.record(data, named: "keyless-\(name)-\(language).png")
|
||||
#expect(data.count > 10_000)
|
||||
}
|
||||
}
|
||||
@@ -54,15 +54,6 @@ struct PresentationCopyTests {
|
||||
#expect(!message.contains("NSLanguage"))
|
||||
}
|
||||
|
||||
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
|
||||
func singleFileDeletionWarningDoesNotRepeatTheTitlePath(language: String) {
|
||||
let locale = Locale(identifier: language)
|
||||
let warning = AppLocalization.resolve(AppStrings.SFTP.recursiveDeletionWarning, locale: locale)
|
||||
#expect(!warning.contains("/home/example/file.txt"))
|
||||
#expect(!warning.contains("NSLanguage"))
|
||||
#expect(!warning.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: ["en", "zh-Hans", "zh-Hant"], [0, 1, 2])
|
||||
func groupDeletionUsesTheRequestedLanguageAndPreservesPluralRules(language: String, count: Int) {
|
||||
let locale = Locale(identifier: language)
|
||||
|
||||
@@ -13,7 +13,7 @@ struct ProAccessTests {
|
||||
@Test
|
||||
func entitlementStatesExposeOnlyRealAccess() {
|
||||
#expect(!EntitlementStatus.loading.hasProAccess)
|
||||
#expect(!EntitlementStatus.free(trialAvailable: true).hasProAccess)
|
||||
#expect(!EntitlementStatus.free.hasProAccess)
|
||||
#expect(!EntitlementStatus.verificationFailed.hasProAccess)
|
||||
#expect(EntitlementStatus.purchased(.purchased).hasProAccess)
|
||||
#expect(EntitlementStatus.purchased(.familyShared).hasProAccess)
|
||||
@@ -40,7 +40,7 @@ struct ProAccessTests {
|
||||
|
||||
@Test(arguments: ProFeature.allCases) @MainActor
|
||||
func freeAccessRefusesEveryPaidFeatureBeforeAnyCallerCanProceed(feature: ProFeature) {
|
||||
let access = ProAccessController.testing(status: .free(trialAvailable: true))
|
||||
let access = ProAccessController.testing(status: .free)
|
||||
#expect(!access.canUse(feature))
|
||||
#expect(!access.require(feature))
|
||||
#expect(access.isPaywallPresented)
|
||||
|
||||
@@ -48,7 +48,7 @@ struct SFTPSettingsIntegrationTests {
|
||||
|
||||
@Test func freeStartupAndSyncedPreferencesUseOnlyTheEffectiveStrategy() {
|
||||
let settings = SFTPSettingsModel()
|
||||
let pro = ProAccessController.testing(status: .free(trialAvailable: true))
|
||||
let pro = ProAccessController.testing(status: .free)
|
||||
let app = AppModel(hosts: HostLibraryModel(), sftpSettings: settings, proAccess: pro)
|
||||
defer { app.shutdownForTermination() }
|
||||
#expect(settings.preferences.strategy == .smart)
|
||||
@@ -107,7 +107,6 @@ struct SFTPSettingsIntegrationTests {
|
||||
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
|
||||
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
Attachment.record(data, named: "sftp-settings-\(language)-\(value.globalLimit).png")
|
||||
#expect(data.count > 5_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +114,6 @@ struct SFTPSettingsIntegrationTests {
|
||||
@MainActor
|
||||
private final class ConnectionPurchaseService: ProPurchaseService {
|
||||
let updates = AsyncStream<EntitlementStatus> { $0.finish() }
|
||||
var status = EntitlementStatus.free(trialAvailable: false)
|
||||
var status = EntitlementStatus.free
|
||||
func currentStatus() async throws -> EntitlementStatus { status }
|
||||
}
|
||||
@@ -32,7 +32,6 @@ struct SSHAgentForwardingRenderingTests {
|
||||
let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds))
|
||||
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
|
||||
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
#expect(data.count > 5000)
|
||||
Attachment.record(data, named: "agent-forwarding-mac-\(language).png")
|
||||
try data.write(
|
||||
to: FileManager.default.temporaryDirectory.appending(path: "agent-forwarding-mac-\(language).png"))
|
||||
|
||||
@@ -15,7 +15,6 @@ nonisolated private struct InertSerialConnection: SerialPortConnection {
|
||||
}
|
||||
|
||||
nonisolated private struct InertSerialService: SerialPortServing {
|
||||
func availablePorts() async -> [SerialPortDescriptor] { [] }
|
||||
func portUpdates() -> AsyncStream<[SerialPortDescriptor]> {
|
||||
AsyncStream { $0.finish() }
|
||||
}
|
||||
@@ -32,7 +31,6 @@ nonisolated private struct InertSerialService: SerialPortServing {
|
||||
nonisolated private struct EmittingSerialService: SerialPortServing {
|
||||
let data: Data
|
||||
|
||||
func availablePorts() async -> [SerialPortDescriptor] { [] }
|
||||
func portUpdates() -> AsyncStream<[SerialPortDescriptor]> {
|
||||
AsyncStream { $0.finish() }
|
||||
}
|
||||
@@ -60,7 +58,6 @@ nonisolated private final class RecordingSerialConnection: SerialPortConnection,
|
||||
nonisolated private struct RecordingSerialService: SerialPortServing {
|
||||
let connection: RecordingSerialConnection
|
||||
|
||||
func availablePorts() async -> [SerialPortDescriptor] { [] }
|
||||
func portUpdates() -> AsyncStream<[SerialPortDescriptor]> {
|
||||
AsyncStream { $0.finish() }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import Testing
|
||||
struct SettingsNavigationTests {
|
||||
@Test(
|
||||
arguments: [
|
||||
EntitlementStatus.free(trialAvailable: true), .purchased(.purchased), .purchased(.familyShared),
|
||||
EntitlementStatus.free, .purchased(.purchased), .purchased(.familyShared),
|
||||
.subscription(
|
||||
ProSubscriptionStatus(
|
||||
phase: .subscribed, expiresAt: .distantFuture, willAutoRenew: true, ownership: .purchased)),
|
||||
|
||||
@@ -11,7 +11,7 @@ import Testing
|
||||
struct SheetQuitInteractionTests {
|
||||
@Test(arguments: ["connect", "batch", "pro"])
|
||||
func nativeQuitWorksWithSheetFocusAndAfterCancellingAndReopening(_ surface: String) async throws {
|
||||
let app = AppModel(hosts: HostLibraryModel(), proAccess: .testing(status: .free(trialAvailable: true)))
|
||||
let app = AppModel(hosts: HostLibraryModel(), proAccess: .testing(status: .free))
|
||||
let model = WindowModel()
|
||||
let root = RootView(window: model)
|
||||
.environment(app)
|
||||
|
||||
@@ -33,6 +33,5 @@ import Testing
|
||||
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
|
||||
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
Attachment.record(data, named: "ai-skills-memory-\(language).png")
|
||||
#expect(data.count > 20_000)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import Foundation
|
||||
import StoreKit
|
||||
import StoreKitTest
|
||||
import Testing
|
||||
|
||||
@Suite(
|
||||
.enabled(
|
||||
if: ProcessInfo.processInfo.environment["TERMIND_STOREKIT_TESTS"] == "1",
|
||||
"Set TERMIND_STOREKIT_TESTS=1 to run StoreKit tests that require manual authorization"),
|
||||
.serialized)
|
||||
@MainActor
|
||||
struct StoreKitTrialTests {
|
||||
// This local fixture validates catalog metadata only. RevenueCatCommerceTests covers app access.
|
||||
@Test
|
||||
func fixtureHasTheUniversalAnnualAndLifetimeProductsWithAFreeIntroOffer() async throws {
|
||||
let configuration = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent()
|
||||
.appending(path: "StoreKit/Termind.storekit")
|
||||
let session = try SKTestSession(contentsOf: configuration)
|
||||
session.resetToDefaultState()
|
||||
session.disableDialogs = true
|
||||
defer { session.resetToDefaultState() }
|
||||
|
||||
let products = try await Product.products(
|
||||
for: ["com.akinokaede.termind.pro.annual", "com.akinokaede.termind.pro"])
|
||||
let annual = try #require(products.first { $0.id == "com.akinokaede.termind.pro.annual" })
|
||||
let lifetime = try #require(products.first { $0.id == "com.akinokaede.termind.pro" })
|
||||
#expect(annual.type == .autoRenewable)
|
||||
#expect(annual.isFamilyShareable)
|
||||
#expect(lifetime.type == .nonConsumable)
|
||||
#expect(lifetime.isFamilyShareable)
|
||||
let subscription = try #require(annual.subscription)
|
||||
#expect(subscription.subscriptionPeriod.unit == .year)
|
||||
#expect(subscription.subscriptionPeriod.value == 1)
|
||||
let offer = try #require(subscription.introductoryOffer)
|
||||
#expect(offer.paymentMode == .freeTrial)
|
||||
#expect(offer.price == .zero)
|
||||
#expect(offer.period.unit == .week)
|
||||
#expect(offer.period.value * offer.periodCount == 2)
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,5 @@ struct TerminalSettingsRenderingTests {
|
||||
view.cacheDisplay(in: view.bounds, to: bitmap)
|
||||
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
Attachment.record(data, named: "\(name).png")
|
||||
#expect(data.count > 5_000)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ nonisolated private struct SidebarTestSerialConnection: SerialPortConnection {
|
||||
}
|
||||
|
||||
nonisolated private struct SidebarTestSerialService: SerialPortServing {
|
||||
func availablePorts() async -> [SerialPortDescriptor] { [] }
|
||||
func portUpdates() -> AsyncStream<[SerialPortDescriptor]> {
|
||||
AsyncStream { $0.finish() }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import struct TermindDomain.Host
|
||||
struct WorkspaceAccessTests {
|
||||
nonisolated static let nonProStatuses: [EntitlementStatus] =
|
||||
[
|
||||
.loading, .free(trialAvailable: true), .free(trialAvailable: false), .verificationFailed,
|
||||
.loading, .free, .verificationFailed,
|
||||
]
|
||||
+ [ProSubscriptionPhase.billingRetry, .expired, .revoked].map {
|
||||
.subscription(
|
||||
|
||||
Reference in new issue
Block a user