fix(ng2): review fixes for agent/system-proxy switch split

- setAgentEnabled(true) 在意图已开启且未达健康态(off/启动失败/服务
  失败)时执行重收敛,成为失败态的显式重试入口;健康运行中不做无谓
  的注销重拉
- StatusMenuModel.Summary 增加 systemProxyStateLabel 短标签,去掉
  HomeView 对呈现文本的字符串剥离耦合
- 控制器头注释改为如实描述:launchd/契约动作序列在 ProxyRuntimePlan,
  系统代理门禁收敛决策留在控制器(issue #60)
- 新增重试路径测试
This commit is contained in:
Qiu Yuzhou
2026-09-26 00:55:52 +08:00
parent 398e8d84b6
commit 41379bb1cb
4 changed files with 49 additions and 16 deletions
+1 -5
View File
@@ -76,7 +76,7 @@ private struct RuntimeControlCard: View {
Toggle("设置系统代理", isOn: systemProxyBinding)
.toggleStyle(.switch)
Spacer(minLength: 0)
Text(systemProxyShortStatus(summary))
Text(summary.systemProxyStateLabel)
.font(.callout.weight(.medium))
.foregroundStyle(.secondary)
.lineLimit(1)
@@ -109,10 +109,6 @@ private struct RuntimeControlCard: View {
set: { enabled in Task { await control.setSystemProxyEnabled(enabled) } })
}
private func systemProxyShortStatus(_ summary: StatusMenuModel.Summary) -> String {
summary.systemProxyStatus.replacingOccurrences(of: "系统代理:", with: "")
}
private func systemProxyHint(_ summary: StatusMenuModel.Summary) -> String {
switch control.snapshot.systemProxyApplication {
case .idle:
@@ -13,9 +13,10 @@ struct SystemEndpointProbe: EndpointProbing {
}
/// 代理运行时控制器(spec #21 D2/D5/D7/D9,issue #27/#28/#60):把激活状态机
/// 的产出接到「GUI → LaunchAgent → wrapper → sslocal」链路。决策全部在纯域
/// `ProxyRuntimePlan`,本类按序执行动作并负责健康呈现;GUI 退出不影响任何
/// 一侧(agent 由 launchd 持有,构造上成立)。
/// 的产出接到「GUI → LaunchAgent → wrapper → sslocal」链路。launchd/契约动作
/// 序列在纯域 `ProxyRuntimePlan`,本类按序执行、负责健康呈现与系统代理门禁
/// 收敛(issue #60:意图持久化先行,门禁与待应用策略是应用层决策);GUI 退出
/// 不影响任何一侧(agent 由 launchd 持有,构造上成立)。
///
/// 两个用户意图相互独立(issue #60):agent 意图(`settings.agentEnabled`,
/// 默认开启)驱动 LaunchAgent 注册与本地监听;系统代理意图
@@ -230,9 +231,20 @@ final class ProxyRuntimeController: ObservableObject {
}
/// Agent 开关(issue #60):先持久化意图(显式关闭在 GUI 重启后仍生效),
/// 再收敛运行时。持久化失败保留现状并点名,不静默偏离持久化事实。
/// 再收敛运行时。持久化失败保留现状并点名,不静默偏离持久化事实。意图已
/// 开启时的开启命令仅在未达健康态时重收敛——它是启动失败/重置后的显式
/// 重试入口;健康运行中不做无谓的注销重拉。
func setAgentEnabled(_ enabled: Bool) async {
guard enabled != settings.agentEnabled else { return }
if enabled == settings.agentEnabled {
guard enabled else { return }
switch state {
case .off, .launchFailed, .serviceFailed:
await convergeAgent()
case .starting, .running, .firewallBlocked, .requiresApproval:
break
}
return
}
var next = settings
next.agentEnabled = enabled
do {
+9 -6
View File
@@ -20,6 +20,8 @@ enum StatusMenuModel {
let systemProxyIntentEnabled: Bool
/// 系统代理实际应用状态文本(与 agent 运行状态分开呈现)。
let systemProxyStatus: String
/// 同上但不带「系统代理:」前缀(首页等已有上下文的呈现面用)。
let systemProxyStateLabel: String
/// 系统代理写入/恢复失败的点名原因;非失败态为 nil。
let systemProxyDetail: String?
let modeLabel: String
@@ -50,20 +52,20 @@ enum StatusMenuModel {
let agentDetail =
snapshot.runtime.failure.map { AppPresentation.message(for: $0) }
?? snapshot.activationFailure.map { AppPresentation.message(for: $0) }
let systemProxyStatus: String
let stateLabel: String
let systemProxyDetail: String?
switch snapshot.systemProxyApplication {
case .idle:
systemProxyStatus = "系统代理:未接管"
stateLabel = "未接管"
systemProxyDetail = nil
case .pending:
systemProxyStatus = "系统代理:待应用"
stateLabel = "待应用"
systemProxyDetail = nil
case .applied:
systemProxyStatus = "系统代理:已应用"
stateLabel = "已应用"
systemProxyDetail = nil
case .failed(let facts):
systemProxyStatus = "系统代理:应用失败"
stateLabel = "应用失败"
systemProxyDetail = AppPresentation.message(for: RuntimeFailureFacts.systemProxy(facts))
}
return Summary(
@@ -72,7 +74,8 @@ enum StatusMenuModel {
status: status,
detail: agentDetail,
systemProxyIntentEnabled: snapshot.systemProxyIntentEnabled,
systemProxyStatus: systemProxyStatus,
systemProxyStatus: "系统代理:\(stateLabel)",
systemProxyStateLabel: stateLabel,
systemProxyDetail: systemProxyDetail,
modeLabel: snapshot.proxyMode.label,
targetPath: snapshot.activeTarget?.pathSummary)
@@ -692,3 +692,25 @@ extension ProxyRuntimeControllerTests {
XCTAssertTrue(systemProxy.applied.isEmpty, "系统代理意图关闭,不写系统设置")
}
}
extension ProxyRuntimeControllerTests {
/// 意图已开启时的开启命令是失败态的显式重试入口(issue #60):无需先关再开。
func testAgentEnableCommandRetriesConvergenceWhileIntentAlreadyOn() async throws {
let seeded = try makeSeededCatalog()
let probe = ProxyRuntimeFixture.FakeProbe.refusing()
let controller = makeController(probe: probe)
// 激活即按默认意图部署:端点不可达 → 启动失败(走满 15 秒健康窗)。
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)
}
}