Fix browser eval numeric value formatting (#8077)
* test: cover browser eval scalar output * Fix browser eval numeric value formatting * Fix browser eval test bundle lookup
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import Foundation
|
||||
|
||||
/// Converts browser RPC values into their plain-text CLI representation.
|
||||
struct BrowserValueTextFormatter {
|
||||
func string(from value: Any) -> String {
|
||||
if let dictionary = value as? [String: Any],
|
||||
dictionary["__cmux_t"] as? String == "undefined" {
|
||||
return "undefined"
|
||||
}
|
||||
if value is NSNull {
|
||||
return "null"
|
||||
}
|
||||
if let string = value as? String {
|
||||
return string
|
||||
}
|
||||
if let number = value as? NSNumber {
|
||||
return string(from: number)
|
||||
}
|
||||
if JSONSerialization.isValidJSONObject(value),
|
||||
let data = try? JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted]),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
return text
|
||||
}
|
||||
return String(describing: value)
|
||||
}
|
||||
|
||||
private func string(from number: NSNumber) -> String {
|
||||
if CFGetTypeID(number) == CFBooleanGetTypeID() {
|
||||
return number.boolValue ? "true" : "false"
|
||||
}
|
||||
|
||||
let value = number.doubleValue
|
||||
if value.isNaN {
|
||||
return "NaN"
|
||||
}
|
||||
if value == .infinity {
|
||||
return "Infinity"
|
||||
}
|
||||
if value == -.infinity {
|
||||
return "-Infinity"
|
||||
}
|
||||
return number.stringValue
|
||||
}
|
||||
}
|
||||
+4
-29
@@ -13068,6 +13068,7 @@ struct CMUXCLI {
|
||||
}
|
||||
let subcommand = subcommandRaw.lowercased()
|
||||
let subArgs = Array(args.dropFirst())
|
||||
let browserValueTextFormatter = BrowserValueTextFormatter()
|
||||
|
||||
func requireSurface() throws -> String {
|
||||
guard let raw = surfaceRaw else {
|
||||
@@ -13114,32 +13115,6 @@ struct CMUXCLI {
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
func displayBrowserValue(_ value: Any) -> String {
|
||||
if let dict = value as? [String: Any],
|
||||
let type = dict["__cmux_t"] as? String,
|
||||
type == "undefined" {
|
||||
return "undefined"
|
||||
}
|
||||
if value is NSNull {
|
||||
return "null"
|
||||
}
|
||||
if let string = value as? String {
|
||||
return string
|
||||
}
|
||||
if let bool = value as? Bool {
|
||||
return bool ? "true" : "false"
|
||||
}
|
||||
if let number = value as? NSNumber {
|
||||
return number.stringValue
|
||||
}
|
||||
if JSONSerialization.isValidJSONObject(value),
|
||||
let data = try? JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted]),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
return text
|
||||
}
|
||||
return String(describing: value)
|
||||
}
|
||||
|
||||
func displayBrowserLogItems(_ value: Any?) -> String? {
|
||||
guard let items = value as? [Any], !items.isEmpty else {
|
||||
return nil
|
||||
@@ -13147,7 +13122,7 @@ struct CMUXCLI {
|
||||
|
||||
let lines = items.map { item -> String in
|
||||
guard let dict = item as? [String: Any] else {
|
||||
return displayBrowserValue(item)
|
||||
return browserValueTextFormatter.string(from: item)
|
||||
}
|
||||
|
||||
let text = (dict["text"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
@@ -13159,7 +13134,7 @@ struct CMUXCLI {
|
||||
!message.isEmpty {
|
||||
return "[error] \(message)"
|
||||
}
|
||||
return displayBrowserValue(dict)
|
||||
return browserValueTextFormatter.string(from: dict)
|
||||
}
|
||||
return "[\(level)] \(text)"
|
||||
}
|
||||
@@ -13798,7 +13773,7 @@ struct CMUXCLI {
|
||||
let payload = try client.sendV2(method: "browser.eval", params: ["surface_id": sid, "script": trimmed])
|
||||
let fallback: String
|
||||
if let value = payload["value"] {
|
||||
fallback = displayBrowserValue(value)
|
||||
fallback = browserValueTextFormatter.string(from: value)
|
||||
} else {
|
||||
fallback = "OK"
|
||||
}
|
||||
|
||||
@@ -319,6 +319,8 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources
|
||||
4BBF42E8A86EAAB94BC9EA16 /* BrowserSystemProxyMirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048DACB3F8147BCBDD266297 /* BrowserSystemProxyMirror.swift */; };
|
||||
C7B800062D4202A13C962D2D /* BrowserSystemProxyMirrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D828DA0070335773EBAE83F /* BrowserSystemProxyMirrorTests.swift */; };
|
||||
0796994CA7CCA25DB990BFEF /* BrowserSystemProxyWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E7A5E13C6DF650B9F8971 /* BrowserSystemProxyWatcher.swift */; };
|
||||
805500000000000000000004 /* BrowserValueTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805500000000000000000003 /* BrowserValueTextFormatter.swift */; };
|
||||
805500000000000000000005 /* BrowserValueTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805500000000000000000003 /* BrowserValueTextFormatter.swift */; };
|
||||
A50100000000000000000002 /* BrowserWebAuthnAssertionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50100000000000000000001 /* BrowserWebAuthnAssertionExtensions.swift */; };
|
||||
A50100000000000000000004 /* BrowserWebAuthnAssertionPublicKeyOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50100000000000000000003 /* BrowserWebAuthnAssertionPublicKeyOptions.swift */; };
|
||||
A50100000000000000000006 /* BrowserWebAuthnAssertionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50100000000000000000005 /* BrowserWebAuthnAssertionRequest.swift */; };
|
||||
@@ -376,6 +378,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources
|
||||
A9F200000000000000000015 /* ClaudeStreamJSONAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000015 /* ClaudeStreamJSONAccumulator.swift */; };
|
||||
CE7000000000000000000001 /* ClaudeWrapperResumeEnvironmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7000000000000000000002 /* ClaudeWrapperResumeEnvironmentTests.swift */; };
|
||||
A5D4120DA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */; };
|
||||
805500000000000000000001 /* CLIBrowserEvalOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805500000000000000000002 /* CLIBrowserEvalOutputTests.swift */; };
|
||||
CA11E4D0FA017DE500000B101 /* CLICallerWorkspaceDefaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E4D0FA017DE500000F102 /* CLICallerWorkspaceDefaultTests.swift */; };
|
||||
C0D3F1F00000000000000103 /* CLICodexHookTimeoutRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */; };
|
||||
C0D3F1F10000000000000103 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */; };
|
||||
@@ -2256,6 +2259,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
048DACB3F8147BCBDD266297 /* BrowserSystemProxyMirror.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserSystemProxyMirror.swift; sourceTree = "<group>"; };
|
||||
8D828DA0070335773EBAE83F /* BrowserSystemProxyMirrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserSystemProxyMirrorTests.swift; sourceTree = "<group>"; };
|
||||
326E7A5E13C6DF650B9F8971 /* BrowserSystemProxyWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserSystemProxyWatcher.swift; sourceTree = "<group>"; };
|
||||
805500000000000000000003 /* BrowserValueTextFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserValueTextFormatter.swift; sourceTree = "<group>"; };
|
||||
A50100000000000000000001 /* BrowserWebAuthnAssertionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserWebAuthnAssertionExtensions.swift; sourceTree = "<group>"; };
|
||||
A50100000000000000000003 /* BrowserWebAuthnAssertionPublicKeyOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserWebAuthnAssertionPublicKeyOptions.swift; sourceTree = "<group>"; };
|
||||
A50100000000000000000005 /* BrowserWebAuthnAssertionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserWebAuthnAssertionRequest.swift; sourceTree = "<group>"; };
|
||||
@@ -2313,6 +2317,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
A9F100000000000000000015 /* ClaudeStreamJSONAccumulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ClaudeStreamJSONAccumulator.swift; sourceTree = "<group>"; };
|
||||
CE7000000000000000000002 /* ClaudeWrapperResumeEnvironmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeWrapperResumeEnvironmentTests.swift; sourceTree = "<group>"; };
|
||||
A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIAuthAliasTests.swift; sourceTree = "<group>"; };
|
||||
805500000000000000000002 /* CLIBrowserEvalOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIBrowserEvalOutputTests.swift; sourceTree = "<group>"; };
|
||||
CA11E4D0FA017DE500000F102 /* CLICallerWorkspaceDefaultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICallerWorkspaceDefaultTests.swift; sourceTree = "<group>"; };
|
||||
C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookTimeoutRegressionTests.swift; sourceTree = "<group>"; };
|
||||
C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookTimeoutRegressionTestSupport.swift; sourceTree = "<group>"; };
|
||||
@@ -5151,6 +5156,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
B9000003A1B2C3D4E5F60719 /* CLI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
805500000000000000000003 /* BrowserValueTextFormatter.swift */,
|
||||
B9000001A1B2C3D4E5F60719 /* cmux.swift */,
|
||||
REE0CA0000000000000000C1 /* CMUXCLI+Remotes.swift */,
|
||||
C77070000000000000000001 /* SSHPTYAttachExitCode.swift */,
|
||||
@@ -5485,6 +5491,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
A7367002A1B2C3D4E5F60718 /* CLISSHSessionAttachAnchorTests.swift */,
|
||||
773600000000000000000002 /* CLIWindowCommandMockServer.swift */,
|
||||
773600000000000000000004 /* CLIWindowHandleRoutingTests.swift */,
|
||||
805500000000000000000002 /* CLIBrowserEvalOutputTests.swift */,
|
||||
CA11E4D0FA017DE500000F102 /* CLICallerWorkspaceDefaultTests.swift */,
|
||||
C0DE31390000000000000106 /* CMUXCLIErrorOutputRegressionTests.swift */,
|
||||
C0DE64950000000000000006 /* CMUXCLISessionsListTests.swift */,
|
||||
@@ -7322,6 +7329,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
files = (
|
||||
D36A00030000000000000003 /* AgentHibernationLifecycleState.swift in Sources */,
|
||||
A76110010000000000000001 /* AgentHookNotificationPolicy.swift in Sources */,
|
||||
805500000000000000000004 /* BrowserValueTextFormatter.swift in Sources */,
|
||||
B900004AA1B2C3D4E5F60719 /* CLISocketPathResolver.swift in Sources */,
|
||||
C12985000000000000000002 /* CLISocketSentryTelemetry.swift in Sources */,
|
||||
B9000002A1B2C3D4E5F60719 /* cmux.swift in Sources */,
|
||||
@@ -7549,6 +7557,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
B6585001B6585001B658PW01 /* BrowserPrewarmedWebViewPoolTests.swift in Sources */,
|
||||
C2035A060000000000000001 /* BrowserSSLTrustBypassStateTests.swift in Sources */,
|
||||
C7B800062D4202A13C962D2D /* BrowserSystemProxyMirrorTests.swift in Sources */,
|
||||
805500000000000000000005 /* BrowserValueTextFormatter.swift in Sources */,
|
||||
C0DE49870000000000000001 /* BrowserWebContentProcessTests.swift in Sources */,
|
||||
C0DE62600000000000000001 /* BrowserWindowPortalRegistryNotificationTests.swift in Sources */,
|
||||
C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */,
|
||||
@@ -7570,6 +7579,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
|
||||
A5D41222A1B2C3D4E5F60718 /* ClaudeNotificationStatusLifecycleTests.swift in Sources */,
|
||||
CE7000000000000000000001 /* ClaudeWrapperResumeEnvironmentTests.swift in Sources */,
|
||||
A5D4120DA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift in Sources */,
|
||||
805500000000000000000001 /* CLIBrowserEvalOutputTests.swift in Sources */,
|
||||
CA11E4D0FA017DE500000B101 /* CLICallerWorkspaceDefaultTests.swift in Sources */,
|
||||
C0D3F1F00000000000000103 /* CLICodexHookTimeoutRegressionTests.swift in Sources */,
|
||||
C0D3F1F10000000000000103 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
final class CLIBrowserEvalOutputTests {
|
||||
private struct Case {
|
||||
let name: String
|
||||
let wireValue: String
|
||||
let expectedOutput: String
|
||||
}
|
||||
|
||||
// https://github.com/manaflow-ai/cmux/issues/8055
|
||||
@Test("browser eval preserves scalar values in text output")
|
||||
func browserEvalPreservesScalarValues() throws {
|
||||
let cases = [
|
||||
Case(name: "integer zero", wireValue: "0", expectedOutput: "0"),
|
||||
Case(name: "floating-point zero", wireValue: "0.0", expectedOutput: "0"),
|
||||
Case(name: "integer one", wireValue: "1", expectedOutput: "1"),
|
||||
Case(name: "floating-point one", wireValue: "1.0", expectedOutput: "1"),
|
||||
Case(name: "false", wireValue: "false", expectedOutput: "false"),
|
||||
Case(name: "true", wireValue: "true", expectedOutput: "true"),
|
||||
Case(name: "empty string", wireValue: "\"\"", expectedOutput: ""),
|
||||
Case(name: "null", wireValue: "null", expectedOutput: "null"),
|
||||
Case(
|
||||
name: "undefined envelope",
|
||||
wireValue: #"{"__cmux_t":"undefined","__cmux_v":null}"#,
|
||||
expectedOutput: "undefined"
|
||||
),
|
||||
]
|
||||
|
||||
for testCase in cases {
|
||||
try assertBrowserEvalOutput(testCase)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("browser value formatter distinguishes booleans from every numeric representation")
|
||||
func browserValueFormatterPreservesFoundationScalarTypes() {
|
||||
let formatter = BrowserValueTextFormatter()
|
||||
|
||||
#expect(formatter.string(from: NSNumber(value: false)) == "false")
|
||||
#expect(formatter.string(from: NSNumber(value: true)) == "true")
|
||||
#expect(formatter.string(from: NSNumber(value: 0)) == "0")
|
||||
#expect(formatter.string(from: NSNumber(value: 0.0)) == "0")
|
||||
#expect(formatter.string(from: NSNumber(value: 1)) == "1")
|
||||
#expect(formatter.string(from: NSNumber(value: 1.0)) == "1")
|
||||
#expect(formatter.string(from: NSNumber(value: Double.nan)) == "NaN")
|
||||
#expect(formatter.string(from: NSNumber(value: Double.infinity)) == "Infinity")
|
||||
#expect(formatter.string(from: NSNumber(value: -Double.infinity)) == "-Infinity")
|
||||
#expect(formatter.string(from: "") == "")
|
||||
#expect(formatter.string(from: NSNull()) == "null")
|
||||
}
|
||||
|
||||
private func assertBrowserEvalOutput(_ testCase: Case) throws {
|
||||
let socketPath = "/tmp/cmux-eval-\(UUID().uuidString.prefix(8)).sock"
|
||||
let response = #"{"id":null,"ok":true,"result":{"value":\#(testCase.wireValue)}}"#
|
||||
let responder = try UnixSocketResponder(path: socketPath, response: response)
|
||||
defer { responder.stop() }
|
||||
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
for key in Array(environment.keys) where key.hasPrefix("CMUX_") {
|
||||
environment.removeValue(forKey: key)
|
||||
}
|
||||
environment["CMUX_SOCKET_PATH"] = socketPath
|
||||
environment["CMUX_CLI_SENTRY_DISABLED"] = "1"
|
||||
|
||||
let result = try runProcess(
|
||||
executablePath: BundledCLITestSupport.bundledCLIPath(for: Self.self),
|
||||
arguments: ["browser", UUID().uuidString, "eval", "0"],
|
||||
environment: environment
|
||||
)
|
||||
|
||||
#expect(!result.timedOut, Comment(rawValue: "\(testCase.name): \(result.output)"))
|
||||
#expect(result.status == 0, Comment(rawValue: "\(testCase.name): \(result.output)"))
|
||||
#expect(
|
||||
result.output == testCase.expectedOutput + "\n",
|
||||
Comment(rawValue: "\(testCase.name): \(result.output.debugDescription)")
|
||||
)
|
||||
}
|
||||
|
||||
private func runProcess(
|
||||
executablePath: String,
|
||||
arguments: [String],
|
||||
environment: [String: String]
|
||||
) throws -> (status: Int32, output: String, timedOut: Bool) {
|
||||
let process = Process()
|
||||
let outputPipe = Pipe()
|
||||
let exited = DispatchSemaphore(value: 0)
|
||||
|
||||
process.executableURL = URL(fileURLWithPath: executablePath)
|
||||
process.arguments = arguments
|
||||
process.environment = environment
|
||||
process.standardInput = FileHandle.nullDevice
|
||||
process.standardOutput = outputPipe
|
||||
process.standardError = outputPipe
|
||||
process.terminationHandler = { _ in exited.signal() }
|
||||
|
||||
try process.run()
|
||||
let timedOut = exited.wait(timeout: .now() + 5) == .timedOut
|
||||
if timedOut {
|
||||
process.terminate()
|
||||
_ = exited.wait(timeout: .now() + 1)
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
return (
|
||||
status: process.terminationStatus,
|
||||
output: String(data: outputData, encoding: .utf8) ?? "",
|
||||
timedOut: timedOut
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1502,7 +1502,7 @@ import Testing
|
||||
}
|
||||
}
|
||||
|
||||
private final class UnixSocketResponder {
|
||||
final class UnixSocketResponder {
|
||||
let path: String
|
||||
private let response: String
|
||||
private let responseDelay: TimeInterval
|
||||
|
||||
Reference in New Issue
Block a user