diff --git a/CONTEXT.md b/CONTEXT.md index edca4929..a556f161 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -31,7 +31,7 @@ - **Proxy runtime**: The per-user background host process, independent of the GUI's lifetime, that runs the external tunnel service and loads the optional generated ACL sidecar. - **Legacy configuration**: The server list and preferences persisted by the frozen implementation in `Legacy/`. - **Legacy import**: The user-confirmed, read-only translation of a Legacy configuration's server records into a separate manual group in 2.0. It migrates no preferences, activates no target, and neither starts proxying nor alters the Legacy source. -- **Runtime configuration file**: The derived JSON document used by the external tunnel service for the active target. Fields NG2 omits are left to the bundled `sslocal` defaults; the file is not the user-managed server configuration or subscription document. +- **Runtime configuration file**: The derived JSON document used by the external tunnel service for the active target. It carries only connection-effective values (endpoints, credentials, encryption, plugin, listeners, ACL); user-visible display names are catalog metadata and never enter the file. Fields NG2 omits are left to the bundled `sslocal` defaults; the file is not the user-managed server configuration or subscription document. - **Sensitive information**: Server passwords, plugin options (sensitive in their entirety), and nonempty user-provided remote URLs, including subscription URLs. - **Credential exposure boundary**: This product reduces accidental disclosure and exposure to other user accounts, but does not guarantee protection against a compromised same-user process, root access, or APFS snapshots and backups; it does not promise secure erasure. - **Credential reference**: A non-secret association from a server configuration, subscription, or remote-URL setting to its durable credential; the credential value is kept outside the configuration tree and resolved only when needed. @@ -60,6 +60,7 @@ - Activating a server selects that one server. Activating a group recursively expands activation-candidate server leaves in explicit child order and passes them to the external proxy service; invalid leaves are omitted and surfaced, while a group with no activation candidate rejects activation atomically. - The active target remains the selected node ID and is not silently replaced by a descendant chosen by `sslocal`. If it is deleted, has no activation candidate, or is otherwise invalid, the target is cleared and proxying stops rather than falling back silently. - After a committed edit to the active target's subtree, the target is re-expanded immediately: a non-empty set of activation candidates updates the runtime atomically; no activation candidate clears the target and stops proxying without fallback. +- A committed catalog edit that leaves the runtime configuration file's bytes unchanged — including edits outside the active target's subtree, remark-only changes, and no-op re-saves — triggers no runtime convergence work: no contract write, no reload signal, no restart of the external tunnel service, and no launch health re-probe. Catalog revalidation (activation-candidate skips, target invalidation) still happens on every commit. Within an active group subtree, a connection-value change — including member order — does change the runtime file and converges. - Manual nodes may move between the catalog root and manual groups, carrying their subtree and child order without changing identity or ownership; cross-source moves, shared parents, and cycles are rejected. - Manual catalog ownership is independent of proxy mode: removing the manual proxy mode does not remove manual groups, manual servers, or records imported into a manual group. - Empty groups may persist and remain editable, but cannot be activated. Deleting a server configuration or an empty manual group takes one user confirmation. Deleting a non-empty manual group recursively deletes its descendants and takes a second confirmation that names the subtree scale and that the subtree's server credentials are removed. diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+SettingsSync.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+SettingsSync.swift index 224c2600..2f1fe5af 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+SettingsSync.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+SettingsSync.swift @@ -13,6 +13,12 @@ extension ProxyRuntimeController { guard settings.agentEnabled else { return .revalidated } + if runtimeAlreadyConverged(with: configuration.document) { + RuntimeLog.emit(.contractUnchanged) + return Self.syncOutcome( + agentState: state, systemProxyState: systemProxyState, + skippedServers: configuration.skippedServers) + } await deploy(configuration.document) return Self.syncOutcome( agentState: state, systemProxyState: systemProxyState, @@ -36,6 +42,35 @@ extension ProxyRuntimeController { } } + /// 目录提交去抖(CONTEXT.md「有效值未变的提交不做运行时收敛」):派生文档 + /// (含模式/ACL 合并)与磁盘契约逐字节相等、agent 已注册且 wrapper 存活、 + /// 控制器处于健康运行态时,本次提交对运行时无事可做——不写契约、不发信号、 + /// 不闪 starting、不重走健康门与系统代理收敛。判定复用计划层语义 + /// (`ProxyRuntimePlan.actions` 返回空 = 幂等跳过):磁盘漂移或 wrapper 失踪 + /// 时动作序列自然非空,走完整 deploy 自愈。目录重校验(跳过名单、目标失效 + /// 清除)已在 `reexpand` 完成,不受此门影响。 + private func runtimeAlreadyConverged(with sourceDocument: SslocalRuntimeDocument) -> Bool { + switch state { + case .running, .firewallBlocked: + break + case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed: + return false + } + let document: SslocalRuntimeDocument + do { + document = try runtimeDocument(sourceDocument, for: proxyMode) + } catch { + // 规则快照缺失/损坏:交完整 deploy 如实呈现,不静默当作已收敛。 + return false + } + let actions = ProxyRuntimePlan.actions( + intent: .run(document), + agentStatus: agent.status, + wrapper: wrapperState(), + contractOnDisk: runtimeFileStore.readData()) + return actions.isEmpty + } + /// 部署后的控制器状态 → 结构化收敛结果:健康通过(含防火墙受阻的运行态) /// 视为已收敛;系统代理写入失败携带点名细节,待应用不算目录收敛失败。 private static func syncOutcome( diff --git a/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift b/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift index dbe00bcb..3f2ef959 100644 --- a/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift +++ b/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift @@ -221,7 +221,6 @@ struct ActivationStateMachine: Equatable, Sendable { return .success( SslocalServerDocument( id: leafID.rawValue, - remarks: fields.remark, server: fields.address.trimmingCharacters(in: .whitespacesAndNewlines), serverPort: fields.port, password: password, diff --git a/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift b/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift index b8927011..845ae8bf 100644 --- a/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift +++ b/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift @@ -164,11 +164,11 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { }() } -/// 上游 `servers[]` 条目。id/remarks 携带叶子身份与显示名,供上游稳定标识; +/// 上游 `servers[]` 条目。id 携带叶子身份,供上游稳定标识;显示名是目录 +/// 元数据,不进契约(CONTEXT.md「Runtime configuration file」)。 /// 插件字段在无插件时整体省略(D10)。 struct SslocalServerDocument: Codable, Equatable, Sendable { let id: String - let remarks: String let server: String let serverPort: Int let password: String @@ -177,7 +177,7 @@ struct SslocalServerDocument: Codable, Equatable, Sendable { let pluginOpts: String? enum CodingKeys: String, CodingKey { - case id, remarks, server + case id, server case serverPort = "server_port" case password, method, plugin case pluginOpts = "plugin_opts" diff --git a/ShadowsocksX-NG2/Tests/ActivationStateMachineTests.swift b/ShadowsocksX-NG2/Tests/ActivationStateMachineTests.swift index e0337c28..ab5e467f 100644 --- a/ShadowsocksX-NG2/Tests/ActivationStateMachineTests.swift +++ b/ShadowsocksX-NG2/Tests/ActivationStateMachineTests.swift @@ -30,7 +30,6 @@ final class ActivationStateMachineTests: XCTestCase { XCTAssertEqual(configuration.document.servers.count, 1) let entry = configuration.document.servers[0] XCTAssertEqual(entry.id, server.rawValue) - XCTAssertEqual(entry.remarks, "香港 01") XCTAssertEqual(entry.server, "203.0.113.7") XCTAssertEqual(entry.serverPort, 8388) XCTAssertEqual(entry.method, "aes-256-gcm") diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift new file mode 100644 index 00000000..3c6464cf --- /dev/null +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+CatalogConvergence.swift @@ -0,0 +1,137 @@ +import XCTest + +@testable import ShadowsocksX_NG2 + +/// 目录提交 → 运行时收敛语义(D3/D5 与 CONTEXT.md「有效值未变的提交不做 +/// 运行时收敛」):与主测试类同构扩展,按既有模式独立成文件。 +extension ProxyRuntimeControllerTests { + // MARK: 目录重展开消费(D3/D5) + + /// 模拟 wrapper 在跑(pid 指向本测试进程,kill(pid, 0) 判活通过)。 + private func simulateRunningWrapper() throws { + try Data("\(ProcessInfo.processInfo.processIdentifier)".utf8).write(to: runtime.pidFile) + } + + private func committedCatalog() throws -> ConfigurationCatalog { + try CatalogFileStore(fileURL: catalogFileURL).load().catalog + } + + private func replacedFields( + _ fields: ServerFields, address: String? = nil, remark: String? = nil + ) -> ServerFields { + ServerFields( + address: address ?? fields.address, + port: fields.port, + encryptionMethod: fields.encryptionMethod, + passwordRef: fields.passwordRef, + remark: remark ?? fields.remark, + pluginProgram: fields.pluginProgram, + pluginOptionsRef: fields.pluginOptionsRef) + } + + func testCatalogValueEditOfActiveTargetRewritesAndSignalsRunningWrapper() async throws { + let seeded = try makeSeededCatalog() + let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable()) + try await controller.activate(seeded.server) + await controller.setAgentEnabled(true) + try simulateRunningWrapper() + + // 修改激活服务器的连接地址(契约有效值变化),提交目录变更。 + var catalog = try CatalogFileStore(fileURL: catalogFileURL).load().catalog + let fields = try ActivationFixture.serverFields(of: seeded.server, in: catalog) + try catalog.updateServer( + seeded.server, with: replacedFields(fields, address: "203.0.113.9")) + try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog)) + + // 协调器生产适配入口(issue #40):以刚提交的内存快照重展开。 + _ = await controller.catalogDidCommit(snapshot: try committedCatalog()) + + XCTAssertEqual(controller.state, .running) + let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument()) + XCTAssertEqual(onDisk.servers.first?.server, "203.0.113.9", "运行时文档已原子更新") + let signalsSent = signals.signalsSent.filter { $0.signal != 0 } + XCTAssertEqual(signalsSent.count, 1, "代理运行中:重写后应发一次 SIGUSR1") + XCTAssertEqual(signalsSent.first?.signal, SIGUSR1) + } + + func testCatalogRemarkOnlyEditOfActiveTargetKeepsRuntimeUntouched() async throws { + let seeded = try makeSeededCatalog() + let probe = ProxyRuntimeFixture.FakeProbe.reachable() + let controller = makeController(probe: probe) + try await controller.activate(seeded.server) + await controller.setAgentEnabled(true) + try simulateRunningWrapper() + let contractDataBefore = try Data(contentsOf: runtime.contract) + + // 修改激活服务器的备注:显示名是目录元数据,不进契约 → 零收敛 + // (不写盘、不发信号、不闪 starting、不重走健康门)。 + var catalog = try CatalogFileStore(fileURL: catalogFileURL).load().catalog + let fields = try ActivationFixture.serverFields(of: seeded.server, in: catalog) + try catalog.updateServer( + seeded.server, with: replacedFields(fields, remark: "新加坡 01")) + try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog)) + + let outcome = await controller.catalogDidCommit(snapshot: try committedCatalog()) + + XCTAssertEqual(outcome, .converged(skippedServers: [])) + XCTAssertEqual(controller.state, .running, "状态面原地不动,不闪回 starting") + XCTAssertEqual(try Data(contentsOf: runtime.contract), contractDataBefore, "契约逐字节未变") + XCTAssertEqual( + signals.signalsSent.filter { $0.signal != 0 }.count, 0, "不向 wrapper 发信号") + XCTAssertEqual(probe.callCount, 2, "不重走健康门(初始部署探测过 SOCKS+HTTP 各一次)") + } + + func testCatalogValueEditOutsideActiveSubtreeKeepsRuntimeUntouched() async throws { + var catalog = ConfigurationCatalog() + let activeServer = try ActivationFixture.addPlainServer( + "香港 01", in: &catalog, credentials: credentials) + let idleServer = try ActivationFixture.addPlainServer( + "备用 02", in: &catalog, credentials: credentials) + try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog)) + let probe = ProxyRuntimeFixture.FakeProbe.reachable() + let controller = makeController(probe: probe) + try await controller.activate(activeServer) + await controller.setAgentEnabled(true) + try simulateRunningWrapper() + let contractDataBefore = try Data(contentsOf: runtime.contract) + + // 修改目标子树之外服务器的连接地址:派生契约不变 → 零收敛。 + var working = try CatalogFileStore(fileURL: catalogFileURL).load().catalog + let fields = try ActivationFixture.serverFields(of: idleServer, in: working) + try working.updateServer(idleServer, with: replacedFields(fields, address: "198.51.100.9")) + try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: working)) + + let outcome = await controller.catalogDidCommit(snapshot: try committedCatalog()) + + XCTAssertEqual(outcome, .converged(skippedServers: [])) + XCTAssertEqual(controller.state, .running, "状态面原地不动,不闪回 starting") + XCTAssertEqual(try Data(contentsOf: runtime.contract), contractDataBefore, "契约逐字节未变") + XCTAssertEqual(signals.signalsSent.filter { $0.signal != 0 }.count, 0, "不向 wrapper 发信号") + XCTAssertEqual(probe.callCount, 2, "不重走健康门") + } + + func testCatalogGroupMemberValueEditRewritesAndSignalsRunningWrapper() async throws { + var catalog = ConfigurationCatalog() + let group = try catalog.addGroup("分组") + let member = try ActivationFixture.addPlainServer( + "香港 01", to: group, in: &catalog, credentials: credentials) + try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog)) + let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable()) + try await controller.activate(group) + await controller.setAgentEnabled(true) + try simulateRunningWrapper() + + // 激活目标是分组:成员连接值变化是活动子树的真实值变化 → 正常收敛。 + var working = try CatalogFileStore(fileURL: catalogFileURL).load().catalog + let fields = try ActivationFixture.serverFields(of: member, in: working) + try working.updateServer(member, with: replacedFields(fields, address: "203.0.113.9")) + try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: working)) + + _ = await controller.catalogDidCommit(snapshot: try committedCatalog()) + + XCTAssertEqual(controller.state, .running) + let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument()) + XCTAssertEqual(onDisk.servers.first?.server, "203.0.113.9", "成员值变化进入运行时文档") + XCTAssertEqual(signals.signalsSent.filter { $0.signal != 0 }.count, 1, "转发一次 SIGUSR1") + } +} diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift index 7a96559b..2208dde6 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift @@ -11,7 +11,7 @@ final class ProxyRuntimeControllerTests: XCTestCase { var runtime: ProxyRuntimeFixture.TemporaryRuntime! var catalogFileURL: URL! var activationFileURL: URL! - private var credentials: InMemoryCredentialStore! + var credentials: InMemoryCredentialStore! var agent: ProxyRuntimeFixture.FakeLaunchAgent! var systemProxy: ProxyRuntimeFixture.FakeSystemProxy! var signals: SignalRecorder! @@ -267,40 +267,4 @@ final class ProxyRuntimeControllerTests: XCTestCase { XCTAssertFalse(FileManager.default.fileExists(atPath: runtime.pidFile.path)) } - // MARK: 目录重展开消费(D3/D5) - - func testCatalogEditOfActiveTargetRewritesAndSignalsRunningWrapper() async throws { - let seeded = try makeSeededCatalog() - let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable()) - try await controller.activate(seeded.server) - await controller.setAgentEnabled(true) - // 模拟 wrapper 在跑:pid 指向本测试进程(kill(pid, 0) 判活通过)。 - try Data("\(ProcessInfo.processInfo.processIdentifier)".utf8).write(to: runtime.pidFile) - - // 修改服务器备注(文档内容随之变化),提交目录变更。 - var catalog = try CatalogFileStore(fileURL: catalogFileURL).load().catalog - var fields = try ActivationFixture.serverFields(of: seeded.server, in: catalog) - fields = ServerFields( - address: fields.address, - port: fields.port, - encryptionMethod: fields.encryptionMethod, - passwordRef: fields.passwordRef, - remark: "新加坡 01", - pluginProgram: fields.pluginProgram, - pluginOptionsRef: fields.pluginOptionsRef) - try catalog.updateServer(seeded.server, with: fields) - try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog)) - - // 协调器生产适配入口(issue #40):以刚提交的内存快照重展开。 - let committed = try CatalogFileStore(fileURL: catalogFileURL).load().catalog - _ = await controller.catalogDidCommit(snapshot: committed) - - XCTAssertEqual(controller.state, .running) - let expectedRemark = "新加坡 01" - let onDisk = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument()) - XCTAssertEqual(onDisk.servers.first?.remarks, expectedRemark, "运行时文档已原子更新") - let signalsSent = signals.signalsSent.filter { $0.signal != 0 } - XCTAssertEqual(signalsSent.count, 1, "代理运行中:重写后应发一次 SIGUSR1") - XCTAssertEqual(signalsSent.first?.signal, SIGUSR1) - } } diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeFixture.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeFixture.swift index cec112ff..29636507 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeFixture.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeFixture.swift @@ -117,7 +117,6 @@ enum ProxyRuntimeFixture { servers: [ SslocalServerDocument( id: "server-1", - remarks: "香港 01", server: serverAddress, serverPort: 8388, password: password, diff --git a/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift b/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift index ab5823d8..344b545b 100644 --- a/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift +++ b/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift @@ -19,7 +19,6 @@ final class RuntimeDocumentTests: XCTestCase { servers: [ SslocalServerDocument( id: "server-1", - remarks: "香港 01", server: "203.0.113.7", serverPort: 8388, password: "resolved-password", @@ -56,7 +55,8 @@ final class RuntimeDocumentTests: XCTestCase { XCTAssertEqual(servers.count, 1) let server = servers[0] XCTAssertEqual(server["id"] as? String, "server-1", "叶子身份进入文档,供诊断与稳定标识") - XCTAssertEqual(server["remarks"] as? String, "香港 01") + XCTAssertFalse( + server.keys.contains("remarks"), "显示名是目录元数据,不进契约(只承载连接有效值)") XCTAssertEqual(server["server"] as? String, "203.0.113.7") XCTAssertEqual(server["server_port"] as? Int, 8388) XCTAssertEqual(server["password"] as? String, "resolved-password") @@ -92,7 +92,7 @@ final class RuntimeDocumentTests: XCTestCase { let document = SslocalRuntimeDocument( servers: [ SslocalServerDocument( - id: "server-1", remarks: "test", server: "example.com", serverPort: 8388, + id: "server-1", server: "example.com", serverPort: 8388, password: "pw", method: "aes-256-gcm", plugin: nil, pluginOpts: nil) ], listen: SslocalListenSettings()) diff --git a/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift b/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift index 91124a07..84961a07 100644 --- a/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift +++ b/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift @@ -276,7 +276,7 @@ final class RuntimeFileStoreTests: XCTestCase { method: String = "aes-256-gcm" ) -> SslocalServerDocument { SslocalServerDocument( - id: id, remarks: "", server: address, serverPort: port, password: "pw", + id: id, server: address, serverPort: port, password: "pw", method: method, plugin: nil, pluginOpts: nil) } diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift index e9899eee..f8076925 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift @@ -12,7 +12,7 @@ extension RealSslocalSmokeTests { let document = SslocalRuntimeDocument( servers: [], listen: listen, - acl: .direct(at: workDir.appendingPathComponent("sslocal-active.acl"))) + acl: .direct(at: aclFileURL)) XCTAssertTrue(document.isWellFormed) let wrapper = try launchWrapper(document) defer { @@ -60,7 +60,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "global-smoke-server", - remarks: "global-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -69,7 +68,7 @@ extension RealSslocalSmokeTests { pluginOpts: nil) ], listen: listen, - acl: .global(at: workDir.appendingPathComponent("sslocal-active.acl"))) + acl: .global(at: aclFileURL)) XCTAssertTrue(document.isWellFormed) let wrapper = try launchWrapper(document) defer { @@ -108,7 +107,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "rule-smoke-server", - remarks: "rule-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -118,7 +116,7 @@ extension RealSslocalSmokeTests { ], listen: listen, acl: .rule( - at: workDir.appendingPathComponent("sslocal-active.acl"), + at: aclFileURL, defaultAction: .proxyWhenUnmatched, rules: chinaRules)) XCTAssertTrue(document.isWellFormed) @@ -150,7 +148,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "cidr-smoke-server", - remarks: "cidr-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -160,7 +157,7 @@ extension RealSslocalSmokeTests { ], listen: listen, acl: .rule( - at: workDir.appendingPathComponent("sslocal-active.acl"), + at: aclFileURL, defaultAction: .proxyWhenUnmatched, rules: try chinaCIDRSmokeRules())) XCTAssertTrue(document.isWellFormed) diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift index 26f6d8b8..dae2f23e 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift @@ -29,7 +29,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "custom-smoke-server", - remarks: "custom-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -39,7 +38,7 @@ extension RealSslocalSmokeTests { ], listen: listen, acl: .rule( - at: workDir.appendingPathComponent("sslocal-active.acl"), + at: aclFileURL, defaultAction: .proxyWhenUnmatched, rules: customRules)) XCTAssertTrue(document.isWellFormed) @@ -127,7 +126,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "custom-direct-smoke-server", - remarks: "custom-direct-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -137,7 +135,7 @@ extension RealSslocalSmokeTests { ], listen: listen, acl: .rule( - at: workDir.appendingPathComponent("sslocal-active.acl"), + at: aclFileURL, defaultAction: .directWhenUnmatched, rules: customRules)) XCTAssertTrue(document.isWellFormed) diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift index 9f270158..aab17575 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift @@ -26,7 +26,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "gfw-smoke-server", - remarks: "gfw-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -36,7 +35,7 @@ extension RealSslocalSmokeTests { ], listen: listen, acl: .rule( - at: workDir.appendingPathComponent("sslocal-active.acl"), + at: aclFileURL, defaultAction: .directWhenUnmatched, rules: gfwRules)) XCTAssertTrue(document.isWellFormed) @@ -65,7 +64,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "priority-smoke-server", - remarks: "priority-smoke", server: "127.0.0.1", serverPort: fakeSSServer.port, password: "smoke-password", @@ -117,7 +115,7 @@ extension RealSslocalSmokeTests { 8.8.8.0/24 """ let acl = ProxyACLDocument( - path: workDir.appendingPathComponent("sslocal-active.acl").standardizedFileURL.path, + path: aclFileURL.standardizedFileURL.path, summary: "priority-probe", content: aclContent) XCTAssertTrue(acl.isWellFormed) diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+Plugin.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+Plugin.swift index 900926dd..3fbcdcbb 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+Plugin.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+Plugin.swift @@ -27,7 +27,6 @@ extension RealSslocalSmokeTests { servers: [ SslocalServerDocument( id: "smoke-plugin-server", - remarks: "smoke-plugin", server: "127.0.0.1", serverPort: 1, // 远端必然拒绝:只验证本地链路与插件进程边界 password: "smoke-password", diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests.swift index 93059d48..0fcf7a81 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests.swift @@ -24,6 +24,12 @@ final class RealSslocalSmokeTests: XCTestCase { try super.tearDownWithError() } + /// `RuntimeFileStore.write` 校验的 ACL sidecar 路径:ACL 文档的 `path` 必须 + /// 严格等于它,否则契约写入即抛「ACL sidecar path or digest is invalid」。 + var aclFileURL: URL { + RuntimeFileStore(fileURL: contractURL).aclFileURL + } + private var sslocalURL: URL { get throws { let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/sslocal") @@ -144,7 +150,6 @@ final class RealSslocalSmokeTests: XCTestCase { servers: [ SslocalServerDocument( id: "smoke-server", - remarks: "smoke", server: "127.0.0.1", serverPort: 1, // 远端必然拒绝:只验证本地握手与 SSLOCAL 配置格式 password: "smoke-password",