diff --git a/Sources/Nirux/Model/MissionEventCLI.swift b/Sources/Nirux/Model/MissionEventCLI.swift index ef43865..c52af01 100644 --- a/Sources/Nirux/Model/MissionEventCLI.swift +++ b/Sources/Nirux/Model/MissionEventCLI.swift @@ -274,13 +274,16 @@ enum MissionEventCLI { /// Wait for the next unconsumed child question/completion belonging to /// this parent column. Questions stay pending until a response is sent; - /// completion is acknowledged immediately after it is printed. + /// completion is acknowledged after it is printed, and `receive` returns + /// only once Nirux recorded that (or after `confirmationTimeout`), so + /// running it again right away does not print the same completion again. static func receive( arguments: [String], environment: [String: String] = ProcessInfo.processInfo.environment, now: @escaping () -> TimeInterval = { Date().timeIntervalSince1970 }, eventsURL: URL = MissionEventCenter.defaultEventsURL, missionsURL: URL = MissionStore.defaultFileURL, + confirmationTimeout: TimeInterval = 5, pollInterval: TimeInterval = 0.2 ) -> Int32 { guard let context = parentContext(environment) else { return notAMissionTerminal(child: false) } @@ -340,6 +343,13 @@ enum MissionEventCLI { timestamp: now() ) guard append(acknowledgement, to: eventsURL) else { return 1 } + _ = waitForLedger( + &ledger, timeout: confirmationTimeout, pollInterval: pollInterval + ) { missions in + missions.lazy.flatMap(\.events).contains(where: { + $0.id == event.id && $0.parentConsumedAt != nil + }) ? true : nil + } } return 0 } diff --git a/Tests/NiruxTests/MissionAskTests.swift b/Tests/NiruxTests/MissionAskTests.swift index c2855a4..1fb1d12 100644 --- a/Tests/NiruxTests/MissionAskTests.swift +++ b/Tests/NiruxTests/MissionAskTests.swift @@ -271,6 +271,7 @@ final class MissionAskTests: XCTestCase { environment: parentEnvironment, eventsURL: fixture.eventsURL, missionsURL: fixture.missionsURL, + confirmationTimeout: 0, pollInterval: 0.01 ) } @@ -301,6 +302,35 @@ final class MissionAskTests: XCTestCase { ), 1) } + func testReceiveDeliversACompletionOnceWhenRunAgainRightAway() async throws { + let fixture = try makeFixture() + complete(fixture) + let environment = parentEnvironment + let eventsURL = fixture.eventsURL + let missionsURL = fixture.missionsURL + let receive: @Sendable () -> Int32 = { + MissionEventCLI.receive( + arguments: ["--timeout", "0"], + environment: environment, + eventsURL: eventsURL, + missionsURL: missionsURL, + pollInterval: 0.01 + ) + } + // The parent runs `receive` again as soon as the first one returns. + let twice = Task.detached { (receive(), receive()) } + // Nirux drains the queue a moment after the acknowledgement lands. + try await Task.sleep(nanoseconds: 200_000_000) + let deadline = Date().addingTimeInterval(5) + while fixture.store.missions[0].events[0].parentConsumedAt == nil, Date() < deadline { + fixture.center.drain() + try await Task.sleep(nanoseconds: 10_000_000) + } + let (first, second) = await twice.value + XCTAssertEqual(first, 0, "prints the completion") + XCTAssertEqual(second, 4, "the completion is not printed again") + } + func testReplyStopsWhenTheQuestionWasAlreadyAnswered() throws { let fixture = try makeFixture() XCTAssertEqual(ask("Which API?", in: fixture), 3) diff --git a/Tests/NiruxTests/MissionTellTests.swift b/Tests/NiruxTests/MissionTellTests.swift index d1387b0..94a23d1 100644 --- a/Tests/NiruxTests/MissionTellTests.swift +++ b/Tests/NiruxTests/MissionTellTests.swift @@ -234,6 +234,7 @@ final class MissionTellTests: XCTestCase { environment: parentEnvironment(), eventsURL: fixture.eventsURL, missionsURL: fixture.missionsURL, + confirmationTimeout: 0, pollInterval: 0.01 ) } diff --git a/Tests/NiruxTests/MissionTests.swift b/Tests/NiruxTests/MissionTests.swift index bcb3f14..68bfa97 100644 --- a/Tests/NiruxTests/MissionTests.swift +++ b/Tests/NiruxTests/MissionTests.swift @@ -208,6 +208,7 @@ final class MissionTests: XCTestCase { now: { 40 }, eventsURL: eventsURL, missionsURL: missionsURL, + confirmationTimeout: 0, pollInterval: 0.01 ), 0) let acknowledgement = try decodeSingleEvent(from: eventsURL) @@ -387,6 +388,7 @@ extension MissionTests { now: { 40 }, eventsURL: eventsURL, missionsURL: missionsURL, + confirmationTimeout: 0, pollInterval: 0.01 ), 0) let center = MissionEventCenter(store: store, eventsURL: eventsURL, isEnabled: { true })