import XCTest @testable import LChatCore // 🤖 The dismiss-a-labelled-ask contract (#1375), the native twin of the web's // "every cancel fires the cancelled hook" test: waving the question off clears // the composer pill (label + hint) AND disarms the flow's waiter, so the next // plain message is an ordinary message — never a swallowed answer. The parked // step re-arms on the next cold open, exactly like the web after a reload. @MainActor final class ElevenConversationModelTests: XCTestCase { // origin "" → no baseURL → api/socket are nil and every network path is a // quiet no-op, so the model exercises pure conversation state. private func makeModel() throws -> ElevenConversationModel { let instance = SavedInstance(origin: "", deviceToken: "t", screenName: "Paul", userPub: "", userPriv: "") let thread = ChatThread(id: "t1", kind: "self", title: "Eleven", preview: "", lastAt: 0, unread: false, pinned: false, hidden: false, muted: nil, members: [], threadKeyB64: "x", avatar: nil, isApp: true, appId: "setup", glyph: nil, badge: nil) return try XCTUnwrap(ElevenConversationModel(instance: instance, thread: thread, myUserId: "u1")) } func testDismissClearsPillAndDisarmsPendingAnswer() async throws { let model = try makeModel() model.composerLabel = "Your name:" model.composerHint = "Type your name" var answered: [String] = [] model.ask { answered.append($0.text) } model.cancelAnswer() XCTAssertNil(model.composerLabel, "dismiss drops the inset label") XCTAssertNil(model.composerHint, "dismiss drops the hint with it") await model.send("just a message") XCTAssertEqual(answered, [], "a later plain message is never a swallowed answer") guard case .mine(let text)? = model.rows.last?.kind else { return XCTFail("the plain message still lands in the transcript") } XCTAssertEqual(text, "just a message") } func testDismissWithNoWaiterStillClearsThePill() throws { // The web twin: cancelAnswer on an idle thread still clears the label. let model = try makeModel() model.composerLabel = "Your name:" model.composerHint = "Type your name" model.cancelAnswer() XCTAssertNil(model.composerLabel) XCTAssertNil(model.composerHint) } func testSendDeliversAnArmedAnswerAndDropsThePill() async throws { // The undismissed path keeps working: an armed ask consumes the send. let model = try makeModel() model.composerLabel = "Your name:" var answered: [String] = [] model.ask { answered.append($0.text) } await model.send("Paul") XCTAssertEqual(answered, ["Paul"]) XCTAssertNil(model.composerLabel, "the answered ask retires its label") } }