From 17c2a76194e230d4ec204becec243dad7acaa8bb Mon Sep 17 00:00:00 2001 From: LS Date: Mon, 3 Aug 2026 02:19:11 +0800 Subject: [PATCH] fix(tests): serialize AffordanceRegistry tests to remove flake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `booted simulator apiLinks include plugin stream when registered` and `booted simulator affordances include plugin stream when registered` failed intermittently under parallel scheduling. AffordanceRegistry is process-global mutable state. AffordanceRegistryTests is `@Suite(.serialized)` and resets the registry in `init()`, while SimulatorTests is a plain parallel `@Suite` whose plugin tests register a provider and then read the result. `.serialized` only orders tests *within* a suite — separate suites still run concurrently, so the reset lands between another suite's `register()` and its assertion and the affordance reads back nil. The `.serialized` trait on the individual test functions does not help: it scopes parameterized cases, not cross-test isolation, so the three Simulator plugin tests were also racing each other. Move the three registry-touching Simulator tests into the serialized AffordanceRegistryTests suite so every test that mutates the global registry shares one serial scope. Assertions are unchanged; the redundant inline `reset()` calls are dropped since the suite's `init()` already resets. Measured with `swift test --filter "SimulatorTests|AffordanceRegistryTests"`, 20 runs each: 15/20 failed before, 0/20 after. Full suite 5/5 green, 2383 tests unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../Shared/AffordanceRegistryTests.swift | 41 +++++++++++++++++++ .../Simulators/SimulatorTests.swift | 40 ++---------------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/Tests/DomainTests/Shared/AffordanceRegistryTests.swift b/Tests/DomainTests/Shared/AffordanceRegistryTests.swift index 73df071b..4d93f914 100644 --- a/Tests/DomainTests/Shared/AffordanceRegistryTests.swift +++ b/Tests/DomainTests/Shared/AffordanceRegistryTests.swift @@ -66,4 +66,45 @@ struct AffordanceRegistryTests { #expect(link.method == "POST") #expect(link.href.contains("simulators")) } + + // MARK: - Plugin affordances merged into a real domain model + // + // These live here, not in SimulatorTests, on purpose. AffordanceRegistry is + // process-global mutable state, and `.serialized` only orders tests *within* + // a suite — separate suites still run concurrently. Any test that registers a + // provider must therefore share this one serialized suite, or `init()`'s reset + // will wipe its registration mid-test. Register from anywhere else and you + // reintroduce a flake that only shows up under parallel scheduling. + + @Test func `booted simulator affordances include plugin stream when registered`() { + AffordanceRegistry.register(Simulator.self) { id, props in + guard props["isBooted"] == "true" else { return [] } + return [Affordance(key: "stream", command: "simulators", action: "stream", params: ["udid": id])] + } + let sim = MockRepositoryFactory.makeSimulator(id: "SIM-1", state: .booted) + // Plugin affordance should be merged into the model's own affordances + #expect(sim.affordances["stream"] == "asc simulators stream --udid SIM-1") + // Model's own affordances still present + #expect(sim.affordances["shutdown"] == "asc simulators shutdown --udid SIM-1") + } + + @Test func `shutdown simulator does not get stream affordance from plugin`() { + AffordanceRegistry.register(Simulator.self) { id, props in + guard props["isBooted"] == "true" else { return [] } + return [Affordance(key: "stream", command: "simulators", action: "stream", params: ["udid": id])] + } + let sim = MockRepositoryFactory.makeSimulator(id: "SIM-2", state: .shutdown) + #expect(sim.affordances["stream"] == nil) + } + + @Test func `booted simulator apiLinks include plugin stream when registered`() { + AffordanceRegistry.register(Simulator.self) { id, props in + guard props["isBooted"] == "true" else { return [] } + return [Affordance(key: "stream", command: "simulators", action: "stream", params: ["udid": id])] + } + let sim = MockRepositoryFactory.makeSimulator(id: "SIM-1", state: .booted) + // Plugin affordance should appear in REST links too + #expect(sim.apiLinks["stream"] != nil) + #expect(sim.apiLinks["stream"]?.method == "POST") + } } diff --git a/Tests/DomainTests/Simulators/SimulatorTests.swift b/Tests/DomainTests/Simulators/SimulatorTests.swift index 1379b6c7..eb68a5ff 100644 --- a/Tests/DomainTests/Simulators/SimulatorTests.swift +++ b/Tests/DomainTests/Simulators/SimulatorTests.swift @@ -151,41 +151,7 @@ struct SimulatorTests { #expect(sim.registryProperties["isBooted"] == "false") } - @Test(.serialized) func `booted simulator affordances include plugin stream when registered`() { - AffordanceRegistry.reset() - AffordanceRegistry.register(Simulator.self) { id, props in - guard props["isBooted"] == "true" else { return [] } - return [Affordance(key: "stream", command: "simulators", action: "stream", params: ["udid": id])] - } - let sim = MockRepositoryFactory.makeSimulator(id: "SIM-1", state: .booted) - // Plugin affordance should be merged into the model's own affordances - #expect(sim.affordances["stream"] == "asc simulators stream --udid SIM-1") - // Model's own affordances still present - #expect(sim.affordances["shutdown"] == "asc simulators shutdown --udid SIM-1") - AffordanceRegistry.reset() - } - - @Test(.serialized) func `shutdown simulator does not get stream affordance from plugin`() { - AffordanceRegistry.reset() - AffordanceRegistry.register(Simulator.self) { id, props in - guard props["isBooted"] == "true" else { return [] } - return [Affordance(key: "stream", command: "simulators", action: "stream", params: ["udid": id])] - } - let sim = MockRepositoryFactory.makeSimulator(id: "SIM-2", state: .shutdown) - #expect(sim.affordances["stream"] == nil) - AffordanceRegistry.reset() - } - - @Test(.serialized) func `booted simulator apiLinks include plugin stream when registered`() { - AffordanceRegistry.reset() - AffordanceRegistry.register(Simulator.self) { id, props in - guard props["isBooted"] == "true" else { return [] } - return [Affordance(key: "stream", command: "simulators", action: "stream", params: ["udid": id])] - } - let sim = MockRepositoryFactory.makeSimulator(id: "SIM-1", state: .booted) - // Plugin affordance should appear in REST links too - #expect(sim.apiLinks["stream"] != nil) - #expect(sim.apiLinks["stream"]?.method == "POST") - AffordanceRegistry.reset() - } + // Tests that register plugin providers for Simulator live in + // AffordanceRegistryTests — they need that suite's serialized scope because + // AffordanceRegistry is process-global. See the note there before adding more. }