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 <noreply@deepseek.com>
This commit is contained in:
Qiu Yuzhou
2026-10-21 02:44:35 +08:00
co-authored by DeepSeek Harness
parent 5872015ca0
commit 7977daafea
6 changed files with 36 additions and 57 deletions
@@ -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
}
}
@@ -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
}
}
@@ -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
@@ -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)
}
/// 校验拒绝:固定本地冲突整批不落地,旧规则保持不变。
@@ -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
}
}
+1 -1
View File
@@ -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)。