Files
Qiu Yuzhou f6abd4343e feat(rules)!: add isolated routing presets
Add Mainland China proxy routing with independent rule workspaces.
Preserve existing routing choices through equivalent preset mappings.

BREAKING CHANGE: existing custom rules and enablement settings are
not migrated. Recreate them in the appropriate preset.
2026-10-25 14:32:12 +08:00

160 lines
6.1 KiB
Swift

import XCTest
@testable import ShadowsocksX_NG2
/// 状态菜单呈现逻辑:头部状态摘要从代理
/// 控制工作流的整体 snapshot 派生,agent 与系统代理两个状态面分开呈现。不
/// 持有 AppKit 类型;HTTP 导出行与目标路径摘要分别在代理控制与目录 module
/// 测试覆盖。
final class StatusMenuModelTests: XCTestCase {
private struct RuntimeSummaryCase {
let facts: ProxyRuntimeFacts
let expectedIsOn: Bool
}
private static let runtimeSummaryCases: [RuntimeSummaryCase] = [
RuntimeSummaryCase(
facts: ProxyRuntimeFacts(status: .starting, isOn: true),
expectedIsOn: true),
RuntimeSummaryCase(
facts: ProxyRuntimeFacts(status: .running, isOn: true),
expectedIsOn: true),
RuntimeSummaryCase(
facts: ProxyRuntimeFacts(
status: .firewallBlocked,
isOn: true,
failure: .firewallBlocked(FirewallBlockedFacts(executableName: "sslocal"))),
expectedIsOn: true),
RuntimeSummaryCase(
facts: ProxyRuntimeFacts(
status: .launchFailed,
isOn: false,
failure: .launch(
.localEndpoint(
endpoint: "http", host: "127.0.0.1", port: 11087, cause: .refused))),
expectedIsOn: false),
RuntimeSummaryCase(
facts: ProxyRuntimeFacts(
status: .requiresApproval, isOn: true, failure: .requiresApproval),
expectedIsOn: true),
RuntimeSummaryCase(
facts: ProxyRuntimeFacts(
status: .serviceFailed,
isOn: false,
failure: .service(.persistence)),
expectedIsOn: false),
]
/// 从运行时事实构造最小 snapshot(其余字段与本组用例无关)。
private func makeSnapshot(
facts: ProxyRuntimeFacts,
agentIntent: Bool = true,
activationFailure: ActivationFailure? = nil,
systemProxyIntent: Bool = false,
systemProxyApplication: SystemProxyApplicationFacts = .idle,
systemProxyApprovalRequired: Bool = false,
mode: ProxyMode = .rule,
activeTarget: ProxyActiveTargetFacts? = nil
) -> ProxyControlSnapshot {
ProxyControlSnapshot(
runtime: facts,
agentIntentEnabled: agentIntent,
activationFailure: activationFailure,
systemProxyIntentEnabled: systemProxyIntent,
systemProxyApplication: systemProxyApplication,
systemProxyApprovalRequired: systemProxyApprovalRequired,
proxyMode: mode,
rulePreset: .chinaDirect,
availableModes: ProxyMode.availableModes,
activeTarget: activeTarget,
skippedInvalidServerCount: 0,
httpExport: HTTPExportCapability(
copyableLine:
"export http_proxy=http://127.0.0.1:11087;export https_proxy=http://127.0.0.1:11087;"),
commandAddressPicker: TerminalCommandAddressPicker(
isVisible: false,
candidates: [
TerminalCommandAddress(bsdName: "lo0", address: "127.0.0.1", displayName: "lo0")
],
selected: TerminalCommandAddress(bsdName: "lo0", address: "127.0.0.1", displayName: "lo0")),
terminalProxyEnvironmentCommands: TerminalProxyEnvironmentCommands(
listen: RuntimeListenFacts(listen: ProxyListeningConfiguration()),
commandAddress: TerminalCommandAddress(
bsdName: "lo0", address: "127.0.0.1", displayName: "lo0")))
}
// MARK: - 头部状态摘要
func testSummaryRunningStateIsOnWithNoDetail() {
let summary = StatusMenuModel.summary(
from: makeSnapshot(facts: ProxyRuntimeFacts(status: .running, isOn: true)))
XCTAssertTrue(summary.agentIntentEnabled)
XCTAssertTrue(summary.isOn)
XCTAssertNil(summary.detail)
}
func testSummaryOffStateIsOff() {
let summary = StatusMenuModel.summary(
from: makeSnapshot(
facts: ProxyRuntimeFacts(status: .off, isOn: false),
agentIntent: false,
mode: .global))
XCTAssertFalse(summary.agentIntentEnabled, "开关意图独立于运行状态")
XCTAssertFalse(summary.isOn)
XCTAssertNil(summary.detail)
}
func testSummaryProjectsEveryStableRuntimeState() {
for testCase in Self.runtimeSummaryCases {
let summary = StatusMenuModel.summary(from: makeSnapshot(facts: testCase.facts))
XCTAssertEqual(summary.isOn, testCase.expectedIsOn)
XCTAssertEqual(
summary.detail,
testCase.facts.failure.map { AppPresentation.message(for: $0) })
}
}
/// 激活拒绝/目标清除的点名原因独立于运行状态呈现:agent 仍
/// 在监听时也必须可读。
func testSummaryCarriesActivationFailureAlongsideRunningState() {
let summary = StatusMenuModel.summary(
from: makeSnapshot(
facts: ProxyRuntimeFacts(status: .running, isOn: true),
activationFailure: .targetNotFound(NodeID(rawValue: "manual:gone"))))
XCTAssertEqual(
summary.detail,
AppPresentation.message(
for: ActivationFailure.targetNotFound(
NodeID(rawValue: "manual:gone"))))
}
/// 系统代理状态面:意图与实际应用分开,失败携带点名原因。
func testSummaryProjectsSystemProxyIntentAndFailureDetail() {
let failed = StatusMenuModel.summary(
from: makeSnapshot(
facts: ProxyRuntimeFacts(status: .running, isOn: true),
systemProxyIntent: true,
systemProxyApplication: .failed(.operation(.applyFailed))))
XCTAssertTrue(failed.systemProxyIntentEnabled)
XCTAssertEqual(
failed.systemProxyDetail,
AppPresentation.message(
for: RuntimeFailureFacts.systemProxy(.operation(.applyFailed))))
}
func testSummaryCarriesTargetPathFromSnapshot() {
let summary = StatusMenuModel.summary(
from: makeSnapshot(
facts: ProxyRuntimeFacts(status: .running, isOn: true),
activeTarget: ProxyActiveTargetFacts(
id: NodeID(rawValue: "manual:server"), pathSummary: "订阅分组 / 嵌套分组 / 日本 02")))
XCTAssertEqual(summary.targetPath, "订阅分组 / 嵌套分组 / 日本 02")
}
func testSummaryTargetPathIsNilWithoutActiveTarget() {
let summary = StatusMenuModel.summary(
from: makeSnapshot(facts: ProxyRuntimeFacts(status: .off, isOn: false)))
XCTAssertNil(summary.targetPath)
}
}