refactor(ng2): unify committed catalog source

This commit is contained in:
Qiu Yuzhou
2026-09-23 16:47:13 +08:00
parent 6c98930999
commit e65d7112f1
10 changed files with 256 additions and 38 deletions
@@ -8,6 +8,45 @@ struct CommittedCatalogSnapshot: Equatable, Sendable {
let subscriptions: [SubscriptionRecord]
}
/// Runtime commands read the latest committed catalog through this narrow seam.
/// The catalog remains a value snapshot; a command captures it once and uses it
/// for its full re-expansion operation.
@MainActor
protocol RuntimeCatalogSnapshotReading: AnyObject {
var catalogSnapshot: ConfigurationCatalog { get }
}
/// Shared in-process catalog value. Only the commit coordinator can publish
/// after this file's bootstrap; runtime code receives the read-only protocol.
@MainActor
private final class CommittedCatalogState: RuntimeCatalogSnapshotReading {
private(set) var catalogSnapshot: ConfigurationCatalog
init(catalog: ConfigurationCatalog) {
catalogSnapshot = catalog
}
fileprivate func publish(_ catalog: ConfigurationCatalog) {
catalogSnapshot = catalog
}
}
/// One-time startup load shared by the controller and coordinator. The reader
/// exposes only catalog data; subscription state remains coordinator-owned.
@MainActor
struct CatalogCommitBootstrap {
let catalogSnapshotReader: RuntimeCatalogSnapshotReading
fileprivate let committedCatalogState: CommittedCatalogState
fileprivate let subscriptions: [SubscriptionRecord]
fileprivate init(document: CatalogDocument) {
let state = CommittedCatalogState(catalog: document.catalog)
catalogSnapshotReader = state
committedCatalogState = state
subscriptions = document.subscriptions
}
}
/// 目录提交后一次运行时收敛的结构化结果(issue #40)。协调器不生成本地化
/// 文案:失败细节原样携带平台运行时给出的点名事实,由上层 presentation 决定
/// 如何呈现。
@@ -56,17 +95,37 @@ final class CatalogCommitCoordinator: ObservableObject {
let fileStore: CatalogFileStore
private let runtime: CatalogRuntimeSyncing
private let catalogState: CommittedCatalogState
/// 提交代次:每次提交递增,用于丢弃被更新提交取代的旧同步结果。
private var generation = 0
/// 已提交状态(协调器持有的内存快照;`commit` 成功后整体替换)。
private(set) var committedCatalog: ConfigurationCatalog
/// 目录在共享来源中只有一份;订阅状态仍由协调器持有。
var committedCatalog: ConfigurationCatalog { catalogState.catalogSnapshot }
private(set) var committedSubscriptions: [SubscriptionRecord]
init(fileStore: CatalogFileStore, runtime: CatalogRuntimeSyncing) {
init(
fileStore: CatalogFileStore,
runtime: CatalogRuntimeSyncing,
bootstrap: CatalogCommitBootstrap
) {
self.fileStore = fileStore
self.runtime = runtime
(committedCatalog, committedSubscriptions) = Self.loadCommitted(from: fileStore)
catalogState = bootstrap.committedCatalogState
committedSubscriptions = bootstrap.subscriptions
}
/// Test and isolated-module convenience: still performs exactly one document
/// load, then gives the coordinator sole publication authority.
convenience init(fileStore: CatalogFileStore, runtime: CatalogRuntimeSyncing) {
self.init(
fileStore: fileStore, runtime: runtime, bootstrap: Self.bootstrap(fileStore: fileStore))
}
/// Loads the initial document once so the composition root can share its
/// catalog with the runtime controller before constructing the adapter.
/// Direct out-of-band file edits are not observed during a GUI session.
static func bootstrap(fileStore: CatalogFileStore) -> CatalogCommitBootstrap {
CatalogCommitBootstrap(document: loadCommitted(from: fileStore))
}
/// 普通目录变更的唯一提交管线。任一同步步骤失败则已提交状态不动并原样
@@ -79,7 +138,7 @@ final class CatalogCommitCoordinator: ObservableObject {
let result = try mutate(&workingCatalog, &workingSubscriptions)
try fileStore.save(
CatalogDocument(catalog: workingCatalog, subscriptions: workingSubscriptions))
committedCatalog = workingCatalog
catalogState.publish(workingCatalog)
committedSubscriptions = workingSubscriptions
scheduleRuntimeSync(
CommittedCatalogSnapshot(catalog: workingCatalog, subscriptions: workingSubscriptions))
@@ -89,7 +148,9 @@ final class CatalogCommitCoordinator: ObservableObject {
/// Legacy 导入等独立路径直接落盘后对齐已提交状态;不触发运行时收敛
/// (导入的运行时边界由独立路径自行驱动,不经普通提交管线)。
func reloadCommittedStateFromStore() {
(committedCatalog, committedSubscriptions) = Self.loadCommitted(from: fileStore)
let document = Self.loadCommitted(from: fileStore)
catalogState.publish(document.catalog)
committedSubscriptions = document.subscriptions
}
/// 异步收敛调度:无活动目标不调用运行时;同步开始前与结束后都以代次
@@ -114,9 +175,8 @@ final class CatalogCommitCoordinator: ObservableObject {
/// 用户经由导入/设置路径修复。
private static func loadCommitted(
from fileStore: CatalogFileStore
) -> (catalog: ConfigurationCatalog, subscriptions: [SubscriptionRecord]) {
let loaded = (try? fileStore.load()) ?? CatalogDocument()
return (loaded.catalog, loaded.subscriptions)
) -> CatalogDocument {
(try? fileStore.load()) ?? CatalogDocument()
}
}
+8 -2
View File
@@ -11,8 +11,13 @@ struct ShadowsocksXNG2App: App {
init() {
let dependencies = ApplicationDependencies.make()
// The catalog document is read exactly once at startup. The coordinator
// publishes later committed snapshots; the controller receives read-only
// access to this same in-process source.
let catalogBootstrap = CatalogCommitCoordinator.bootstrap(
fileStore: dependencies.catalogFileStore)
let controller = ProxyRuntimeController(
catalogFileStore: dependencies.catalogFileStore,
catalogSnapshotReader: catalogBootstrap.catalogSnapshotReader,
activationFileStore: dependencies.activationFileStore,
runtimeFileStore: dependencies.runtimeFileStore,
credentials: dependencies.credentials,
@@ -27,7 +32,8 @@ struct ShadowsocksXNG2App: App {
// 各 scene 不再重复设置提交回调;Legacy 导入后的 2.0 运行时边界同为一次性注入。
let coordinator = CatalogCommitCoordinator(
fileStore: dependencies.catalogFileStore,
runtime: ProxyRuntimeSyncAdapter(controller: controller))
runtime: ProxyRuntimeSyncAdapter(controller: controller),
bootstrap: catalogBootstrap)
let catalogWorkflow = CatalogWorkflow(
coordinator: coordinator,
credentials: dependencies.credentials,
@@ -47,8 +47,7 @@ final class ProxyRuntimeController: ObservableObject {
@Published private(set) var skippedServers: [SkippedServer] = []
private(set) var machine: ActivationStateMachine
private var catalog: ConfigurationCatalog
private let catalogFileStore: CatalogFileStore
private let catalogSnapshotReader: RuntimeCatalogSnapshotReading
private let activationFileStore: ActivationStateFileStore
private let runtimeFileStore: RuntimeFileStore
private let credentials: CredentialStoring
@@ -76,8 +75,7 @@ final class ProxyRuntimeController: ObservableObject {
@Published private(set) var proxyMode: ProxyMode
init(
catalogFileStore: CatalogFileStore = CatalogFileStore(
fileURL: CatalogFileStore.defaultFileURL()),
catalogSnapshotReader: RuntimeCatalogSnapshotReading,
activationFileStore: ActivationStateFileStore = ActivationStateFileStore(
fileURL: ActivationStateFileStore.defaultFileURL()),
runtimeFileStore: RuntimeFileStore = RuntimeFileStore(),
@@ -96,7 +94,7 @@ final class ProxyRuntimeController: ObservableObject {
firewallPollIntervalNanoseconds: UInt64 = 2_000_000_000,
sendSignal: @escaping @Sendable (Int32, Int32) -> Int32 = { kill($0, $1) }
) {
self.catalogFileStore = catalogFileStore
self.catalogSnapshotReader = catalogSnapshotReader
self.activationFileStore = activationFileStore
self.runtimeFileStore = runtimeFileStore
self.credentials = credentials
@@ -121,7 +119,6 @@ final class ProxyRuntimeController: ObservableObject {
self.firewallExecutableURLs = firewallExecutableURLs ?? Self.defaultFirewallExecutableURLs
self.firewallPollIntervalNanoseconds = firewallPollIntervalNanoseconds
self.sendSignal = sendSignal
catalog = (try? catalogFileStore.load().catalog) ?? ConfigurationCatalog()
let persistedTarget = try? activationFileStore.loadActiveTargetID()
machine = ActivationStateMachine(activeTargetID: persistedTarget)
activeTargetID = machine.activeTargetID
@@ -161,7 +158,7 @@ final class ProxyRuntimeController: ObservableObject {
/// `.rejectedActivation`;意外错误 throws 并进入 `serviceFailed`。
@discardableResult
func activate(_ target: NodeID) async throws -> ActivationCommandOutcome {
reloadCatalog()
let catalog = catalogSnapshotReader.catalogSnapshot
do {
let configuration = try machine.activate(
target, in: catalog, credentials: credentials, plugins: plugins, listen: settings.listen,
@@ -255,8 +252,8 @@ final class ProxyRuntimeController: ObservableObject {
/// 事实来源——注册过即视为开启并重校验;随后与磁盘契约对齐(相同内容跳
/// 过写入)。GUI 崩溃期间 agent 与 wrapper 均不受影响。
func resyncOnLaunch() async {
reloadCatalog()
switch reexpand() {
let catalog = catalogSnapshotReader.catalogSnapshot
switch reexpand(in: catalog) {
case .deployed(let configuration):
let status = agent.status
if status == .registered || status == .requiresApproval {
@@ -293,8 +290,8 @@ final class ProxyRuntimeController: ObservableObject {
presentNoActiveTarget()
return
}
reloadCatalog()
switch reexpand() {
let catalog = catalogSnapshotReader.catalogSnapshot
switch reexpand(in: catalog) {
case .deployed(let configuration):
await deploy(configuration.document)
case .clearedAndStopped(let failure):
@@ -454,8 +451,7 @@ extension ProxyRuntimeController {
/// → 原子更新运行时;目标失效 → 清除目标并停止代理。返回结构化收敛结果,
/// 健康检查耗时属于本调用的异步收敛阶段,不改变「目录已提交」的事实。
func catalogDidCommit(snapshot catalog: ConfigurationCatalog) async -> RuntimeSyncOutcome {
self.catalog = catalog
switch reexpand() {
switch reexpand(in: catalog) {
case .deployed(let configuration):
guard state != .off else {
return .revalidated
@@ -497,6 +493,7 @@ extension ProxyRuntimeController {
/// before persistence, so the mode kind and its URL commit as one logical
/// change and a validation failure precedes the write.
func updateSettings(_ proposed: ProxySettings) async throws {
let catalog = catalogSnapshotReader.catalogSnapshot
var next = proposed
var resolvedMode = proxyMode
if case .externalPAC = proxyMode {
@@ -515,7 +512,7 @@ extension ProxyRuntimeController {
settingsUnreadable = false
guard state != .off else { return }
switch reexpand() {
switch reexpand(in: catalog) {
case .deployed(let configuration):
await deploy(configuration.document)
case .clearedAndStopped(let failure):
@@ -557,7 +554,6 @@ extension ProxyRuntimeController {
listenSettingsUnreadable = false
proxyMode = Self.makeProxyMode(from: restored)
}
reloadCatalog()
let persistedTarget = try? activationFileStore.loadActiveTargetID()
machine = ActivationStateMachine(activeTargetID: persistedTarget ?? nil)
activeTargetID = machine.activeTargetID
@@ -743,12 +739,9 @@ extension ProxyRuntimeController {
// MARK: - 目录同步
private func reloadCatalog() {
catalog = (try? catalogFileStore.load().catalog) ?? catalog
}
/// 目录以磁盘为事实来源重载后重展开;目标被清除时一并发布。
private func reexpand() -> ActivationEffect? {
/// Re-expands one command-start snapshot; a concurrently published catalog
/// is intentionally observed by the next command, not halfway through this one.
private func reexpand(in catalog: ConfigurationCatalog) -> ActivationEffect? {
let effect = machine.catalogDidCommit(
catalog, credentials: credentials, plugins: plugins, listen: settings.listen,
timeout: settings.timeoutSeconds, verbose: settings.verboseLogging,
@@ -69,6 +69,7 @@ final class CatalogCommitCoordinatorTests: XCTestCase {
private var fileURL: URL!
private var runtime: FakeCatalogRuntime!
private var coordinator: CatalogCommitCoordinator!
private var bootstrap: CatalogCommitBootstrap!
override func setUp() async throws {
try await super.setUp()
@@ -87,7 +88,9 @@ final class CatalogCommitCoordinatorTests: XCTestCase {
}
private func makeCoordinator() -> CatalogCommitCoordinator {
CatalogCommitCoordinator(fileStore: CatalogFileStore(fileURL: fileURL), runtime: runtime)
let fileStore = CatalogFileStore(fileURL: fileURL)
bootstrap = CatalogCommitCoordinator.bootstrap(fileStore: fileStore)
return CatalogCommitCoordinator(fileStore: fileStore, runtime: runtime, bootstrap: bootstrap)
}
/// 预置目录后重建协调器(模拟已落盘的既有状态)。
@@ -112,6 +115,9 @@ final class CatalogCommitCoordinatorTests: XCTestCase {
}
XCTAssertTrue(coordinator.committedCatalog.contains(added), "提交后已发布新状态")
XCTAssertTrue(
bootstrap.catalogSnapshotReader.catalogSnapshot.contains(added),
"controller reader 与 coordinator 观察同一已提交目录")
XCTAssertEqual(try persistedCatalog().rootChildren, [added], "变更已原子持久化")
XCTAssertEqual(runtime.convergeCount, 0, "无活动目标不驱动运行时")
XCTAssertEqual(coordinator.syncStatus, .idle)
@@ -281,7 +287,29 @@ final class CatalogCommitCoordinatorTests: XCTestCase {
coordinator.reloadCommittedStateFromStore()
XCTAssertEqual(coordinator.committedCatalog.rootChildren, [groupID], "已提交状态对齐磁盘")
XCTAssertEqual(
bootstrap.catalogSnapshotReader.catalogSnapshot.rootChildren, [groupID],
"独立导入 reload 发布到共享目录来源")
XCTAssertEqual(coordinator.committedSubscriptions.count, 1)
XCTAssertEqual(runtime.convergeCount, 0, "对齐不触发运行时收敛")
}
func testFailedPersistenceDoesNotPublishSharedCatalog() throws {
let blocker = workDir.appendingPathComponent("not-a-directory")
try Data().write(to: blocker)
let blockedStore = CatalogFileStore(fileURL: blocker.appendingPathComponent("catalog.json"))
let blockedBootstrap = CatalogCommitCoordinator.bootstrap(fileStore: blockedStore)
let blockedCoordinator = CatalogCommitCoordinator(
fileStore: blockedStore, runtime: runtime, bootstrap: blockedBootstrap)
let original = blockedBootstrap.catalogSnapshotReader.catalogSnapshot
XCTAssertThrowsError(
try blockedCoordinator.commit { catalog, _ in
try catalog.addTestServer("不能发布")
})
XCTAssertEqual(blockedBootstrap.catalogSnapshotReader.catalogSnapshot, original)
XCTAssertEqual(blockedCoordinator.committedCatalog, original)
XCTAssertEqual(runtime.convergeCount, 0, "持久化失败不驱动运行时")
}
}
@@ -13,6 +13,8 @@ final class CatalogWorkflowRollbackTests: XCTestCase {
private var activationURL: URL!
private var credentials: InMemoryCredentialStore!
private var runtime: FakeCatalogRuntime!
private var bootstrap: CatalogCommitBootstrap!
private var rootsSeenByLegacyPostImport: [NodeID]?
override func setUp() async throws {
try await super.setUp()
@@ -46,8 +48,10 @@ final class CatalogWorkflowRollbackTests: XCTestCase {
postLegacyImport: ((LegacyImportOutcome) async -> Void)? = nil
) -> CatalogWorkflow {
runtime.hasActiveTarget = true
let fileStore = CatalogFileStore(fileURL: fileURL)
bootstrap = CatalogCommitCoordinator.bootstrap(fileStore: fileStore)
let coordinator = CatalogCommitCoordinator(
fileStore: CatalogFileStore(fileURL: fileURL), runtime: runtime)
fileStore: fileStore, runtime: runtime, bootstrap: bootstrap)
return CatalogWorkflow(
coordinator: coordinator,
credentials: credentials,
@@ -221,7 +225,13 @@ final class CatalogWorkflowRollbackTests: XCTestCase {
let service = try makeLegacyService()
var postCalls = 0
let workflow = makeWorkflow(legacyImportService: service) { _ in postCalls += 1 }
rootsSeenByLegacyPostImport = nil
let workflow = makeWorkflow(legacyImportService: service) { [weak self] _ in
postCalls += 1
self?.rootsSeenByLegacyPostImport =
self?.bootstrap.catalogSnapshotReader
.catalogSnapshot.rootChildren
}
XCTAssertTrue(workflow.legacyImportState.shouldOffer, "首启发现快照后应提供自动导入入口")
// 有活动目标也不得触发运行时收敛(导入不启动代理、不写系统代理)。
XCTAssertTrue(runtime.hasActiveTarget)
@@ -234,6 +244,9 @@ final class CatalogWorkflowRollbackTests: XCTestCase {
XCTAssertTrue(group.isGroup)
XCTAssertTrue(group.isManual)
XCTAssertEqual(group.childCount, 1)
XCTAssertEqual(
rootsSeenByLegacyPostImport, [group.id],
"独立导入先 reload 并发布共享目录快照,再进入运行时清理回调")
XCTAssertEqual(runtime.convergeCount, 0, "导入不经提交管线触发运行时收敛")
XCTAssertEqual(postCalls, 1, "导入后的运行时边界只经注入闭包(独立于普通提交)")
XCTAssertEqual(workflow.legacyImportState.completed, true, "成功导入写完成标记")
@@ -105,4 +105,110 @@ extension ProxyRuntimeControllerTests {
XCTAssertTrue(systemProxy.applied.isEmpty)
XCTAssertEqual(agent.registerCount, 0)
}
}
/// Coordinator/controller integration for the shared committed catalog source.
@MainActor
final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase {
private var runtime: ProxyRuntimeFixture.TemporaryRuntime!
private var catalogFileURL: URL!
private var activationFileURL: URL!
private var credentials: InMemoryCredentialStore!
private var agent: ProxyRuntimeFixture.FakeLaunchAgent!
private var systemProxy: ProxyRuntimeFixture.FakeSystemProxy!
override func setUpWithError() throws {
try super.setUpWithError()
runtime = ProxyRuntimeFixture.makeTemporaryRuntime()
catalogFileURL = runtime.directory.appendingPathComponent("catalog.json")
activationFileURL = runtime.directory.appendingPathComponent("activation.json")
credentials = InMemoryCredentialStore()
agent = ProxyRuntimeFixture.FakeLaunchAgent()
systemProxy = ProxyRuntimeFixture.FakeSystemProxy()
}
override func tearDownWithError() throws {
try? FileManager.default.removeItem(at: runtime.directory)
try super.tearDownWithError()
}
func testCommitWithoutActiveTargetFeedsLaterActivationThroughSharedSnapshot() async throws {
let fileStore = CatalogFileStore(fileURL: catalogFileURL)
let bootstrap = CatalogCommitCoordinator.bootstrap(fileStore: fileStore)
let controller = makeController(catalogSnapshotReader: bootstrap.catalogSnapshotReader)
let coordinator = CatalogCommitCoordinator(
fileStore: fileStore,
runtime: ProxyRuntimeSyncAdapter(controller: controller),
bootstrap: bootstrap)
let server = try coordinator.commit { catalog, _ in
try ActivationFixture.addPlainServer(
"提交后激活", in: &catalog, credentials: credentials)
}
XCTAssertEqual(coordinator.syncStatus, .idle, "无目标时目录只发布,不触发运行时")
XCTAssertNil(controller.activeTargetID)
try await controller.activate(server)
XCTAssertEqual(controller.activeTargetID, server, "激活读取协调器刚发布的唯一快照")
XCTAssertEqual(controller.state, .off, "该命令仅选择目标,不启动代理")
}
func testCoordinatorUsesProductionAdapterToClearRemovedActiveTarget() async throws {
let server = try makeSeededCatalog()
try ActivationStateFileStore(fileURL: activationFileURL).save(activeTargetID: server)
let fileStore = CatalogFileStore(fileURL: catalogFileURL)
let bootstrap = CatalogCommitCoordinator.bootstrap(fileStore: fileStore)
let controller = makeController(catalogSnapshotReader: bootstrap.catalogSnapshotReader)
let coordinator = CatalogCommitCoordinator(
fileStore: fileStore,
runtime: ProxyRuntimeSyncAdapter(controller: controller),
bootstrap: bootstrap)
try coordinator.commit { catalog, _ in try catalog.remove(server) }
await waitUntilRuntimeSettles(
{
if case .finished = coordinator.syncStatus { return true }
return false
}())
guard case .finished(_, .clearedAndStopped) = coordinator.syncStatus else {
return XCTFail("生产 adapter 应以刚提交快照清理无效目标")
}
XCTAssertNil(controller.activeTargetID)
XCTAssertNil(
try ActivationStateFileStore(fileURL: activationFileURL).loadActiveTargetID(),
"adapter 收敛后持久化清除失效目标")
}
private func makeController(
catalogSnapshotReader: RuntimeCatalogSnapshotReading
) -> ProxyRuntimeController {
ProxyRuntimeController(
catalogSnapshotReader: catalogSnapshotReader,
activationFileStore: ActivationStateFileStore(fileURL: activationFileURL),
runtimeFileStore: RuntimeFileStore(fileURL: runtime.contract),
credentials: credentials,
plugins: ActivationFixture.plugins,
listenRestore: RestoredListenSettings(
settings: ActivationFixture.listen, unreadableError: nil),
settingsStore: InMemoryProxySettingsStore(),
agent: agent,
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
pacProbe: ProxyRuntimeFixture.FakePACProbe(),
systemProxy: systemProxy,
firewallExecutableURLs: [URL(fileURLWithPath: "/bundle/Helpers/sslocal")],
firewallPollIntervalNanoseconds: 1_000_000,
sendSignal: { _, _ in 0 })
}
private func makeSeededCatalog() throws -> NodeID {
var catalog = ConfigurationCatalog()
let server = try ActivationFixture.addPlainServer(
"待删除", in: &catalog, credentials: credentials)
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
return server
}
}
@@ -78,7 +78,7 @@ final class ProxyRuntimeControllerTests: XCTestCase {
) -> ProxyRuntimeController {
agent.setStatus(agentStatus)
return ProxyRuntimeController(
catalogFileStore: CatalogFileStore(fileURL: catalogFileURL),
catalogSnapshotReader: ProxyRuntimeFixture.catalogSnapshotReader(at: catalogFileURL),
activationFileStore: ActivationStateFileStore(fileURL: activationFileURL),
runtimeFileStore: RuntimeFileStore(fileURL: runtime.contract),
credentials: credentials,
@@ -25,6 +25,12 @@ enum ProxyRuntimeFixture {
pidFile: directory.appendingPathComponent("agent.pid"))
}
@MainActor
static func catalogSnapshotReader(at fileURL: URL) -> RuntimeCatalogSnapshotReading {
CatalogCommitCoordinator.bootstrap(fileStore: CatalogFileStore(fileURL: fileURL))
.catalogSnapshotReader
}
static func unusedLoopbackPort() throws -> Int {
let descriptor = socket(AF_INET, SOCK_STREAM, 0)
guard descriptor >= 0 else { throw POSIXError(.ENOTSOCK) }
@@ -116,8 +116,11 @@ final class SettingsWorkflowTests: XCTestCase {
// MARK: - 夹具
private func makePair() -> (controller: ProxyRuntimeController, workflow: SettingsWorkflow) {
let catalogSnapshotReader = CatalogCommitCoordinator.bootstrap(
fileStore: CatalogFileStore(fileURL: catalogFileURL)
).catalogSnapshotReader
let controller = ProxyRuntimeController(
catalogFileStore: CatalogFileStore(fileURL: catalogFileURL),
catalogSnapshotReader: catalogSnapshotReader,
activationFileStore: ActivationStateFileStore(fileURL: activationFileURL),
runtimeFileStore: RuntimeFileStore(fileURL: runtime.contract),
credentials: credentials,
@@ -33,8 +33,11 @@ final class UnreadableListenSettingsTests: XCTestCase {
listenUnreadable: ListenSettingsStoreError
) -> ProxyRuntimeController {
agent.setStatus(agentStatus)
let catalogSnapshotReader = CatalogCommitCoordinator.bootstrap(
fileStore: CatalogFileStore(fileURL: catalogFileURL)
).catalogSnapshotReader
return ProxyRuntimeController(
catalogFileStore: CatalogFileStore(fileURL: catalogFileURL),
catalogSnapshotReader: catalogSnapshotReader,
activationFileStore: ActivationStateFileStore(fileURL: activationFileURL),
runtimeFileStore: RuntimeFileStore(fileURL: runtime.contract),
credentials: credentials,