Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/ProxyRuntime/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift
T
Qiu YuzhouandDeepSeek Harness e7116a5ebd test: merge duplicated cases into the stronger one
Every case here restates a rule another test already pins with the same or a
wider input class:

- the activation group case is byte-identical to its neighbour, minus an extra
  assertion; the rule snapshot version guard is re-driven with a second value
  of the same schemaVersion check; the global-mode deployment-failure case is
  a mode-literal copy of the strictly stronger direct-mode one; the clear-
  failure case duplicates the deferred-retry sibling
- two RuntimeDocument projection cases only re-map locals, already asserted as
  full array equality; the IPv6-LAN endpoint case repeats the direct-mode case
  (the derivation reads only ports)
- the three HTTP export cases varied only listenerMode while the derivation
  ignores it and one name contradicted its own expectation; one loop over all
  listener modes now states the real rule
- the 0700/0600 permission baseline is one AtomicFileWriter behavior and stays
  pinned through RuntimeFileStoreTests, which also covers self-heal and atomic
  replace

The command-boundary case keeps its mixed-scenario isolation check (four
commands, four independent adapter counters) and drops only its unfalsifiable
queriedTargetIDs claim.

Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
2026-10-21 01:46:29 +08:00

514 lines
25 KiB
Swift

import Combine
import XCTest
@testable import ShadowsocksX_NG2
/// Agent 开关与系统代理开关的拆分语义:与既有开关用例同文件
/// 追加(Xcode 测试扫描限制),扩展持有独立用例组。
final class ProxyRuntimeAgentAndSystemProxyTests: ProxyRuntimeControllerTests {
override func makeDefaultRuleSnapshots() -> BuiltinRuleSnapshots {
BuiltinRuleSnapshots(loader: ProxyRuntimeFixture.controlFlowRuleSnapshot)
}
func testEnableWithoutActiveTargetDeploysEmptyServerListening() async throws {
_ = try makeSeededCatalog()
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
settings: ProxyRuntimeSettings(listen: ActivationFixture.listen, agentEnabled: false))
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.state, .running, "无活动目标 agent 仍提供本地监听")
XCTAssertEqual(agent.registerCount, 1, "开启意图驱动注册")
XCTAssertEqual(controller.controlPreferences.agentEnabled, true, "开启意图随快照持久化")
let onDisk = try XCTUnwrap(
RuntimeFileStore(fileURL: runtime.contract).loadDocument())
XCTAssertTrue(onDisk.servers.isEmpty, "无活动目标以空服务器列表监听")
XCTAssertEqual(onDisk.socksPort, ActivationFixture.listen.socksPort)
XCTAssertTrue(systemProxy.applied.isEmpty, "系统代理意图关闭,不写系统设置")
XCTAssertEqual(controller.systemProxyState, .idle)
}
/// 首次运行默认语义(ADR-0011):全新用户没有服务器配置,GUI 重同步不
/// 注册、不拉起 Agent;启动永不清理系统代理设置。
func testFirstRunResyncKeepsAgentOffWithoutSystemProxy() async throws {
_ = try makeSeededCatalog()
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
settings: ProxyRuntimeSettings(listen: ActivationFixture.listen, agentEnabled: false))
await controller.resyncOnLaunch()
XCTAssertEqual(controller.state, .off)
XCTAssertEqual(agent.registerCount, 0, "首次运行默认不启动 agent")
XCTAssertFalse(controller.settings.agentEnabled)
XCTAssertEqual(systemProxy.clearCount, 0, "启动时意图关闭不清理系统设置")
XCTAssertFalse(systemProxyNetworkChangeMonitor.isObserving, "无观察进行")
XCTAssertNil(
RuntimeFileStore(fileURL: runtime.contract).loadDocument(),
"Agent 未启用时不写运行时契约")
XCTAssertEqual(controller.systemProxyState, .idle)
XCTAssertTrue(systemProxy.applied.isEmpty)
}
/// 显式关闭 agent 的选择持久化:GUI 重启(全新控制器 + 恢复
/// 快照 + 注册态残留)后仍收敛到关闭。
func testExplicitAgentOffPersistsAcrossGUIRestart() async throws {
let seeded = try makeSeededCatalog()
let listeningStore = InMemoryProxyListeningStore()
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(), listeningStore: listeningStore,
settings: ProxyRuntimeSettings(listen: ActivationFixture.listen, agentEnabled: true))
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setAgentEnabled(false)
XCTAssertEqual(controller.controlPreferences.agentEnabled, false, "关闭选择已持久化")
// GUI 重启:agent 注册态残留、运行时文件残留,恢复快照携带关闭意图。
agent.setStatus(.registered)
try Data("stale contract".utf8).write(to: runtime.contract)
try Data("99999".utf8).write(to: runtime.pidFile)
let restarted = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
agentStatus: .registered,
listeningStore: listeningStore,
settingsRestore: RestoredProxyRuntimeSettings(
settings: ProxyRuntimeSettings(
listen: ActivationFixture.listen, preferences: controlPreferences), unreadableError: nil))
await restarted.resyncOnLaunch()
XCTAssertEqual(restarted.state, .off, "显式关闭的选择在 GUI 重启后仍生效")
XCTAssertEqual(agent.registerCount, 1, "重启后的关闭路径不得重新注册")
XCTAssertEqual(agent.unregisterCount, 2, "注册态残留按停止协议再次收敛")
XCTAssertFalse(FileManager.default.fileExists(atPath: runtime.contract.path))
}
func testSystemProxyIntentAppliesOnlyAfterHealthGateAndExitAvailable() async throws {
let seeded = try makeSeededCatalog()
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(controller.systemProxyState, .applied)
XCTAssertEqual(
systemProxy.applied,
[
SystemProxyConfiguration(
socks: .init(host: "127.0.0.1", port: ActivationFixture.listen.socksPort),
http: .init(host: "127.0.0.1", port: ActivationFixture.listen.httpPort),
https: .init(host: "127.0.0.1", port: ActivationFixture.listen.httpPort),
exceptions: FixedLocalProxyRanges.systemProxyExceptions(
including: ProxyRuntimeSettings().proxyExceptionList))
],
"系统代理只在本地端点健康且存在可用出口后写入")
}
/// 端点不健康时系统代理意图保持待应用;条件恢复后随下次收敛自动应用,
/// 无需重新开关系统代理(「条件恢复后自动收敛」)。
func testSystemProxyIntentStaysPendingUntilEndpointsRecover() async throws {
let seeded = try makeSeededCatalog()
let probe = ProxyRuntimeFixture.FakeProbe.refusing()
let controller = makeController(probe: probe)
// 激活即按默认意图部署:端点持续不可达 → 启动失败(走满注入的短健康窗)。
try await controller.activate(seeded.server)
if case .launchFailed = controller.state {
} else {
XCTFail("端点不可达应呈现启动失败,实际 \(controller.state)")
}
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(controller.systemProxyState, .paused, "失效时清除并保留意图")
XCTAssertTrue(systemProxy.applied.isEmpty, "端点不健康不得写系统代理")
// 条件恢复后由用户显式重试;普通收敛不得重试已确认失败。
probe.setOutcomes([.reachable])
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.state, .running)
XCTAssertEqual(controller.systemProxyState, .applied, "恢复后自动收敛待应用意图")
XCTAssertEqual(systemProxy.applied.count, 1)
}
/// 关闭系统代理:on→off 意图迁移触发一次无条件清理;本地监听
/// 与 agent 注册态不受影响。
func testSystemProxyOffClearsAllSettingsAndKeepsListening() async throws {
let seeded = try makeSeededCatalog()
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(controller.systemProxyState, .applied)
await controller.setSystemProxyEnabled(false)
XCTAssertEqual(controller.systemProxyState, .idle)
XCTAssertEqual(systemProxy.clearCount, 1)
XCTAssertEqual(controller.state, .running, "本地监听不受系统代理开关影响")
XCTAssertEqual(agent.unregisterCount, 0, "不注销 agent")
XCTAssertTrue(
FileManager.default.fileExists(atPath: runtime.contract.path), "运行时契约保留")
}
/// 系统代理意图持久化:GUI 重启后从恢复快照读回。
func testSystemProxyIntentPersistsAcrossGUIRestart() async throws {
let seeded = try makeSeededCatalog()
let listeningStore = InMemoryProxyListeningStore()
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(), listeningStore: listeningStore)
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(controller.controlPreferences.systemProxyEnabled, true)
let restarted = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
agentStatus: .registered,
listeningStore: listeningStore,
settingsRestore: RestoredProxyRuntimeSettings(
settings: ProxyRuntimeSettings(
listen: ActivationFixture.listen, preferences: controlPreferences), unreadableError: nil))
XCTAssertTrue(restarted.systemProxyIntentEnabled)
await restarted.resyncOnLaunch()
XCTAssertEqual(restarted.systemProxyState, .applied, "启动时先重应用当前网络位置")
XCTAssertTrue(systemProxyNetworkChangeMonitor.isObserving, "重同步完成后开始网络观察")
}
/// 系统代理写入错误以 typed fact 呈现,agent 继续运行。
func testSystemProxyApplyFailureSurfacesTypedFailureAndKeepsAgentRunning() async throws {
let seeded = try makeSeededCatalog()
systemProxy.applyError = SystemProxyError.applyFailed("write failed")
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(
controller.systemProxyState, .failed(.operation(.applyFailed)), "写入错误以 typed 呈现")
XCTAssertEqual(controller.state, .running, "agent 不受系统代理失败影响")
XCTAssertTrue(systemProxy.applied.isEmpty)
}
/// 关闭 agent 的级联:系统代理意图仍开启时一并持久化为关闭,
/// 先请求清理、再停止监听;之后重启 agent 不会恢复系统代理意图。
}
extension ProxyRuntimeAgentAndSystemProxyTests {
func testAgentOffClearsSystemProxyBeforeStoppingListeningAndCascadesIntentOff() async throws {
let seeded = try makeSeededCatalog()
let listeningStore = InMemoryProxyListeningStore()
let eventLog = ProxyRuntimeEventLog()
agent.eventLog = eventLog
systemProxy.eventLog = eventLog
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
listeningStore: listeningStore,
settings: ProxyRuntimeSettings(
listen: ActivationFixture.listen, agentEnabled: true, systemProxyEnabled: true))
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.systemProxyState, .applied)
await controller.setAgentEnabled(false)
XCTAssertEqual(controller.state, .off, "关闭级联后运行时停止")
XCTAssertEqual(
eventLog.events,
["register", "apply", "clear", "unregister"],
"先清理系统设置,再停止 agent 监听")
XCTAssertEqual(controller.systemProxyState, .idle, "级联后意图关闭且清理完成")
XCTAssertEqual(controller.controlPreferences.agentEnabled, false)
XCTAssertEqual(controller.controlPreferences.systemProxyEnabled, false, "级联关闭一并持久化")
// 重启 agent 不恢复系统代理意图。
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.state, .running)
XCTAssertEqual(systemProxy.applied.count, 1, "不再应用系统代理")
XCTAssertEqual(controller.systemProxyState, .idle)
XCTAssertFalse(systemProxyNetworkChangeMonitor.isObserving, "意图关闭不开网络观察")
}
/// 系统代理意图本就关闭时关闭 agent:不做任何系统设置操作。
func testAgentOffWithSystemProxyIntentAlreadyOffSendsNoClear() async throws {
let seeded = try makeSeededCatalog()
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setAgentEnabled(false)
XCTAssertEqual(controller.state, .off)
XCTAssertEqual(systemProxy.clearCount, 0, "未发生 on→off 迁移,不清理")
XCTAssertEqual(controller.systemProxyState, .idle)
}
/// 特权 helper 待批准:意图保持待应用、系统设置不变,
/// 置位批准路径;收敛被 helper 门禁挡下,不尝试写入。
func testHelperApprovalRequiredKeepsIntentPendingAndOffersApprovalPath() async throws {
let seeded = try makeSeededCatalog()
systemProxyHelper.setStatus(.requiresApproval)
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(controller.systemProxyState, .pending, "意图保持待应用")
XCTAssertTrue(controller.systemProxyApprovalRequired, "呈现批准路径")
XCTAssertTrue(systemProxy.applied.isEmpty, "helper 不可用不写入")
XCTAssertEqual(systemProxyNetworkChangeMonitor.isObserving, true, "意图开启照常观察")
// 批准路径:打开登录项设置并重试收敛;批准达成后 applied。
systemProxyHelper.setStatus(.approved)
await controller.systemProxyObserver.openSystemProxyHelperApproval()
XCTAssertEqual(controller.systemProxyState, .applied)
XCTAssertFalse(controller.systemProxyApprovalRequired)
XCTAssertEqual(systemProxyHelper.openApprovalPathCount, 1)
}
/// helper 未注册时显式开启即注册:注册成功走正常
/// 收敛;注册失败保持待应用与批准路径。
func testExplicitEnableRegistersHelperAndSurvivesRegistrationFailure() async throws {
let seeded = try makeSeededCatalog()
systemProxyHelper.setStatus(.notRegistered)
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(systemProxyHelper.registerCount, 1, "首次显式开启注册 helper")
XCTAssertEqual(controller.systemProxyState, .applied)
// 重新安装/注册丢失后的重试路径:注册失败也保持意图待应用。
let secondSystemProxy = ProxyRuntimeFixture.FakeSystemProxy()
systemProxyHelper.setStatus(.notRegistered)
systemProxyHelper.setRegisterError(
ProxyListeningStoreError.corrupt(detail: "SMError"))
systemProxyHelper.statusAfterRegister = .notRegistered
let second = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
settings: ProxyRuntimeSettings(
listen: ActivationFixture.listen, agentEnabled: true, systemProxyEnabled: true),
systemProxy: secondSystemProxy)
try await second.activate(seeded.server)
await second.setAgentEnabled(true)
await second.setSystemProxyEnabled(false)
await second.setSystemProxyEnabled(true)
XCTAssertEqual(second.systemProxyState, .pending, "注册失败保持待应用")
XCTAssertTrue(second.systemProxyApprovalRequired, "呈现批准路径")
XCTAssertTrue(secondSystemProxy.applied.isEmpty, "注册失败不写入")
}
/// 清理失败:typed 呈现且无后台重试;观察在该次尝试后停止。
func testDisableClearFailureIsPresentedWithoutDeferredRetry() async throws {
let seeded = try makeSeededCatalog()
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
systemProxy.clearError = SystemProxyError.helperUnavailable("helper gone")
await controller.setSystemProxyEnabled(false)
XCTAssertEqual(controller.systemProxyState, .clearFailed(.operation(.helperUnavailable)))
XCTAssertFalse(systemProxyNetworkChangeMonitor.isObserving, "清理失败后也结束临时观察")
XCTAssertEqual(systemProxy.clearCount, 1, "只请求一次清理,无后台重试")
}
func testEnabledSystemProxyInspectsAllPassiveChangesWithoutReapplying() async throws {
let seeded = try makeSeededCatalog()
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
XCTAssertEqual(systemProxy.applied.count, 1)
XCTAssertTrue(systemProxyNetworkChangeMonitor.isObserving)
systemProxyNetworkChangeMonitor.emit(.proxyConfiguration)
await Task.yield()
XCTAssertEqual(systemProxy.applied.count, 1, "代理配置变化不触发重写")
systemProxyNetworkChangeMonitor.emit(.networkConfiguration)
let reads = systemProxy.readCount
await waitUntil(systemProxy.readCount > reads)
XCTAssertEqual(systemProxy.applied.count, 1, "网络位置或服务变化只检查")
systemProxyNetworkChangeMonitor.emit(.networkPath)
let pathReads = systemProxy.readCount
await waitUntil(systemProxy.readCount > pathReads)
XCTAssertEqual(systemProxy.applied.count, 1, "网络路径变化只检查")
}
func testDisableCleanupRescansChangesThatArriveDuringCleanupThenStopsObservation() async throws {
let seeded = try makeSeededCatalog()
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
var emittedCleanupChange = false
let monitor = systemProxyNetworkChangeMonitor!
systemProxy.onClear = {
guard !emittedCleanupChange else { return }
emittedCleanupChange = true
monitor.emit(.proxyConfiguration)
}
await controller.setSystemProxyEnabled(false)
XCTAssertEqual(systemProxy.clearCount, 2, "清理期间代理变化触发第二次全量扫描")
XCTAssertFalse(systemProxyNetworkChangeMonitor.isObserving, "关闭清理结束后停止监听")
XCTAssertEqual(controller.systemProxyState, .idle)
}
/// 无效活动目标:清除目标且不悄悄回退;agent 继续以空服务器
/// 列表监听;目标失效不清理系统设置,意图保持待应用。
func testInvalidTargetOnCommitKeepsListeningAndHoldsSystemProxyIntent() async throws {
let seeded = try makeSeededCatalog()
try Data("\(ProcessInfo.processInfo.processIdentifier)".utf8).write(to: runtime.pidFile)
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
settings: ProxyRuntimeSettings(
listen: ActivationFixture.listen, agentEnabled: true, systemProxyEnabled: true))
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.systemProxyState, .applied)
// 删除活动目标 → 重展开失败 → 清除目标,agent 继续监听。
var catalog = try makeTestCatalogStore(fileURL: catalogFileURL).load().catalog
try catalog.remove(seeded.server)
try makeTestCatalogStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
let committed = try makeTestCatalogStore(fileURL: catalogFileURL).load().catalog
_ = await controller.catalogDidCommit(snapshot: committed)
XCTAssertNil(controller.activeTargetID)
XCTAssertNotNil(controller.lastActivationFailure, "点名原因独立呈现")
XCTAssertEqual(controller.state, .running, "agent 保持监听")
XCTAssertEqual(agent.unregisterCount, 0, "不注销 agent")
let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument())
XCTAssertTrue(onDisk.servers.isEmpty, "运行时以空服务器列表继续监听")
XCTAssertEqual(systemProxy.clearCount, 1, "目标失效清除不可用入口")
XCTAssertEqual(controller.systemProxyState, .paused, "清除成功后暂停并保留意图")
}
func testResyncWithInvalidActiveTargetClearsAndKeepsListening() async throws {
_ = try makeSeededCatalog()
try Data("garbage".utf8).write(to: runtime.contract)
let missingTarget = NodeID(rawValue: "removed-target")
try ActivationStateFileStore(fileURL: activationFileURL).save(activeTargetID: missingTarget)
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(), agentStatus: .registered)
await controller.resyncOnLaunch()
XCTAssertEqual(controller.state, .running, "目标失效后 agent 继续监听")
XCTAssertNil(controller.activeTargetID)
let persisted = try ActivationStateFileStore(fileURL: activationFileURL).loadActiveTargetID()
XCTAssertNil(persisted, "失效目标已清除")
let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument())
XCTAssertTrue(onDisk.servers.isEmpty, "以空服务器列表继续监听")
XCTAssertNotNil(controller.lastActivationFailure)
XCTAssertTrue(systemProxy.applied.isEmpty, "系统代理意图关闭,不写系统设置")
}
}
extension ProxyRuntimeAgentAndSystemProxyTests {
/// 意图已开启时的开启命令是失败态的显式重试入口:无需先关再开。
func testAgentEnableCommandRetriesConvergenceWhileIntentAlreadyOn() async throws {
let seeded = try makeSeededCatalog()
let probe = ProxyRuntimeFixture.FakeProbe.refusing()
let controller = makeController(probe: probe)
// 激活即按默认意图部署:端点不可达 → 启动失败(走满注入的短健康窗)。
try await controller.activate(seeded.server)
if case .launchFailed = controller.state {
} else {
XCTFail("端点不可达应呈现启动失败,实际 \(controller.state)")
}
probe.setOutcomes([.reachable])
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.state, .running, "意图已开启时开启命令重走收敛")
XCTAssertEqual(controller.systemProxyState, .idle)
}
/// 等值零写入路径:seam 返回 unchanged 时仍呈现 applied,并
/// 发一条 GUI 事件行供诊断日志回答「这次为何没有授权弹窗」。
func testUnchangedSystemProxyValuesStayAppliedAndEmitEventLine() async throws {
let seeded = try makeSeededCatalog()
systemProxy.applyOutcome = .unchanged
let recorder = RuntimeLogRecorder()
RuntimeLog.setSink(recorder)
defer { RuntimeLog.setSink(nil) }
let controller = makeController(
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
settings: ProxyRuntimeSettings(
listen: ActivationFixture.listen, agentEnabled: true, systemProxyEnabled: true))
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
XCTAssertEqual(controller.systemProxyState, .applied, "unchanged 仍视为已应用")
XCTAssertGreaterThanOrEqual(
systemProxy.applied.count, 1, "收敛调用照常发生,跳过发生在 seam 内")
XCTAssertTrue(
recorder.recorded.contains(.systemProxyUnchanged),
"跳过路径发事件行,实际 \(recorder.recorded)")
}
/// LaunchDaemon 清单漂移:launchd 沿用注册提交时的 job
/// 定义快照,app 更新改了清单后须注销重注才生效。已批准注册仅在清单指纹
/// 与注册时不一致时重注(审批与签名绑定,不重弹授权);首次注册成功即记录
/// 指纹,指纹一致不动作。
func testHelperRegistrationRefreshesOnlyOnPlistDrift() async throws {
let defaults = UserDefaults.standard
let stampKey = SystemProxyHelperRegistrationStamp.defaultsKey
defer { defaults.removeObject(forKey: stampKey) }
defaults.removeObject(forKey: stampKey)
let seeded = try makeSeededCatalog()
systemProxyHelper.setStatus(.notRegistered)
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
try await controller.activate(seeded.server)
await controller.setAgentEnabled(true)
await controller.setSystemProxyEnabled(true)
let stampAfterRegister = try XCTUnwrap(
defaults.string(forKey: stampKey), "注册成功应记录 LaunchDaemon 清单指纹")
let registerCountAfterEnable = systemProxyHelper.registerCount
let unregisterCountAfterEnable = systemProxyHelper.unregisterCount
// 指纹一致:后续收敛不注销重注。
await controller.systemProxyObserver.convergeSystemProxy()
XCTAssertEqual(systemProxyHelper.registerCount, registerCountAfterEnable, "指纹一致不重注")
XCTAssertEqual(systemProxyHelper.unregisterCount, unregisterCountAfterEnable)
XCTAssertEqual(controller.systemProxyState, .applied)
// 指纹漂移(模拟 app 更新改了清单):注销重注一次并更新指纹。
defaults.set("drifted-stamp", forKey: stampKey)
await controller.systemProxyObserver.convergeSystemProxy()
XCTAssertEqual(systemProxyHelper.unregisterCount, unregisterCountAfterEnable + 1, "漂移触发重注")
XCTAssertEqual(systemProxyHelper.registerCount, registerCountAfterEnable + 1)
XCTAssertEqual(defaults.string(forKey: stampKey), stampAfterRegister, "重注后记录当前清单指纹")
XCTAssertEqual(controller.systemProxyState, .applied, "重注后照常收敛应用")
}
}
/// 测试专用事件接收缝。
private final class RuntimeLogRecorder: RuntimeEventSink, @unchecked Sendable {
private let lock = NSLock()
private var events: [RuntimeLogEvent] = []
var recorded: [RuntimeLogEvent] {
lock.lock()
defer { lock.unlock() }
return events
}
func append(event: RuntimeLogEvent, timestamp: Date) {
lock.lock()
events.append(event)
lock.unlock()
}
}