From 71c4cea5cdba7a17477d40f27e558a0f1f6366f7 Mon Sep 17 00:00:00 2001 From: Ayman Hamed Date: Sat, 10 Oct 2026 17:30:23 +0300 Subject: [PATCH] sim_launch: refuse a shut-down device, say what to check on a timeout, driver reports a failed launch A run spent 240 s on two launch timeouts and then paused: the session's device wasn't booted and the only signal was "The request timed out" (Naseem, 2026-10-10). sim_launch now refuses a device that isn't booted with the reason; a driver timeout says to check sim_list and retry once; the driver's /launch waits for the foreground and reports launch_failed with the app's state instead of answering OK and failing on the next call. Co-Authored-By: Claude Fable 5.1 --- .../SimDriverUITests/DriverRoutes.swift | 8 ++++ .../Tools/SimLifecycleTools.swift | 22 +++++++++- .../SimToolsTests.swift | 42 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftAgentKitSimulator/Resources/SimDriverProject/SimDriverUITests/DriverRoutes.swift b/Sources/SwiftAgentKitSimulator/Resources/SimDriverProject/SimDriverUITests/DriverRoutes.swift index 6487a2e..d27965f 100644 --- a/Sources/SwiftAgentKitSimulator/Resources/SimDriverProject/SimDriverUITests/DriverRoutes.swift +++ b/Sources/SwiftAgentKitSimulator/Resources/SimDriverProject/SimDriverUITests/DriverRoutes.swift @@ -94,6 +94,14 @@ final class DriverRoutes { let a = app(r.bundleId) if r.terminateFirst { a.terminate() } a.launch() + // launch() reports a failure to XCTest, not to us: without + // this the route answered OK and the next call failed with + // "application is not running" (code 10001). + guard a.wait(for: .runningForeground, timeout: 30) else { + return .json(SimWire.ErrorResponse(code: "launch_failed", + message: "\(r.bundleId) did not reach the foreground within 30 s (state \(a.state.rawValue)). " + + "Is it installed on this device? Use sim_build_install, then sim_launch."), status: 500) + } return .json(SimWire.OKResponse()) case ("POST", "/terminate"): let r = try decode(SimWire.LaunchRequest.self, req) diff --git a/Sources/SwiftAgentKitSimulator/Tools/SimLifecycleTools.swift b/Sources/SwiftAgentKitSimulator/Tools/SimLifecycleTools.swift index 4a86baf..fe3f0f2 100644 --- a/Sources/SwiftAgentKitSimulator/Tools/SimLifecycleTools.swift +++ b/Sources/SwiftAgentKitSimulator/Tools/SimLifecycleTools.swift @@ -142,12 +142,26 @@ public struct SimLaunchTool: AgentTool { public var requiresConfirmation: Bool { true } let client: any SimDriving let session: SimSession - public init(client: any SimDriving, session: SimSession) { self.client = client; self.session = session } + /// The device list, so a launch on a shut-down device is refused with + /// the reason instead of timing out in the driver (tests inject one). + let devices: @Sendable () async throws -> [Simctl.SimDevice] + public init(client: any SimDriving, session: SimSession, + devices: @escaping @Sendable () async throws -> [Simctl.SimDevice] = { try await Simctl.listDevices() }) { + self.client = client; self.session = session; self.devices = devices + } public func execute(parameters: [String: Any]) async throws -> AgentToolResult { guard let bundleId = parameters["bundle_id"] as? String, !bundleId.isEmpty else { return .error(toolCallId: "", toolName: name, message: "sim_launch requires `bundle_id`.") } + // A launch on a device that isn't booted hangs in the driver until the + // request times out (120 s, twice in one run, 2026-10-10). Say so now. + if let udid = session.udid, let device = try? await devices().first(where: { $0.udid == udid }), + !device.isBooted { + return .error(toolCallId: "", toolName: name, message: + "\(device.name) [\(udid)] is not booted (state: \(device.state)). " + + "Call sim_boot for it first, or sim_list to pick a booted device.") + } let terminateFirst = (parameters["terminate_first"] as? Bool) ?? false do { try await client.launch(bundleId: bundleId, terminateFirst: terminateFirst) @@ -157,6 +171,12 @@ public struct SimLaunchTool: AgentTool { } catch let e as SimDriverError { return .error(toolCallId: "", toolName: name, message: e.localizedDescription + (e.tree.map { "\n\nCurrent UI:\n" + $0.renderCompact() } ?? "")) + } catch let e as URLError where e.code == .timedOut { + return .error(toolCallId: "", toolName: name, message: + "sim_launch: the simulator driver did not answer within the request timeout. " + + "The simulator may still be booting, or the driver is stuck. " + + "Run sim_list to check the device is Booted, then retry sim_launch once; " + + "if it times out again, report that instead of retrying.") } catch { return .error(toolCallId: "", toolName: name, message: "sim_launch failed: \(error.localizedDescription)") } diff --git a/Tests/SwiftAgentKitSimulatorTests/SimToolsTests.swift b/Tests/SwiftAgentKitSimulatorTests/SimToolsTests.swift index 68c42af..a3b79f0 100644 --- a/Tests/SwiftAgentKitSimulatorTests/SimToolsTests.swift +++ b/Tests/SwiftAgentKitSimulatorTests/SimToolsTests.swift @@ -275,6 +275,48 @@ final class SimToolsTests: XCTestCase { XCTAssertTrue(result.result.contains("bundle_id")) } + /// A run spent 240 s on two launch timeouts and then paused: the session's + /// device wasn't booted, and the only signal was "The request timed out" + /// (Naseem, 2026-10-10). The launch is refused with the reason instead. + func testSimLaunchRefusesAnUnbootedDevice() async throws { + let mock = MockDriver() + let session = SimSession() + session.udid = "U1" + let tool = SimLaunchTool(client: mock, session: session, devices: { + [Simctl.SimDevice(udid: "U1", name: "iPad Pro 13", state: "Shutdown", runtime: "iOS-26-1")] + }) + let result = try await tool.execute(parameters: ["bundle_id": "com.example.app"]) + XCTAssertTrue(result.isError) + XCTAssertTrue(result.result.contains("iPad Pro 13")) + XCTAssertTrue(result.result.contains("not booted")) + XCTAssertTrue(result.result.contains("sim_boot")) + XCTAssertEqual(mock.lastCall, "", "the driver must not be asked to launch on a shut-down device") + } + + func testSimLaunchProceedsOnABootedDevice() async throws { + let mock = MockDriver() + let session = SimSession() + session.udid = "U1" + let tool = SimLaunchTool(client: mock, session: session, devices: { + [Simctl.SimDevice(udid: "U1", name: "iPhone 17 Pro", state: "Booted", runtime: "iOS-26-1")] + }) + let result = try await tool.execute(parameters: ["bundle_id": "com.example.app"]) + XCTAssertFalse(result.isError) + XCTAssertEqual(mock.lastCall, "launch:com.example.app") + } + + /// The driver answering nothing for the whole request timeout says what + /// to do next, not just "timed out". + func testSimLaunchTimeoutSaysWhatToCheck() async throws { + let mock = MockDriver() + mock.errorToThrow = URLError(.timedOut) + let tool = SimLaunchTool(client: mock, session: SimSession()) + let result = try await tool.execute(parameters: ["bundle_id": "com.example.app"]) + XCTAssertTrue(result.isError) + XCTAssertTrue(result.result.contains("sim_list")) + XCTAssertTrue(result.result.contains("once")) + } + // MARK: sim_terminate func testSimTerminateClearsSessionBundleId() async throws {