Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 21 additions & 1 deletion Sources/SwiftAgentKitSimulator/Tools/SimLifecycleTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)")
}
Expand Down
42 changes: 42 additions & 0 deletions Tests/SwiftAgentKitSimulatorTests/SimToolsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading