From 7977daafea4abfece7348db198efd9274c68b4c0 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Wed, 21 Oct 2026 02:44:35 +0800 Subject: [PATCH] refactor: keep test doubles and test-only wrappers out of production Move InMemoryDiagnosticReportExporter and InMemoryConfigurationGroupFileExporter from App/PlatformEffects to Tests/Support: both are test doubles with no production caller, and production composition wires only the AppKit adapters. Delete RuleRuntimeCompiler.validation, a wrapper whose only callers were two assertion blocks in the controller deployment test; those restated custom-to-custom shadowing and validation order-independence, both pinned by CustomRuleValidatorTests and RuleAnalysisTests. RuleRuntimeCompiler.compile stays and keeps its production caller. Co-Authored-By: DeepSeek Harness --- .../ConfigurationGroupFileExporter.swift | 16 --------- .../DiagnosticReportExporter.swift | 16 --------- .../Rules/Custom/CustomRuleValidator.swift | 12 ------- .../Custom/CustomRuleControllerTests.swift | 12 ------- .../Tests/Support/InMemoryExporters.swift | 35 +++++++++++++++++++ docs/project-structure.md | 2 +- 6 files changed, 36 insertions(+), 57 deletions(-) create mode 100644 ShadowsocksX-NG2/Tests/Support/InMemoryExporters.swift diff --git a/ShadowsocksX-NG2/App/PlatformEffects/ConfigurationGroupFileExporter.swift b/ShadowsocksX-NG2/App/PlatformEffects/ConfigurationGroupFileExporter.swift index 8ab653b6..ee00faa4 100644 --- a/ShadowsocksX-NG2/App/PlatformEffects/ConfigurationGroupFileExporter.swift +++ b/ShadowsocksX-NG2/App/PlatformEffects/ConfigurationGroupFileExporter.swift @@ -72,19 +72,3 @@ final class AppKitConfigurationGroupFileExporter: ConfigurationGroupFileExporter } } } - -/// Tests and previews receive the complete draft without opening a panel or writing a file. -@MainActor -final class InMemoryConfigurationGroupFileExporter: ConfigurationGroupFileExporter { - var result: ConfigurationGroupFileExportResult - private(set) var drafts: [ConfigurationGroupExportDraft] = [] - - init(result: ConfigurationGroupFileExportResult = .cancelled) { - self.result = result - } - - func export(draft: ConfigurationGroupExportDraft) -> ConfigurationGroupFileExportResult { - drafts.append(draft) - return result - } -} diff --git a/ShadowsocksX-NG2/App/PlatformEffects/DiagnosticReportExporter.swift b/ShadowsocksX-NG2/App/PlatformEffects/DiagnosticReportExporter.swift index 3064072a..a02e5ce0 100644 --- a/ShadowsocksX-NG2/App/PlatformEffects/DiagnosticReportExporter.swift +++ b/ShadowsocksX-NG2/App/PlatformEffects/DiagnosticReportExporter.swift @@ -47,19 +47,3 @@ struct DiagnosticReportExportAction { } } } - -/// 测试与 Preview 使用的内存实现:记录收到的 draft,但不打开保存面板或写文件。 -@MainActor -final class InMemoryDiagnosticReportExporter: DiagnosticReportExporter { - var result: DiagnosticReportExportResult - private(set) var drafts: [DiagnosticReportDraft] = [] - - init(result: DiagnosticReportExportResult = .cancelled) { - self.result = result - } - - func export(draft: DiagnosticReportDraft) -> DiagnosticReportExportResult { - drafts.append(draft) - return result - } -} diff --git a/ShadowsocksX-NG2/Domain/Rules/Custom/CustomRuleValidator.swift b/ShadowsocksX-NG2/Domain/Rules/Custom/CustomRuleValidator.swift index 64051521..82a909f6 100644 --- a/ShadowsocksX-NG2/Domain/Rules/Custom/CustomRuleValidator.swift +++ b/ShadowsocksX-NG2/Domain/Rules/Custom/CustomRuleValidator.swift @@ -346,18 +346,6 @@ enum RuleRuntimeCompiler { let builtIn: [ProxyRule] let aclURL: URL } - static func validation( - document: CustomRuleDocument, builtIn: [ProxyRule], defaultAction: RuleDefaultAction - ) -> CustomRuleValidationResult { - let custom = document.rules.filter { !document.disabledIdentities.contains($0.identity) } - let enabled = builtIn.filter { !document.disabledIdentities.contains($0) } - let validated = CustomRuleValidator.validate( - custom: custom, builtIn: enabled, defaultAction: defaultAction) - return CustomRuleValidationResult( - accepted: RuleAnalysis.runtimeCandidates( - enabled + validated.accepted, defaultAction: defaultAction), - rejected: validated.rejected, relationships: validated.relationships) - } static func compile(_ input: Input) -> SslocalRuntimeDocument { let source = input.source diff --git a/ShadowsocksX-NG2/Tests/Rules/Custom/CustomRuleControllerTests.swift b/ShadowsocksX-NG2/Tests/Rules/Custom/CustomRuleControllerTests.swift index f1a43f06..57753167 100644 --- a/ShadowsocksX-NG2/Tests/Rules/Custom/CustomRuleControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/Rules/Custom/CustomRuleControllerTests.swift @@ -102,14 +102,6 @@ final class CustomRuleControllerTests: ProxyRuntimeControllerTests { XCTAssertEqual(first, .saved) await controller.ruleApplicationTask?.value XCTAssertEqual(try store.load(), rules, "保存保留用户 UUID 和全部意图") - // 遮蔽关系由领域层验证器判定(与运行时部署无关的纯函数)。 - let validation = RuleRuntimeCompiler.validation( - document: CustomRuleDocument(rules: rules, disabledIdentities: []), - builtIn: [], defaultAction: .directWhenUnmatched) - XCTAssertTrue( - validation.relationships.contains { - $0.rule == rules[2].identity && $0.kind == .shadowing && $0.extent == .full - }) let runtimeStore = RuntimeFileStore(fileURL: runtime.contract) let before = try activeACLContent(runtimeStore) let unregisterBefore = agent.unregisterCount @@ -123,10 +115,6 @@ final class CustomRuleControllerTests: ProxyRuntimeControllerTests { XCTAssertEqual(try activeACLContent(runtimeStore), before) XCTAssertEqual(agent.unregisterCount, unregisterBefore) XCTAssertEqual(controller.readCustomRuleSummary(), summary) - let repeatedValidation = RuleRuntimeCompiler.validation( - document: CustomRuleDocument(rules: reversed, disabledIdentities: []), - builtIn: [], defaultAction: .directWhenUnmatched) - XCTAssertEqual(repeatedValidation, validation) } /// 校验拒绝:固定本地冲突整批不落地,旧规则保持不变。 diff --git a/ShadowsocksX-NG2/Tests/Support/InMemoryExporters.swift b/ShadowsocksX-NG2/Tests/Support/InMemoryExporters.swift new file mode 100644 index 00000000..d04a923d --- /dev/null +++ b/ShadowsocksX-NG2/Tests/Support/InMemoryExporters.swift @@ -0,0 +1,35 @@ +import Foundation + +@testable import ShadowsocksX_NG2 + +/// 导出缝的测试替身:记录收到的 draft,但不打开保存面板、不写文件。 +@MainActor +final class InMemoryDiagnosticReportExporter: DiagnosticReportExporter { + var result: DiagnosticReportExportResult + private(set) var drafts: [DiagnosticReportDraft] = [] + + init(result: DiagnosticReportExportResult = .cancelled) { + self.result = result + } + + func export(draft: DiagnosticReportDraft) -> DiagnosticReportExportResult { + drafts.append(draft) + return result + } +} + +/// 配置组文件导出缝的测试替身:同上,只记录 draft 并返回预设结果。 +@MainActor +final class InMemoryConfigurationGroupFileExporter: ConfigurationGroupFileExporter { + var result: ConfigurationGroupFileExportResult + private(set) var drafts: [ConfigurationGroupExportDraft] = [] + + init(result: ConfigurationGroupFileExportResult = .cancelled) { + self.result = result + } + + func export(draft: ConfigurationGroupExportDraft) -> ConfigurationGroupFileExportResult { + drafts.append(draft) + return result + } +} diff --git a/docs/project-structure.md b/docs/project-structure.md index fc207118..0a269aed 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -12,7 +12,7 @@ - `Application/` — 应用层(无 SwiftUI 视图):`*Workflow/` 工作流 module(UI-facing interface 与实现协作者)、`ProxyRuntime/` 运行时控制器家族与事实投影、`Services/` LaunchAgent/登录项/helper/防火墙。 - `Presentation/` — 跨功能表现层:错误呈现、被多个界面共用的状态模型。 - `UI/` — 视图层,按子视图分目录(`Workspace/`、`Home/`、`Servers/`、`Subscriptions/`、`Rules/`、`Settings/`、`Diagnostics/`、`StatusMenu/`,`Servers/Plugin/` 为插件管理子视图)。 - - `PlatformEffects/` — 平台效应 seam(协议 + InMemory 替身 + AppKit adapter),系统框架访问集中于此。 + - `PlatformEffects/` — 平台效应 seam(协议 + AppKit adapter;测试替身在 `Tests/Support/`),系统框架访问集中于此。 - `Resources/` — `Info.plist`、`entitlements`(生成物)与 `Assets.xcassets`、`*.xcstrings`(入库)。 分层边界由 `Tests/Architecture/SkeletonTests.swift` 的正向圈定守卫断言:只扫描 `App/UI/`,视图层不得引用运行时控制器、工作流实现协作者或原始目录/凭据存储类型;新增运行时适配器放进 `Application/` 即自动落在扫描范围外,无需登记豁免。注意 `App/` 下任何非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle,因此文档不放 `App/`(`project.yml` 已排除 `**/*.md`)。分层与主题的组织方式、守卫为何取正面圈定、以及 XcodeGen 的上述约束见 [ADR-0029](adr/0029-source-layout-declares-layer-and-theme.md)。