diff --git a/ShadowsocksX-NG2/Tests/CustomRuleControllerTests.swift b/ShadowsocksX-NG2/Tests/CustomRuleControllerTests.swift index 0244994f..7226826d 100644 --- a/ShadowsocksX-NG2/Tests/CustomRuleControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/CustomRuleControllerTests.swift @@ -37,6 +37,9 @@ extension ProxyRuntimeControllerTests { return String(bytes: data, encoding: .utf8) ?? "" } +} + +final class CustomRuleControllerTests: ProxyRuntimeControllerTests { /// 规则内容变化重编译 ACL 并按完整重启路径生效。 func testUpdateCustomRulesRecompilesACLAndRestarts() async throws { let seeded = try makeSeededCatalog() diff --git a/ShadowsocksX-NG2/Tests/CustomRuleDeletionControllerTests.swift b/ShadowsocksX-NG2/Tests/CustomRuleDeletionControllerTests.swift index b86fff01..a181b6de 100644 --- a/ShadowsocksX-NG2/Tests/CustomRuleDeletionControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/CustomRuleDeletionControllerTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ShadowsocksX_NG2 -extension ProxyRuntimeControllerTests { +final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests { func testDeletionWhileOffPersistsBatchAndDisabledOrphansWithoutStarting() async throws { let (store, _) = try makeCustomRuleStore() let rules = [ diff --git a/ShadowsocksX-NG2/Tests/CustomRuleEditingControllerTests.swift b/ShadowsocksX-NG2/Tests/CustomRuleEditingControllerTests.swift index 5e375b71..d49a76da 100644 --- a/ShadowsocksX-NG2/Tests/CustomRuleEditingControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/CustomRuleEditingControllerTests.swift @@ -4,7 +4,7 @@ import XCTest /// Verify the editor's public commands against the production commit adapter, /// using the existing hostless runtime and system-proxy fixtures. -extension ProxyRuntimeControllerTests { +final class CustomRuleEditingControllerTests: ProxyRuntimeControllerTests { func testEditorSaveWhileOffPersistsWithoutStartingRuntime() async throws { let (store, _) = try makeCustomRuleStore() let controller = makeControllerWithCustomRules( diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift index 0d676fba..283dca4f 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift @@ -5,7 +5,7 @@ import XCTest /// Mode restoration and commands share the Domain availability policy without /// starting a real runtime or touching host proxy settings. @MainActor -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeControllerModeTests: ProxyRuntimeControllerTests { func testRuntimeFactsProjectControllerStateAtStableSeam() async { let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable()) @@ -26,7 +26,7 @@ extension ProxyRuntimeControllerTests { // 旧收敛复活;restore 先查时效再动持久层) @MainActor -extension ProxyRuntimeControllerTests { +extension ProxyRuntimeControllerModeTests { /// agent-off 落在 mode 切换的健康门 await 窗口内:票据查全四事实 → /// superseded,不触发 restore 重注册,用户的 mode 意图保留在持久层与内存。 func testAgentOffDuringModeTransitionHealthGateSupersedesWithoutRestore() async throws { diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift index 4c473b53..a191d04b 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift @@ -5,7 +5,7 @@ import XCTest /// Agent 开关与系统代理开关的拆分语义(issue #60):与既有开关用例同文件 /// 追加(Xcode 测试扫描限制),扩展持有独立用例组。 -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeAgentAndSystemProxyTests: ProxyRuntimeControllerTests { func testEnableWithoutActiveTargetDeploysEmptyServerListening() async throws { _ = try makeSeededCatalog() let controller = makeController( @@ -195,6 +195,9 @@ extension ProxyRuntimeControllerTests { } /// 关闭 agent 的级联(issue #71):系统代理意图仍开启时一并持久化为关闭, /// 先请求清理、再停止监听;之后重启 agent 不会恢复系统代理意图。 +} + +extension ProxyRuntimeAgentAndSystemProxyTests { func testAgentOffClearsSystemProxyBeforeStoppingListeningAndCascadesIntentOff() async throws { let seeded = try makeSeededCatalog() let settingsStore = InMemoryProxySettingsStore() @@ -450,7 +453,7 @@ extension ProxyRuntimeControllerTests { } } -extension ProxyRuntimeControllerTests { +extension ProxyRuntimeAgentAndSystemProxyTests { /// 意图已开启时的开启命令是失败态的显式重试入口(issue #60):无需先关再开。 func testAgentEnableCommandRetriesConvergenceWhileIntentAlreadyOn() async throws { let seeded = try makeSeededCatalog() diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift index 3c6464cf..54e34fa8 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift @@ -4,7 +4,7 @@ import XCTest /// 目录提交 → 运行时收敛语义(D3/D5 与 CONTEXT.md「有效值未变的提交不做 /// 运行时收敛」):与主测试类同构扩展,按既有模式独立成文件。 -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeCatalogConvergenceTests: ProxyRuntimeControllerTests { // MARK: 目录重展开消费(D3/D5) /// 模拟 wrapper 在跑(pid 指向本测试进程,kill(pid, 0) 判活通过)。 diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Firewall.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Firewall.swift index a7b170a4..17ef5f04 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Firewall.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Firewall.swift @@ -3,7 +3,7 @@ import XCTest @testable import ShadowsocksX_NG2 -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeFirewallTests: ProxyRuntimeControllerTests { func testLocalhostModeNeverQueriesApplicationFirewall() async throws { let seeded = try makeSeededCatalog() let firewall = ProxyRuntimeFixture.FakeFirewallChecker(.blocked) diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift index 4ccdf3e9..a2de13c0 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift @@ -5,7 +5,7 @@ import XCTest /// 全局模式以 ACL 实现(issue #62):proxy_all + 固定本地绕过;无活动服务器 /// 时 agent 仍监听、系统代理保持待应用;ACL 变化完整重启并失败回滚。 -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeGlobalModeTests: ProxyRuntimeControllerTests { func testGlobalModeDeploysProxyAllACLAndProjectsSOCKSProxyWithFixedExceptions() async throws { let seeded = try makeSeededCatalog() let settingsStore = InMemoryProxySettingsStore() diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift index f1b314eb..e623f889 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift @@ -3,7 +3,7 @@ import XCTest @testable import ShadowsocksX_NG2 -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeModeCommandTests: ProxyRuntimeControllerTests { func testSetProxyModePersistsTheChoiceAndARestoreRestoresIt() async throws { let settingsStore = InMemoryProxySettingsStore() let controller = makeController( diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift index 1eec50b1..9ea81546 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift @@ -3,7 +3,7 @@ import XCTest @testable import ShadowsocksX_NG2 -extension ProxyRuntimeControllerTests { +final class ProxyRuntimeResyncAndSettingsTests: ProxyRuntimeControllerTests { // MARK: GUI 重启重同步(D5) func testResyncWithRegisteredAgentAndMatchingContractSkipsRewrite() async throws { diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift index 57321b42..6ff08012 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift @@ -7,7 +7,7 @@ import XCTest /// agent 开关与系统代理开关的独立语义、目录重展开消费与 GUI 重同步重合(不 /// 触真实 SMAppService/launchd/SystemConfiguration)。 @MainActor -final class ProxyRuntimeControllerTests: XCTestCase { +class ProxyRuntimeControllerTests: XCTestCase { var runtime: ProxyRuntimeFixture.TemporaryRuntime! var catalogFileURL: URL! var activationFileURL: URL! @@ -24,6 +24,10 @@ final class ProxyRuntimeControllerTests: XCTestCase { private(set) var records: [(pid: Int32, signal: Int32)] = [] /// SIGUSR1 送达后模拟 wrapper 热重载刷新回执。 var reloadReceipt: (() -> Void)? + /// wrapper 进程存活位:生产中注销 LaunchAgent 后 wrapper 随 launchd 停止 + /// 消亡,pid 文件留存至 deleteRuntimeFiles、死亡靠 kill(_, 0) 探测; + /// 重注册即拉起新进程。不置死则停止协议会空等退出超时上限。 + private var wrapperAlive = true var signalsSent: [(pid: Int32, signal: Int32)] { lock.lock() @@ -37,6 +41,20 @@ final class ProxyRuntimeControllerTests: XCTestCase { lock.unlock() } + /// 注销 LaunchAgent:对应 wrapper 进程消亡。 + func terminateWrapper() { + lock.lock() + wrapperAlive = false + lock.unlock() + } + + /// 重新注册:拉起新 wrapper 进程。 + func relaunchWrapper() { + lock.lock() + wrapperAlive = true + lock.unlock() + } + func send(_ pid: Int32, _ signal: Int32) -> Int32 { record(pid, signal) if signal == SIGUSR1 { @@ -45,7 +63,10 @@ final class ProxyRuntimeControllerTests: XCTestCase { // kill(_, 0) 判活语义:只对夹具约定的存活 pid(42 与本测试进程)报告 // 在跑;陈旧残留 pid 返回 ESRCH,停止协议才不必空等退出超时上限。 if signal == 0 { - return pid == 42 || pid == ProcessInfo.processInfo.processIdentifier + lock.lock() + let alive = wrapperAlive + lock.unlock() + return (pid == 42 && alive) || pid == ProcessInfo.processInfo.processIdentifier ? 0 : Int32(ESRCH) } return 0 @@ -63,6 +84,8 @@ final class ProxyRuntimeControllerTests: XCTestCase { systemProxyHelper = ProxyRuntimeFixture.FakeSystemProxyHelperService() systemProxyNetworkChangeMonitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor() signals = SignalRecorder() + // 注销即 wrapper 消亡:停止协议的退出等待据 kill(_, 0) 放行,不空等超时。 + agent.onUnregister = { [weak self] in self?.signals?.terminateWrapper() } } override func tearDown() async throws { @@ -102,7 +125,9 @@ final class ProxyRuntimeControllerTests: XCTestCase { ) -> ProxyRuntimeController { agent.setStatus(agentStatus) let runtimeFileStore = RuntimeFileStore(fileURL: runtime.contract) - agent.onRegister = { [runtimeFileStore] in + agent.onRegister = { [runtimeFileStore, signals] in + // 重注册=拉起新 wrapper 进程(注销时已随 LaunchAgent 置死)。 + signals?.relaunchWrapper() guard let document = runtimeFileStore.loadDocument() else { return } try? runtimeFileStore.writeRuntimeReceipt(for: document, processID: 42) } @@ -145,6 +170,10 @@ final class ProxyRuntimeControllerTests: XCTestCase { processIsAlive: processIsAlive) } +} + +/// Concrete suite; the shared fixture must not contain inherited test methods. +final class ProxyRuntimeControllerCoreTests: ProxyRuntimeControllerTests { // MARK: Agent 开关与首次默认 func testActivateWhileAgentOffPublishesActiveTargetIDWithoutDeploying() async throws { diff --git a/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift b/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift index c1496641..516e26e3 100644 --- a/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ShadowsocksX_NG2 -extension ProxyRuntimeControllerTests { +final class RuleEnablementControllerTests: ProxyRuntimeControllerTests { func testRapidRuleSavesCoalesceIntoOneLatestACLDeployment() async throws { let seeded = try makeSeededCatalog() let (store, _) = try makeCustomRuleStore() @@ -38,7 +38,7 @@ extension ProxyRuntimeControllerTests { store: store, settings: ProxySettings( listen: ActivationFixture.listen, preferredMode: .rule, agentEnabled: true), - proxyMode: .rule, launchHealthTimeoutSeconds: 2) + proxyMode: .rule, launchHealthTimeoutSeconds: 0.3) try await controller.activate(seeded.server) let runtimeStore = RuntimeFileStore(fileURL: runtime.contract) let started = expectation(description: "first deployment awaiting receipt") @@ -238,6 +238,9 @@ extension ProxyRuntimeControllerTests { XCTAssertEqual(agent.registerCount, 0) } +} + +extension RuleEnablementControllerTests { func testAgentOffDuringRecoveryCannotBeOverwrittenByOldRuleTransaction() async throws { try await interruptRuleRecovery(switchMode: false) } @@ -256,7 +259,7 @@ extension ProxyRuntimeControllerTests { ruleDefaultAction: .proxyWhenUnmatched, agentEnabled: true) let controller = makeControllerWithCustomRules( store: store, settings: settings, proxyMode: .rule, - launchHealthTimeoutSeconds: 1) + launchHealthTimeoutSeconds: 0.2) try await controller.activate(seeded.server) let runtimeStore = RuntimeFileStore(fileURL: runtime.contract) let recoveryStarted = expectation(description: "recovery awaiting health")