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:
Kaede Akino committed 2026-10-02 17:50:47 +08:00
1 parent 8450234179
commit 3708a7c54f
48 files changed
+24 -787

No files matched your search

+3 -1
View File
@@ -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.
+8 -8
View File
@@ -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
-488
View File
@@ -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"
-1
View File
@@ -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"))
-1
View File
@@ -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)
}
}
}
-1
View File
@@ -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)
}
}
}
-1
View File
@@ -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,
-2
View File
@@ -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)
-11
View File
@@ -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
-45
View File
@@ -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")
-16
View File
@@ -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)
+1 -1
View File
@@ -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)
}
}
-9
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -3
View File
@@ -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"))
-3
View File
@@ -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() }
}
+1 -1
View File
@@ -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)),
+1 -1
View File
@@ -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)
}
}
-40
View File
@@ -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)
}
}
-1
View File
@@ -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() }
}
+1 -1
View File
@@ -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(