mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: restore recent native, browser, and audio behavior (#107631)
* fix: repair recent landed regressions * chore: keep release changelog owned * test(macos): align CLI fixtures with Node floor * fix(macos): document Node runtime floor contract * style(auto-reply): restore runtime mode formatting
This commit is contained in:
@@ -4767,6 +4767,7 @@ def mask_reference_declaration_evidence(text: str) -> str:
|
||||
def bare_code_reference(
|
||||
text: str,
|
||||
start: int,
|
||||
end: int,
|
||||
separator: str,
|
||||
value: str,
|
||||
) -> bool:
|
||||
@@ -4796,6 +4797,31 @@ def bare_code_reference(
|
||||
re.DOTALL,
|
||||
):
|
||||
return True
|
||||
# TS/JS named-function parameter annotations use PascalCase type names.
|
||||
function_prefix = re.search(
|
||||
r"\bfunction\b[^()\r\n]*\((?P<parameters>[^()]*)$",
|
||||
declaration,
|
||||
)
|
||||
annotation_suffix = re.match(
|
||||
r"[ \t\r\n]*(?P<terminator>[,)=])",
|
||||
text[end:],
|
||||
)
|
||||
if (
|
||||
function_prefix is not None
|
||||
and re.fullmatch(
|
||||
r"\s*(?:\.\.\.\s*)?",
|
||||
split_top_level_call_arguments(
|
||||
function_prefix.group("parameters")
|
||||
)[-1],
|
||||
)
|
||||
and annotation_suffix is not None
|
||||
):
|
||||
if annotation_suffix.group("terminator") != "=":
|
||||
return True
|
||||
return not fallback_secret_risk(
|
||||
text[end + annotation_suffix.end() :],
|
||||
minimum_length=8,
|
||||
)
|
||||
if camel_reference is None and snake_reference is None:
|
||||
return False
|
||||
return bool(
|
||||
@@ -4909,7 +4935,13 @@ def secret_text_risk(text: str) -> bool:
|
||||
return True
|
||||
if (
|
||||
match.group("bare_value") is not None
|
||||
and bare_code_reference(text, match.start(), separator, value)
|
||||
and bare_code_reference(
|
||||
text,
|
||||
match.start(),
|
||||
match.end(),
|
||||
separator,
|
||||
value,
|
||||
)
|
||||
and safe_secret_assignment_suffix(text, match.end())
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -1762,6 +1762,44 @@ class AutoreviewHardeningTests(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_secret_detector_allows_typescript_function_parameter_types(self) -> None:
|
||||
signature = (
|
||||
"function formatCredentialLabel("
|
||||
+ "credential"
|
||||
+ ": ClaudeCliReadableCredential"
|
||||
+ "): string {"
|
||||
)
|
||||
access_key = "AKIA" + "ABCDEFGHIJKLMNOP"
|
||||
secret_assignment = "const api" + 'Key = "' + access_key + '";'
|
||||
literal_value = "actual-production-" + "secret"
|
||||
parameter_name = "api" + "Key"
|
||||
type_name = "Api" + "Credential"
|
||||
typed_default = (
|
||||
"function connect("
|
||||
+ parameter_name
|
||||
+ ": "
|
||||
+ type_name
|
||||
+ ' = "'
|
||||
+ literal_value
|
||||
+ '") {}'
|
||||
)
|
||||
benign_default = (
|
||||
"function connect("
|
||||
+ parameter_name
|
||||
+ ": "
|
||||
+ type_name
|
||||
+ " = defaultCredential) {}"
|
||||
)
|
||||
|
||||
self.assertFalse(self.helper["secret_text_risk"](signature))
|
||||
self.assertFalse(self.helper["secret_text_risk"](benign_default))
|
||||
self.assertTrue(
|
||||
self.helper["secret_text_risk"](
|
||||
signature + "\n " + secret_assignment + "\n}"
|
||||
)
|
||||
)
|
||||
self.assertTrue(self.helper["secret_text_risk"](typed_default))
|
||||
|
||||
def test_secret_detector_handles_punctuation_and_multiline_diff_values(self) -> None:
|
||||
value = "Correct-Horse!" + "@Battery$Staple"
|
||||
patch = (
|
||||
|
||||
@@ -183,7 +183,7 @@ struct WatchChatStatusLocalizationTests {
|
||||
],
|
||||
"gatewayConnected": false,
|
||||
"agentName": "Main",
|
||||
"agentAvatarURL": "https://example.com/avatar.png",
|
||||
"agentAvatarUrl": "https://example.com/avatar.png",
|
||||
"sessionKey": "main",
|
||||
"talkStatus": [
|
||||
"code": OpenClawWatchAppStatusCode.talkOff.rawValue,
|
||||
|
||||
@@ -6904,6 +6904,7 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi
|
||||
gatewayStatusText: "Connected",
|
||||
gatewayConnected: true,
|
||||
agentName: "Main",
|
||||
agentAvatarURL: "https://example.com/avatar.png",
|
||||
sessionKey: "main",
|
||||
gatewayStableID: "gateway-a",
|
||||
talkStatus: OpenClawWatchAppStatus(code: .talkOff),
|
||||
@@ -6941,6 +6942,8 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi
|
||||
let nestedApprovals = try #require(
|
||||
combined[OpenClawWatchPayloadType.execApprovalSnapshot.rawValue] as? [String: Any])
|
||||
#expect(nestedApp["gatewayStableID"] as? String == "gateway-a")
|
||||
#expect(nestedApp["agentAvatarUrl"] as? String == "https://example.com/avatar.png")
|
||||
#expect(nestedApp["agentAvatarURL"] == nil)
|
||||
let nestedChatStatus = try #require(nestedApp["chatStatus"] as? [String: Any])
|
||||
#expect(nestedChatStatus["code"] as? String == "chatConnectIPhone")
|
||||
#expect(nestedApp["chatStatusCode"] == nil)
|
||||
|
||||
@@ -541,7 +541,7 @@ struct WatchAppSnapshotMessage: Codable, Equatable {
|
||||
let gatewayConnected = Self.boolValue(payload["gatewayConnected"])
|
||||
let agentName = (payload["agentName"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let agentAvatarURL = (payload["agentAvatarURL"] as? String)?
|
||||
let agentAvatarURL = (payload["agentAvatarUrl"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let agentAvatarText = (payload["agentAvatarText"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
@@ -53,18 +53,24 @@ enum RuntimeResolutionError: Error {
|
||||
|
||||
enum RuntimeLocator {
|
||||
private static let logger = Logger(subsystem: "ai.openclaw", category: "runtime")
|
||||
private static let minNode22 = RuntimeVersion(major: 22, minor: 19, patch: 0)
|
||||
private static let minNode23 = RuntimeVersion(major: 23, minor: 11, patch: 0)
|
||||
private static let supportedNodeRange = ">=22.19.0 <23 or >=23.11.0"
|
||||
// Keep these floors aligned with package.json engines so the app never launches
|
||||
// the gateway on an unsupported odd release or an older even-major runtime.
|
||||
private static let minNode22 = RuntimeVersion(major: 22, minor: 22, patch: 3)
|
||||
private static let minNode24 = RuntimeVersion(major: 24, minor: 15, patch: 0)
|
||||
private static let minNode25 = RuntimeVersion(major: 25, minor: 9, patch: 0)
|
||||
private static let supportedNodeRange = ">=22.22.3 <23, >=24.15.0 <25, or >=25.9.0"
|
||||
|
||||
static func isSupportedNodeVersion(_ version: RuntimeVersion) -> Bool {
|
||||
if version.major == self.minNode22.major {
|
||||
return version >= self.minNode22
|
||||
}
|
||||
if version.major == self.minNode23.major {
|
||||
return version >= self.minNode23
|
||||
if version.major == self.minNode24.major {
|
||||
return version >= self.minNode24
|
||||
}
|
||||
return version.major > self.minNode23.major
|
||||
if version.major == self.minNode25.major {
|
||||
return version >= self.minNode25
|
||||
}
|
||||
return version.major > self.minNode25.major
|
||||
}
|
||||
|
||||
static func resolve(
|
||||
|
||||
@@ -67,7 +67,7 @@ import Testing
|
||||
let nodePath = tmp.appendingPathComponent("node_modules/.bin/node")
|
||||
let scriptPath = tmp.appendingPathComponent("bin/openclaw.js")
|
||||
try makeExecutableForTests(at: nodePath)
|
||||
try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
|
||||
try "#!/bin/sh\necho v22.22.3\n".write(to: nodePath, atomically: true, encoding: .utf8)
|
||||
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
|
||||
try makeExecutableForTests(at: scriptPath)
|
||||
|
||||
@@ -162,7 +162,7 @@ import Testing
|
||||
let binDir = tmp.appendingPathComponent("bin")
|
||||
let nodePath = binDir.appendingPathComponent("node")
|
||||
try makeExecutableForTests(at: nodePath)
|
||||
try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
|
||||
try "#!/bin/sh\necho v22.22.3\n".write(to: nodePath, atomically: true, encoding: .utf8)
|
||||
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
|
||||
|
||||
let cmd = CommandResolver.openclawCommand(
|
||||
@@ -215,7 +215,7 @@ import Testing
|
||||
|
||||
@Test func `node manager runtimes precede system runtimes`() throws {
|
||||
let home = try makeTempDirForTests()
|
||||
let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.19.0/bin")
|
||||
let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.22.3/bin")
|
||||
try makeExecutableForTests(at: nodeManagerBin.appendingPathComponent("node"))
|
||||
|
||||
let paths = CommandResolver.preferredPaths(
|
||||
|
||||
@@ -16,7 +16,7 @@ struct RuntimeLocatorTests {
|
||||
@Test func `resolve succeeds with valid node`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
echo v22.19.0
|
||||
echo v22.22.3
|
||||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
@@ -25,13 +25,13 @@ struct RuntimeLocatorTests {
|
||||
return
|
||||
}
|
||||
#expect(res.path == node.path)
|
||||
#expect(res.version == RuntimeVersion(major: 22, minor: 19, patch: 0))
|
||||
#expect(res.version == RuntimeVersion(major: 22, minor: 22, patch: 3))
|
||||
}
|
||||
|
||||
@Test func `resolve fails on boundary below minimum`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
echo v22.18.9
|
||||
echo v22.22.2
|
||||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
@@ -39,38 +39,38 @@ struct RuntimeLocatorTests {
|
||||
Issue.record("Expected unsupported error, got \(result)")
|
||||
return
|
||||
}
|
||||
#expect(found == RuntimeVersion(major: 22, minor: 18, patch: 9))
|
||||
#expect(found == RuntimeVersion(major: 22, minor: 22, patch: 2))
|
||||
#expect(path == node.path)
|
||||
}
|
||||
|
||||
@Test func `resolve rejects early node 23`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
echo v23.7.0
|
||||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
guard case let .failure(.unsupported(_, found, path, _)) = result else {
|
||||
Issue.record("Expected unsupported error, got \(result)")
|
||||
return
|
||||
}
|
||||
#expect(found == RuntimeVersion(major: 23, minor: 7, patch: 0))
|
||||
#expect(path == node.path)
|
||||
}
|
||||
|
||||
@Test func `resolve accepts node 23 with statement columns`() throws {
|
||||
@Test func `resolve rejects node 23`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
echo v23.11.0
|
||||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
guard case let .success(res) = result else {
|
||||
Issue.record("Expected success, got \(result)")
|
||||
guard case let .failure(.unsupported(_, found, path, _)) = result else {
|
||||
Issue.record("Expected unsupported error, got \(result)")
|
||||
return
|
||||
}
|
||||
#expect(res.path == node.path)
|
||||
#expect(res.version == RuntimeVersion(major: 23, minor: 11, patch: 0))
|
||||
#expect(found == RuntimeVersion(major: 23, minor: 11, patch: 0))
|
||||
#expect(path == node.path)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
("22.22.2", false),
|
||||
("22.22.3", true),
|
||||
("23.11.0", false),
|
||||
("24.14.1", false),
|
||||
("24.15.0", true),
|
||||
("25.8.1", false),
|
||||
("25.9.0", true),
|
||||
("26.0.0", true),
|
||||
])
|
||||
func `node support matches the core runtime contract`(version: String, supported: Bool) throws {
|
||||
let parsed = try #require(RuntimeVersion.from(string: version))
|
||||
#expect(RuntimeLocator.isSupportedNodeVersion(parsed) == supported)
|
||||
}
|
||||
|
||||
@Test func `resolve fails when too old`() throws {
|
||||
@@ -105,7 +105,7 @@ struct RuntimeLocatorTests {
|
||||
|
||||
@Test func `describe failure includes paths`() {
|
||||
let msg = RuntimeLocator.describeFailure(.notFound(searchPaths: ["/tmp/a", "/tmp/b"]))
|
||||
#expect(msg.contains("Node >=22.19.0 <23 or >=23.11.0"))
|
||||
#expect(msg.contains("Node >=22.22.3 <23, >=24.15.0 <25, or >=25.9.0"))
|
||||
#expect(msg.contains("PATH searched: /tmp/a:/tmp/b"))
|
||||
|
||||
let parseMsg = RuntimeLocator.describeFailure(
|
||||
@@ -114,7 +114,7 @@ struct RuntimeLocatorTests {
|
||||
raw: "garbage",
|
||||
path: "/usr/local/bin/node",
|
||||
searchPaths: ["/usr/local/bin"]))
|
||||
#expect(parseMsg.contains("Node >=22.19.0 <23 or >=23.11.0"))
|
||||
#expect(parseMsg.contains("Node >=22.22.3 <23, >=24.15.0 <25, or >=25.9.0"))
|
||||
}
|
||||
|
||||
@Test func `runtime version parses with leading V and metadata`() {
|
||||
|
||||
@@ -243,7 +243,9 @@ export async function getCachedChromeGraphicsDiagnostics(
|
||||
running.graphicsDiagnosticsPending ??= load();
|
||||
try {
|
||||
const diagnostics = await running.graphicsDiagnosticsPending;
|
||||
running.graphicsDiagnostics = diagnostics;
|
||||
if (diagnostics.status === "available") {
|
||||
running.graphicsDiagnostics = diagnostics;
|
||||
}
|
||||
return diagnostics;
|
||||
} finally {
|
||||
running.graphicsDiagnosticsPending = undefined;
|
||||
|
||||
@@ -339,6 +339,60 @@ describe("basic browser routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("retries unavailable graphics diagnostics and caches the first available result", async () => {
|
||||
const unavailable = {
|
||||
status: "unavailable",
|
||||
observedAt: 123,
|
||||
reason: "SystemInfo.getInfo timed out",
|
||||
} as const;
|
||||
const available = {
|
||||
status: "available",
|
||||
observedAt: 456,
|
||||
acceleration: "hardware",
|
||||
renderer: "ANGLE (Intel)",
|
||||
vendor: "Intel",
|
||||
version: "OpenGL ES 3.0",
|
||||
backend: "(gl=angle,angle=metal)",
|
||||
devices: [],
|
||||
featureStatus: { webgl: "enabled" },
|
||||
disabledFeatures: [],
|
||||
driverBugWorkarounds: [],
|
||||
videoDecoding: [],
|
||||
videoEncoding: [],
|
||||
} as const;
|
||||
inspectChromeGraphicsDiagnosticsMock
|
||||
.mockResolvedValueOnce(unavailable)
|
||||
.mockResolvedValue(available);
|
||||
const state = createManagedProfileState(
|
||||
{},
|
||||
{
|
||||
isHttpReachable: async () => true,
|
||||
isTransportAvailable: async () => true,
|
||||
},
|
||||
);
|
||||
const profile = (state.forProfile() as { profile: unknown }).profile as never;
|
||||
state.profiles.set("openclaw", {
|
||||
profile,
|
||||
running: {
|
||||
pid: 222,
|
||||
exe: { kind: "chromium", path: "/usr/bin/chromium" },
|
||||
userDataDir: "/tmp/openclaw-profile",
|
||||
cdpPort: 18800,
|
||||
startedAt: Date.now(),
|
||||
proc: {} as never,
|
||||
},
|
||||
});
|
||||
|
||||
const first = await callBasicRouteWithState({ query: { profile: "openclaw" }, state });
|
||||
const second = await callBasicRouteWithState({ query: { profile: "openclaw" }, state });
|
||||
const third = await callBasicRouteWithState({ query: { profile: "openclaw" }, state });
|
||||
|
||||
expect(responseBodyRecord(first).graphics).toEqual(unavailable);
|
||||
expect(responseBodyRecord(second).graphics).toEqual(available);
|
||||
expect(responseBodyRecord(third).graphics).toEqual(available);
|
||||
expect(inspectChromeGraphicsDiagnosticsMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not inspect graphics while the managed process is pending reconcile", async () => {
|
||||
const state = createManagedProfileState(
|
||||
{},
|
||||
|
||||
@@ -167,6 +167,39 @@ describe("local audio selection", () => {
|
||||
capableBackend: "metal",
|
||||
observedBackend: "metal",
|
||||
});
|
||||
|
||||
for (const failedBackend of ["Metal", "MTL0", "CUDA0"]) {
|
||||
expect(
|
||||
recordLocalAudioBackendObservation({
|
||||
command: "whisper-cli",
|
||||
args: ["-m", modelPath, "-otxt", "-of", "{{OutputBase}}", "-nt", "{{MediaPath}}"],
|
||||
output: [
|
||||
`whisper_backend_init_gpu: using ${failedBackend} backend`,
|
||||
`whisper_backend_init_gpu: failed to initialize ${failedBackend} backend`,
|
||||
].join("\n"),
|
||||
}),
|
||||
).toBe("cpu");
|
||||
const failedAccelerationSelection = await inspectLocalAudioSelection({
|
||||
env: {
|
||||
WHISPER_CPP_MODEL: modelPath,
|
||||
SHERPA_ONNX_MODEL_DIR: sherpaDir,
|
||||
},
|
||||
platform: "darwin",
|
||||
arch: "arm64",
|
||||
resolveBinary: async (name) =>
|
||||
name === "whisper-cli"
|
||||
? "/opt/homebrew/bin/whisper-cli"
|
||||
: name === "sherpa-onnx-offline"
|
||||
? "/usr/local/bin/sherpa-onnx-offline"
|
||||
: null,
|
||||
resolveRealpath: async () => "/opt/homebrew/Cellar/whisper-cpp/1.9.1/bin/whisper-cli",
|
||||
inspectLinkedLibraries: async () => null,
|
||||
});
|
||||
expect(failedAccelerationSelection.selected).toMatchObject({ id: "sherpa-onnx-offline" });
|
||||
expect(
|
||||
failedAccelerationSelection.candidates.find((candidate) => candidate.id === "whisper-cli"),
|
||||
).toMatchObject({ observedBackend: "cpu" });
|
||||
}
|
||||
});
|
||||
|
||||
it("reports Parakeet as MLX-capable without treating capability as observation", async () => {
|
||||
|
||||
@@ -91,13 +91,17 @@ export function recordLocalAudioBackendObservation(params: {
|
||||
if (commandId(params.command) !== "whisper-cli") {
|
||||
return undefined;
|
||||
}
|
||||
const backend = /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output)
|
||||
? "metal"
|
||||
: /using\s+CUDA\d*\s+backend/i.test(params.output)
|
||||
? "cuda"
|
||||
: /using\s+CPU\s+backend|no GPU found/i.test(params.output)
|
||||
? "cpu"
|
||||
: undefined;
|
||||
const acceleratorInitializationFailed =
|
||||
/failed to initialize\s+(?:MTL\d+|Metal|CUDA\d*)\s+backend/i.test(params.output);
|
||||
const backend = acceleratorInitializationFailed
|
||||
? "cpu"
|
||||
: /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output)
|
||||
? "metal"
|
||||
: /using\s+CUDA\d*\s+backend/i.test(params.output)
|
||||
? "cuda"
|
||||
: /using\s+CPU\s+backend|no GPU found/i.test(params.output)
|
||||
? "cpu"
|
||||
: undefined;
|
||||
if (backend) {
|
||||
observedBackendCache.set(observationKey(params), backend);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user