fix(runtime): converge proxy runtime only when contract bytes change

Every catalog commit re-expanded the active target and ran the full
deploy cycle unconditionally: the status face flashed back to starting,
the launch health gate re-probed both local inbounds, and system proxy
converged again — even when the derived contract was byte-identical to
what was already deployed. Remark edits went further: remarks were part
of SslocalServerDocument, so renaming the active server rewrote the
contract and signalled sslocal for a change with no runtime effect.

The reload predicate is now contract byte equality, shared by both
layers (CONTEXT.md: a committed edit that leaves the runtime
configuration file's bytes unchanged triggers no runtime convergence
work). remarks leave the contract — upstream treats them as optional
metadata and the GUI resolves display names from the catalog — so
remark-only edits and no-op re-saves derive byte-identical documents.
The catalogDidCommit deploy path skips write, signal, health gate and
system proxy converge when ProxyRuntimePlan.actions returns empty
(registered agent, live wrapper, on-disk contract equals the derived
document); disk drift or a dead wrapper keeps the self-healing deploy.
Catalog revalidation (candidate skips, target invalidation) still runs
on every commit, and explicit intents (activation, switches, settings,
launch resync) are untouched.

The remark-edit runtime test — which encoded the old behavior as the
example of a value change — is replaced by four catalog-convergence
cases: remark-only edit and outside-subtree edit stay untouched, group
member and active server value edits rewrite and signal.

Also repairs the smoke suite, which failed on HEAD before this change:
its ACL documents used a sslocal-active.acl path while
RuntimeFileStore.write validates against acl-active.ini, so every
ACL-bearing case threw before spawning the wrapper. Smoke ACL paths now
bind to the store-validated location, and the suite passes 11/11 against
real sslocal, confirming upstream accepts the contract without remarks.
This commit is contained in:
Qiu Yuzhou
2026-09-29 11:57:37 +08:00
parent 53c84384a4
commit b8e5919bcd
15 changed files with 196 additions and 65 deletions
+2 -1
View File
@@ -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.
@@ -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(
@@ -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,
@@ -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"
@@ -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")
@@ -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")
}
}
@@ -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)
}
}
@@ -117,7 +117,6 @@ enum ProxyRuntimeFixture {
servers: [
SslocalServerDocument(
id: "server-1",
remarks: "香港 01",
server: serverAddress,
serverPort: 8388,
password: password,
@@ -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())
@@ -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)
}
@@ -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)
@@ -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)
@@ -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)
@@ -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",
@@ -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",