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:
Austin Wang
2026-07-14 15:54:02 -07:00
committed by GitHub
parent 78ce5aa968
commit d8f6bdd76e
5 changed files with 170 additions and 30 deletions
+44
View File
@@ -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
View File
@@ -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"
}
+10
View File
@@ -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 */,
+111
View File
@@ -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