Use shared forwarding editor policy on macOS
Remove the redundant desktop adapter and retain its behavior through the existing Application policy. Expand regression coverage and record verification evidence. Verification: 1,399 macOS tests passed. iOS had 428 passing tests and five repeatable failures with baseline status unverified. Strict Swift formatting and Chinese localization checks passed.
This commit is contained in:
1 parent
f06d911b75
commit
decd565f2b
5 files changed
+196
-91
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import TermindApplication
|
||||
import TermindDomain
|
||||
import Testing
|
||||
|
||||
import struct TermindDomain.Host
|
||||
|
||||
@Suite
|
||||
struct PortForwardingEditorPolicyTests {
|
||||
@Test(
|
||||
arguments: [false, true],
|
||||
[
|
||||
(false, false, true, true, false),
|
||||
(true, false, false, true, true),
|
||||
(false, true, false, false, false),
|
||||
(true, true, false, false, false),
|
||||
])
|
||||
func controlsKeepSavedEligibilityIntentAndActivityIndependent(
|
||||
hasSavedConfiguration: Bool,
|
||||
scenario: (enabled: Bool, active: Bool, canStartSaved: Bool, canEdit: Bool, waitsWhenUnsaved: Bool)
|
||||
) {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let rule = PortForwardingRule(hostID: host.id)
|
||||
let policy = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: hasSavedConfiguration ? rule : nil, savedHost: host,
|
||||
isEnabled: scenario.enabled, isRuntimeActive: scenario.active)
|
||||
|
||||
#expect(policy.hasSavedConfiguration == hasSavedConfiguration)
|
||||
#expect(policy.isEnabled == scenario.enabled)
|
||||
#expect(policy.isRuntimeActive == scenario.active)
|
||||
#expect(policy.canStart == (hasSavedConfiguration && scenario.canStartSaved))
|
||||
#expect(policy.canToggle)
|
||||
#expect(policy.canEdit == scenario.canEdit)
|
||||
#expect(policy.isWaitingForSave == (!hasSavedConfiguration && scenario.waitsWhenUnsaved))
|
||||
}
|
||||
|
||||
enum UnavailableConfiguration: CaseIterable {
|
||||
case missingRule, missingHost, mismatchedHost, mismatchedRuleID
|
||||
}
|
||||
|
||||
@Test(arguments: UnavailableConfiguration.allCases)
|
||||
func missingOrMismatchedSavedConfigurationCannotStart(configuration: UnavailableConfiguration) {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let rule = PortForwardingRule(hostID: host.id)
|
||||
var savedRule: PortForwardingRule? = rule
|
||||
var savedHost: Host? = host
|
||||
switch configuration {
|
||||
case .missingRule: savedRule = nil
|
||||
case .missingHost: savedHost = nil
|
||||
case .mismatchedHost: savedHost = Host(name: "Other", host: "other.invalid")
|
||||
case .mismatchedRuleID: savedRule?.id = UUID()
|
||||
}
|
||||
let policy = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: savedRule, savedHost: savedHost,
|
||||
isEnabled: false, isRuntimeActive: false)
|
||||
|
||||
#expect(!policy.hasSavedConfiguration)
|
||||
#expect(!policy.canStart)
|
||||
#expect(policy.canToggle)
|
||||
#expect(policy.canEdit)
|
||||
}
|
||||
|
||||
@Test(arguments: [0, 65_536])
|
||||
func aValidDraftCannotMakeAnInvalidSavedRuleStartable(invalidPort: Int) {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let draft = PortForwardingRule(hostID: host.id)
|
||||
var savedRule = draft
|
||||
savedRule.bindPort = invalidPort
|
||||
let policy = PortForwardingEditorPolicy(
|
||||
rule: PortForwardingEditorPolicy.normalized(draft), savedRule: savedRule, savedHost: host,
|
||||
isEnabled: false, isRuntimeActive: false)
|
||||
|
||||
#expect(!policy.hasSavedConfiguration)
|
||||
#expect(!policy.canStart)
|
||||
#expect(policy.canEdit)
|
||||
}
|
||||
}
|
||||
@@ -135,3 +135,26 @@ Verification in progress:
|
||||
- The pinned full TFM action passed all 1,183 app tests and 213 shared tests, including language and architecture checks. Result: `/tmp/termind-sftp-status-macos-20260916.xcresult`.
|
||||
- The pinned full TFI action compiled both targets and passed 212 of 217 app tests plus all 213 shared tests. Its five failures in `MobileAssistantTests` and `MobileVaultFeatureTests` match the pre-change `/tmp/termind-test-cleanup-full-ios.log` exactly; no unrelated tests were changed. Result: `/tmp/termind-sftp-status-ios-20260916.xcresult`.
|
||||
- Strict Swift formatting, `Scripts/check-zh-localization.sh`, and `git diff --check` passed. Visually reviewed English, Simplified Chinese, and Traditional Chinese status matrices rendered from the built shared property and app localization resources in `/tmp/termind-sftp-status-preview/`; these are label previews, not live transfer screenshots.
|
||||
|
||||
## Retire the Desktop Forwarding Editor Adapter
|
||||
|
||||
- **Implemented; verification pending (reproduced iOS failures).** Evidence: `rg -n 'PortForwardingEditorState|PortForwardingEditorPolicy' TFM TFI Frameworks TFMTests TFITests FrameworkTests Memory Termind.xcodeproj -g '*.swift' -g '*.md' -g '*.pbxproj'` confirms that the 43-line desktop adapter has only three production consumers, all computed state getters in `TFM/Features/Ports/PortForwardingView.swift`, and seven desktop tests. It already delegates saved eligibility and normalization to Application's existing `PortForwardingEditorPolicy`; its remaining flags derive identically from enabled intent and runtime activity. Mobile already uses the policy directly. Action: adopt that policy in the three desktop getters and delete the adapter, retaining synchronous observable saved-rule/saved-host/controller reads and full runtime values for native presentation. Preserve all callbacks, enabled intent OR activity, draft normalization, saved-host lookup by the editor host's ID, and editing locks. Retarget all seven desktop test behaviors and add shared-policy eligibility/intent/activity coverage. Estimated production reduction: 27–28 Swift lines; test additions counted separately. No architecture invariant changes, boundary-guard edits, public API, dependency, persistence, runtime, approval, secret, or localization changes are required.
|
||||
- Implementation outcome: removed the adapter without a replacement wrapper; production Swift decreased by 27 lines (43 deleted, 16 net added to the view). All seven desktop test behaviors remain, including the AppModel save-without-start integration and saved-host lookup through a changed draft host's ID. Expanded active-phase coverage across both intent values, local/remote normalization boundaries and missing targets, and invalid hidden dynamic targets. Added 14 shared-policy matrix/eligibility cases. Test Swift increased by 109 lines; total Swift increased by 82 lines, with regression additions reported separately from the production reduction. Architecture records/guards, feature integration suites, native copy, shared production code, and pinned dependencies are unchanged.
|
||||
|
||||
- Reviews: both independent code reviews and the prior final audit found no code issue or requested revision. This documentation audit independently inspected the actual diff and unchanged shared policy and found no behavior or boundary issue. Only this appended entry was updated; every earlier entry is preserved.
|
||||
|
||||
Verification audit (2026-09-16 UTC):
|
||||
|
||||
- The prior pinned focused TFM action passed **33/33 tests**, zero failures/skips: 14 forwarding feature, seven editor, nine architecture, and three shared-policy tests (the latter cover 14 parameterized cases). Independently re-extracted `/tmp/termind-forwarding-adapter-verify.xMtOCK/focused.xcresult` and checked `focused.log` and `focused.command.txt` in that directory; the action was not repeated.
|
||||
- The full pinned TFM action passed **1,399/1,399 tests** (1,183 TFMTests + 216 FrameworkTests), zero failures/skips, exit 0. Result: `/tmp/termind-forwarding-adapter-recovery.eggzSE/remaining-checks.9wl35uwk/macos.xcresult`.
|
||||
- The full pinned TFI action passed **428/433 tests** (212/217 TFITests + 216/216 FrameworkTests), five failures, zero skips, exit 65. Result: `/tmp/termind-forwarding-adapter-recovery.eggzSE/remaining-checks.9wl35uwk/ios.xcresult`. The exact five-test pinned rerun passed **0/5**, with the same five identifiers and identical failure text, exit 65: `/tmp/termind-forwarding-adapter-recovery.eggzSE/remaining-checks.9wl35uwk/ios-rerun.xcresult`. Both used the freshly discovered iPhone 17 Pro / iOS 26.5 simulator, UDID `38789AE8-B5AA-46CA-A15C-77CE760FC550`.
|
||||
- Actual repeated failures (all in TFITests):
|
||||
- `MobileAssistantTests/attachmentOnlyFirstSendPromotesTheConversation()` and `MobileAssistantTests/firstNewConversationSendPromotesDraftAndStartsRun()`: `Expectation failed: draft.messages.map(\.role) == [.user, .assistant]`.
|
||||
- `MobileAssistantTests/completeRegistryIncludesRemoteAndBuiltInToolsOnlyForCapableModels()`: `Expectation failed: !names.contains("terminal_suggest")`.
|
||||
- `MobileAssistantTests/existingConversationSendStillUsesTheDraftExecutionPath()`: `Expectation failed: conversation.messages.map(\.role) == [.user, .user, .assistant]`.
|
||||
- `MobileVaultFeatureTests/importRequiresAnExplicitUsernameWhenTheConfigurationOmitsIt()`: `Expectation failed: model.canImport`.
|
||||
- These failures are **baseline-unverified**, not established pre-existing failures or proven regressions. No pre-change baseline was run for this patch; earlier backlog notes are not substituted for baseline evidence. Their triage/baseline attribution and a passing required iOS verification remain blockers to completion. All planned automated stages reached terminal completion after the prior provider interruption; none is missing, and that interruption was not a test failure or a user-requested stop.
|
||||
- Static checks passed: full strict recursive Swift formatting over `Frameworks TFM TFI FrameworkTests TFMTests TFITests TermindMCPBridge`, `Scripts/check-zh-localization.sh`, and `git diff --check` (all exit 0). The first-party Swift retired-symbol scan found no `PortForwardingEditorState` (expected `rg` exit 1). The new test's whitespace checks had no diagnostics; the runner initially mislabeled no-index exit 1 (files differ), corrected in `untracked-whitespace-adjudication.json` without altering raw evidence, with a supplementary read-only scan passing at exit 0. Implementation-stage evidence remains in `/tmp/termind-forwarding-adapter-implement.aTsWRl/checks.sh` and `checks.log`.
|
||||
- Integrity: before this documentation update, the complete patch, including the untracked test, matched both the focused verifier's initial patch and `/tmp/termind-forwarding-adapter-recovery.eggzSE/partial.patch` byte-for-byte (SHA-256 `f76b9950aa3bb49140865feec1153836f59ce903d25f3a32edcbf07ab320a691`). Reviewed production/test bytes remain unchanged. `project.pbxproj` and `Package.resolved` hashes match both the saved verification evidence and HEAD; no dependency drift. The same five paths remain uncommitted on `main` at `8b01a631fefdbe14e1324d87c91d0860124d85d3`; no further builds, source/test edits, or commits were made by this audit.
|
||||
- Limitations/warnings: native manual status/toggle/edit-lock and save/cancel/back testing was **not executed**; layout and copy are unchanged, and no visual evidence is claimed. Full-suite xcresult summaries retain 166 macOS runtime warnings (88 environment-locale, 74 constraint, four StoreKit transaction-listener) and 29 iOS warnings (27 environment-locale, two publishing-during-view-update); the targeted rerun reports none. These warnings were not baseline-attributed or broadly investigated. Focused logs also retain native row-height and Xcode diagnostic warnings despite the passing result.
|
||||
- Artifacts: `/tmp/termind-forwarding-adapter-recovery.eggzSE/remaining-checks.9wl35uwk/` contains `verification-report.md`, `verification-report.json`, `runner.log`, `runner.completed.json`, per-stage `*.command.txt`/`*.record.json`/logs, `*-summary.json`, `*-tests.json`, `failure-comparison.json`, simulator selection, and postverification patch/dependency evidence. Independent re-extractions, count/failure comparisons, before/after integrity, and the post-documentation `git diff --check` result are preserved in `/tmp/termind-forwarding-adapter-recovery.eggzSE/audit-record/`.
|
||||
@@ -1,43 +0,0 @@
|
||||
import Foundation
|
||||
import TermindApplication
|
||||
import TermindDomain
|
||||
import TermindRemoteInfrastructure
|
||||
|
||||
import struct TermindDomain.Host
|
||||
|
||||
/// The list and detail keep enabled intent separate from the saved configuration and live runtime.
|
||||
struct PortForwardingEditorState {
|
||||
let hasSavedConfiguration: Bool
|
||||
let isEnabled: Bool
|
||||
let runtime: PortForwardingRuntimeState
|
||||
|
||||
var canStart: Bool { hasSavedConfiguration && !isEnabled && !runtime.isActive }
|
||||
var canToggle: Bool { true }
|
||||
var canEdit: Bool { !runtime.isActive }
|
||||
var isWaitingForSave: Bool { isEnabled && !hasSavedConfiguration && !runtime.isActive }
|
||||
|
||||
init(
|
||||
rule: PortForwardingRule?, host: Host,
|
||||
savedRule: PortForwardingRule?, savedHost: Host?,
|
||||
isEnabled: Bool, runtime: PortForwardingRuntimeState
|
||||
) {
|
||||
let shared = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: savedRule, savedHost: savedHost,
|
||||
isEnabled: isEnabled, isRuntimeActive: runtime.isActive)
|
||||
hasSavedConfiguration = shared.hasSavedConfiguration
|
||||
self.isEnabled = shared.isEnabled
|
||||
self.runtime = runtime
|
||||
}
|
||||
|
||||
init(ruleID: PortForwardingRule.ID, rule: PortForwardingRule?, host: Host, app: AppModel) {
|
||||
self.init(
|
||||
rule: rule, host: host,
|
||||
savedRule: app.forwardings.portForwarding(ruleID), savedHost: app.hostLibrary.host(host.id),
|
||||
isEnabled: app.forwarding.isEnabled(ruleID), runtime: app.forwarding.state(for: ruleID))
|
||||
}
|
||||
|
||||
static func normalized(_ rule: PortForwardingRule) -> PortForwardingRule? {
|
||||
PortForwardingEditorPolicy.normalized(rule)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import TermindApplication
|
||||
import TermindDomain
|
||||
import TermindPresentation
|
||||
import TermindRemoteInfrastructure
|
||||
@@ -90,11 +91,15 @@ private struct HostForwardingRow: View {
|
||||
|
||||
@Environment(AppModel.self) private var app
|
||||
|
||||
private var state: PortForwardingEditorState {
|
||||
PortForwardingEditorState(
|
||||
ruleID: rule.id, rule: PortForwardingEditorState.normalized(rule), host: host, app: app)
|
||||
private var state: PortForwardingEditorPolicy {
|
||||
PortForwardingEditorPolicy(
|
||||
rule: PortForwardingEditorPolicy.normalized(rule),
|
||||
savedRule: app.forwardings.portForwarding(rule.id),
|
||||
savedHost: app.hostLibrary.host(host.id),
|
||||
isEnabled: app.forwarding.isEnabled(rule.id),
|
||||
isRuntimeActive: runtime.isActive)
|
||||
}
|
||||
private var runtime: PortForwardingRuntimeState { state.runtime }
|
||||
private var runtime: PortForwardingRuntimeState { app.forwarding.state(for: rule.id) }
|
||||
private var title: String { rule.label.isEmpty ? rule.kind.displayName : rule.label }
|
||||
|
||||
var body: some View {
|
||||
@@ -152,7 +157,7 @@ private struct HostForwardingRow: View {
|
||||
|
||||
private var enabled: Binding<Bool> {
|
||||
Binding(
|
||||
get: { state.isEnabled || state.runtime.isActive },
|
||||
get: { state.isEnabled || state.isRuntimeActive },
|
||||
set: { value in
|
||||
app.forwarding.setEnabled(value, for: rule.id)
|
||||
}
|
||||
@@ -169,15 +174,19 @@ private struct HostForwardingContextMenu: View {
|
||||
|
||||
@Environment(AppModel.self) private var app
|
||||
|
||||
private var state: PortForwardingEditorState {
|
||||
PortForwardingEditorState(
|
||||
ruleID: rule.id, rule: PortForwardingEditorState.normalized(rule), host: host, app: app)
|
||||
private var state: PortForwardingEditorPolicy {
|
||||
PortForwardingEditorPolicy(
|
||||
rule: PortForwardingEditorPolicy.normalized(rule),
|
||||
savedRule: app.forwardings.portForwarding(rule.id),
|
||||
savedHost: app.hostLibrary.host(host.id),
|
||||
isEnabled: app.forwarding.isEnabled(rule.id),
|
||||
isRuntimeActive: app.forwarding.state(for: rule.id).isActive)
|
||||
}
|
||||
private var isActive: Bool { !state.canEdit }
|
||||
private var command: String? { app.forwardings.sshCommand(for: rule, host: host) }
|
||||
|
||||
var body: some View {
|
||||
let isRunning = state.isEnabled || state.runtime.isActive
|
||||
let isRunning = state.isEnabled || state.isRuntimeActive
|
||||
Button(
|
||||
isRunning ? String(localized: "Disable") : String(localized: "Enable"),
|
||||
systemImage: isRunning ? MenuSymbol.stop : MenuSymbol.start,
|
||||
@@ -198,7 +207,7 @@ private struct HostForwardingContextMenu: View {
|
||||
}
|
||||
|
||||
private func toggle() {
|
||||
if state.isEnabled || state.runtime.isActive {
|
||||
if state.isEnabled || state.isRuntimeActive {
|
||||
app.forwarding.setEnabled(false, for: rule.id)
|
||||
} else {
|
||||
app.forwarding.setEnabled(true, for: rule.id)
|
||||
@@ -245,7 +254,7 @@ struct PortForwardingEditorSheet: View {
|
||||
.disabled(!state.canToggle)
|
||||
LabeledContent(
|
||||
"Status",
|
||||
value: state.isWaitingForSave ? String(localized: "Waiting for Save") : state.runtime.label
|
||||
value: state.isWaitingForSave ? String(localized: "Waiting for Save") : runtime.label
|
||||
)
|
||||
if state.isWaitingForSave {
|
||||
Text("This forwarding will start after the host is saved.")
|
||||
@@ -279,16 +288,23 @@ struct PortForwardingEditorSheet: View {
|
||||
}
|
||||
|
||||
private var normalizedDraft: PortForwardingRule? {
|
||||
PortForwardingEditorState.normalized(draft)
|
||||
PortForwardingEditorPolicy.normalized(draft)
|
||||
}
|
||||
|
||||
private var state: PortForwardingEditorState {
|
||||
PortForwardingEditorState(ruleID: draft.id, rule: normalizedDraft, host: host, app: app)
|
||||
private var runtime: PortForwardingRuntimeState { app.forwarding.state(for: draft.id) }
|
||||
|
||||
private var state: PortForwardingEditorPolicy {
|
||||
PortForwardingEditorPolicy(
|
||||
rule: normalizedDraft,
|
||||
savedRule: app.forwardings.portForwarding(draft.id),
|
||||
savedHost: app.hostLibrary.host(host.id),
|
||||
isEnabled: app.forwarding.isEnabled(draft.id),
|
||||
isRuntimeActive: runtime.isActive)
|
||||
}
|
||||
|
||||
private var enabled: Binding<Bool> {
|
||||
Binding(
|
||||
get: { state.isEnabled || state.runtime.isActive },
|
||||
get: { state.isEnabled || state.isRuntimeActive },
|
||||
set: { value in
|
||||
app.forwarding.setEnabled(value, for: draft.id)
|
||||
})
|
||||
|
||||
@@ -16,12 +16,14 @@ struct PortForwardingEditorTests {
|
||||
@Test func savedConfigurationIsIndependentFromTheCurrentDraft() {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let rule = PortForwardingRule(hostID: host.id)
|
||||
let library = HostLibraryModel(hosts: [host])
|
||||
func state(_ candidate: PortForwardingRule?, _ candidateHost: Host, saved: Bool = true)
|
||||
-> PortForwardingEditorState
|
||||
-> PortForwardingEditorPolicy
|
||||
{
|
||||
PortForwardingEditorState(
|
||||
rule: candidate, host: candidateHost, savedRule: saved ? rule : nil,
|
||||
savedHost: saved ? host : nil, isEnabled: false, runtime: .stopped)
|
||||
PortForwardingEditorPolicy(
|
||||
rule: candidate, savedRule: saved ? rule : nil,
|
||||
savedHost: saved ? library.host(candidateHost.id) : nil,
|
||||
isEnabled: false, isRuntimeActive: false)
|
||||
}
|
||||
#expect(state(rule, host).canStart)
|
||||
#expect(state(rule, host).canEdit)
|
||||
@@ -34,29 +36,35 @@ struct PortForwardingEditorTests {
|
||||
var changedHost = host
|
||||
changedHost.host = "changed.invalid"
|
||||
#expect(state(rule, changedHost).canStart)
|
||||
changedHost.id = UUID()
|
||||
#expect(!state(rule, changedHost).canStart)
|
||||
let unsavedRule = PortForwardingRule(hostID: host.id)
|
||||
#expect(!state(unsavedRule, host).canStart)
|
||||
#expect(state(unsavedRule, host).canToggle)
|
||||
let deletedHost = PortForwardingEditorState(
|
||||
rule: rule, host: host, savedRule: rule, savedHost: nil, isEnabled: false, runtime: .stopped)
|
||||
let deletedHost = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: rule, savedHost: nil, isEnabled: false,
|
||||
isRuntimeActive: PortForwardingRuntimeState.stopped.isActive)
|
||||
#expect(!deletedHost.canStart)
|
||||
}
|
||||
|
||||
@Test func allActivePhasesAllowStoppingDirtyDraftsAndLockEditing() {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
@Test(arguments: [false, true])
|
||||
func allActivePhasesAllowStoppingDirtyDraftsAndLockEditing(isEnabled: Bool) {
|
||||
let phases: [PortForwardingRuntimeState] = [
|
||||
.preparing, .awaitingTrust, .awaitingAuthentication, .connecting, .starting,
|
||||
.running(activeConnections: 2), .reconnecting(attempt: 1, nextRetry: .now, message: "Retry"),
|
||||
]
|
||||
for phase in phases {
|
||||
let state = PortForwardingEditorState(
|
||||
rule: nil, host: host, savedRule: nil, savedHost: nil, isEnabled: true, runtime: phase)
|
||||
let state = PortForwardingEditorPolicy(
|
||||
rule: nil, savedRule: nil, savedHost: nil, isEnabled: isEnabled, isRuntimeActive: phase.isActive)
|
||||
#expect(state.canToggle)
|
||||
#expect(!state.canStart)
|
||||
#expect(!state.canEdit)
|
||||
#expect(!state.isWaitingForSave)
|
||||
#expect(state.isEnabled || state.isRuntimeActive)
|
||||
}
|
||||
let stopped = PortForwardingEditorState(
|
||||
rule: nil, host: host, savedRule: nil, savedHost: nil, isEnabled: false, runtime: .stopped)
|
||||
let stopped = PortForwardingEditorPolicy(
|
||||
rule: nil, savedRule: nil, savedHost: nil, isEnabled: false,
|
||||
isRuntimeActive: PortForwardingRuntimeState.stopped.isActive)
|
||||
#expect(stopped.canEdit)
|
||||
#expect(stopped.canToggle)
|
||||
}
|
||||
@@ -64,16 +72,16 @@ struct PortForwardingEditorTests {
|
||||
@Test func enabledUnsavedRulesWaitForSaveAndRemainEditable() {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let rule = PortForwardingRule(hostID: host.id)
|
||||
let waiting = PortForwardingEditorState(
|
||||
rule: rule, host: host, savedRule: nil, savedHost: nil,
|
||||
isEnabled: true, runtime: .stopped)
|
||||
let waiting = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: nil, savedHost: nil,
|
||||
isEnabled: true, isRuntimeActive: PortForwardingRuntimeState.stopped.isActive)
|
||||
#expect(waiting.isWaitingForSave)
|
||||
#expect(waiting.canEdit)
|
||||
#expect(waiting.canToggle)
|
||||
|
||||
let saved = PortForwardingEditorState(
|
||||
rule: rule, host: host, savedRule: rule, savedHost: host,
|
||||
isEnabled: true, runtime: .failed("Connection refused"))
|
||||
let saved = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: rule, savedHost: host,
|
||||
isEnabled: true, isRuntimeActive: PortForwardingRuntimeState.failed("Connection refused").isActive)
|
||||
#expect(!saved.isWaitingForSave)
|
||||
#expect(saved.canEdit)
|
||||
}
|
||||
@@ -81,49 +89,62 @@ struct PortForwardingEditorTests {
|
||||
@Test func failedSavedRulesCanBeEditedAndRetried() {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let rule = PortForwardingRule(hostID: host.id)
|
||||
let state = PortForwardingEditorState(
|
||||
rule: rule, host: host, savedRule: rule, savedHost: host,
|
||||
isEnabled: false, runtime: .failed("Connection refused"))
|
||||
let state = PortForwardingEditorPolicy(
|
||||
rule: rule, savedRule: rule, savedHost: host,
|
||||
isEnabled: false, isRuntimeActive: PortForwardingRuntimeState.failed("Connection refused").isActive)
|
||||
#expect(state.canEdit)
|
||||
#expect(state.canStart)
|
||||
#expect(state.canToggle)
|
||||
}
|
||||
|
||||
@Test func normalizationMatchesSavedValuesAndRejectsInvalidFields() throws {
|
||||
@Test(arguments: [PortForwardingRule.Kind.local, .remote])
|
||||
func normalizationMatchesSavedValuesAndRejectsInvalidFields(kind: PortForwardingRule.Kind) throws {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let saved = PortForwardingRule(hostID: host.id)
|
||||
let saved = PortForwardingRule(hostID: host.id, kind: kind)
|
||||
var draft = saved
|
||||
draft.bindHost = " 127.0.0.1\n"
|
||||
draft.targetHost = " localhost "
|
||||
let normalized = try #require(PortForwardingEditorState.normalized(draft))
|
||||
let normalized = try #require(PortForwardingEditorPolicy.normalized(draft))
|
||||
#expect(normalized == saved)
|
||||
|
||||
for validPort in [1, 65_535] {
|
||||
var boundary = saved
|
||||
boundary.bindPort = validPort
|
||||
boundary.targetPort = validPort
|
||||
#expect(PortForwardingEditorPolicy.normalized(boundary) == boundary)
|
||||
}
|
||||
for invalidPort in [0, 65_536] {
|
||||
var invalidBind = saved
|
||||
invalidBind.bindPort = invalidPort
|
||||
#expect(PortForwardingEditorState.normalized(invalidBind) == nil)
|
||||
#expect(PortForwardingEditorPolicy.normalized(invalidBind) == nil)
|
||||
|
||||
var invalidTarget = saved
|
||||
invalidTarget.targetPort = invalidPort
|
||||
#expect(PortForwardingEditorState.normalized(invalidTarget) == nil)
|
||||
#expect(PortForwardingEditorPolicy.normalized(invalidTarget) == nil)
|
||||
}
|
||||
|
||||
var emptyBindHost = saved
|
||||
emptyBindHost.bindHost = " \n"
|
||||
#expect(PortForwardingEditorState.normalized(emptyBindHost) == nil)
|
||||
#expect(PortForwardingEditorPolicy.normalized(emptyBindHost) == nil)
|
||||
|
||||
var emptyTargetHost = saved
|
||||
emptyTargetHost.targetHost = " \n"
|
||||
#expect(PortForwardingEditorState.normalized(emptyTargetHost) == nil)
|
||||
#expect(PortForwardingEditorPolicy.normalized(emptyTargetHost) == nil)
|
||||
emptyTargetHost.targetHost = nil
|
||||
#expect(PortForwardingEditorPolicy.normalized(emptyTargetHost) == nil)
|
||||
var missingTargetPort = saved
|
||||
missingTargetPort.targetPort = nil
|
||||
#expect(PortForwardingEditorPolicy.normalized(missingTargetPort) == nil)
|
||||
}
|
||||
|
||||
@Test func dynamicDraftDiscardsHiddenTargetValuesBeforeComparingOrSaving() throws {
|
||||
@Test(arguments: [0, 443, 65_536])
|
||||
func dynamicDraftDiscardsHiddenTargetValuesBeforeComparingOrSaving(hiddenTargetPort: Int) throws {
|
||||
let host = Host(name: "Server", host: "server.invalid")
|
||||
let saved = PortForwardingRule(hostID: host.id, kind: .dynamic)
|
||||
var draft = saved
|
||||
draft.targetHost = "old-target.invalid"
|
||||
draft.targetPort = 443
|
||||
let normalized = try #require(PortForwardingEditorState.normalized(draft))
|
||||
draft.targetPort = hiddenTargetPort
|
||||
let normalized = try #require(PortForwardingEditorPolicy.normalized(draft))
|
||||
#expect(normalized == saved)
|
||||
#expect(normalized.targetHost == nil)
|
||||
#expect(normalized.targetPort == nil)
|
||||
@@ -138,13 +159,24 @@ struct PortForwardingEditorTests {
|
||||
#expect(library.upsert(host, replacingPortForwardings: [rule]))
|
||||
// Reopening the host editor reloads persisted metadata, including its assigned sort index.
|
||||
let savedHost = try #require(library.host(host.id))
|
||||
let state = PortForwardingEditorState(ruleID: rule.id, rule: rule, host: savedHost, app: app)
|
||||
let state = PortForwardingEditorPolicy(
|
||||
rule: PortForwardingEditorPolicy.normalized(rule),
|
||||
savedRule: app.forwardings.portForwarding(rule.id),
|
||||
savedHost: app.hostLibrary.host(savedHost.id),
|
||||
isEnabled: app.forwarding.isEnabled(rule.id),
|
||||
isRuntimeActive: app.forwarding.state(for: rule.id).isActive)
|
||||
#expect(state.canStart)
|
||||
#expect(!app.forwarding.isEnabled(rule.id))
|
||||
#expect(app.forwarding.state(for: rule.id) == .stopped)
|
||||
var remotelyChanged = rule
|
||||
remotelyChanged.bindPort = 9000
|
||||
library.upsert(remotelyChanged)
|
||||
#expect(PortForwardingEditorState(ruleID: rule.id, rule: rule, host: savedHost, app: app).canStart)
|
||||
let reopened = PortForwardingEditorPolicy(
|
||||
rule: PortForwardingEditorPolicy.normalized(rule),
|
||||
savedRule: app.forwardings.portForwarding(rule.id),
|
||||
savedHost: app.hostLibrary.host(savedHost.id),
|
||||
isEnabled: app.forwarding.isEnabled(rule.id),
|
||||
isRuntimeActive: app.forwarding.state(for: rule.id).isActive)
|
||||
#expect(reopened.canStart)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user