mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(android): add Wear proxy protocol (#108835)
* feat(android): add Wear proxy protocol Introduce a bounded, versioned phone/watch RPC contract and wire its test, lint, and build gates before either runtime endpoint lands. Co-authored-by: Sebastian Schubotz <git@sibbl.de> * test(android): lock Wear wire names Cover every supported RPC method and event discriminator so phone and watch cannot silently drift. Co-authored-by: Sebastian Schubotz <git@sibbl.net> * fix(android): bound Wear JSON depth Reject excessively nested Data Layer envelopes before kotlinx.serialization can recurse through them. * fix(android): enforce Wear depth on encode Keep outbound messages inside the same JSON nesting contract enforced by the decoder. * fix(android): preflight Wear payload depth Traverse arbitrary JSON payloads iteratively before serialization so deeply nested local trees fail safely. --------- Co-authored-by: Sebastian Schubotz <git@sibbl.de> Co-authored-by: Sebastian Schubotz <git@sibbl.net>
This commit is contained in:
committed by
GitHub
parent
c11d112b63
commit
0815f6a4f9
@@ -2897,7 +2897,9 @@ jobs:
|
||||
set -euo pipefail
|
||||
case "$TASK" in
|
||||
test-play)
|
||||
./gradlew --no-daemon --build-cache :app:testPlayDebugUnitTest
|
||||
./gradlew --no-daemon --build-cache \
|
||||
:app:testPlayDebugUnitTest \
|
||||
:wear-shared:testDebugUnitTest
|
||||
;;
|
||||
test-third-party)
|
||||
./gradlew --no-daemon --build-cache :app:testThirdPartyDebugUnitTest
|
||||
@@ -2908,7 +2910,9 @@ jobs:
|
||||
:app:assembleThirdPartyDebug \
|
||||
:app:lintPlayDebug \
|
||||
:app:lintThirdPartyDebug \
|
||||
:benchmark:assembleDebug
|
||||
:benchmark:assembleDebug \
|
||||
:wear-shared:assembleDebug \
|
||||
:wear-shared:lintDebug
|
||||
;;
|
||||
build-play-compat)
|
||||
# Frozen targets keep their target-owned Android build contract. New lint rules
|
||||
@@ -2917,7 +2921,10 @@ jobs:
|
||||
;;
|
||||
ktlint)
|
||||
# Mirrors `pnpm android:lint`; keeps formatting drift out of main (see PR #100304 sweep).
|
||||
./gradlew --no-daemon --build-cache :app:ktlintCheck :benchmark:ktlintCheck
|
||||
./gradlew --no-daemon --build-cache \
|
||||
:app:ktlintCheck \
|
||||
:benchmark:ktlintCheck \
|
||||
:wear-shared:ktlintCheck
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported Android task: $TASK" >&2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.android.test) apply false
|
||||
alias(libs.plugins.ktlint) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
|
||||
@@ -80,6 +80,7 @@ robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectr
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
android-test = { id = "com.android.test", version.ref = "agp" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
@@ -17,3 +17,4 @@ dependencyResolutionManagement {
|
||||
rootProject.name = "OpenClawNodeAndroid"
|
||||
include(":app")
|
||||
include(":benchmark")
|
||||
include(":wear-shared")
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ai.openclaw.wear.shared"
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 31
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
lint {
|
||||
warningsAsErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
android.set(true)
|
||||
ignoreFailures.set(false)
|
||||
filter {
|
||||
exclude("**/build/**")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.kotlinx.serialization.json)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
@@ -0,0 +1,258 @@
|
||||
package ai.openclaw.wear.shared
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import java.nio.charset.CharacterCodingException
|
||||
|
||||
object WearProtocol {
|
||||
const val VERSION = 1
|
||||
const val REQUEST_PATH = "/openclaw/wear/v1/request"
|
||||
const val RESPONSE_PATH = "/openclaw/wear/v1/response"
|
||||
const val EVENT_PATH = "/openclaw/wear/v1/event"
|
||||
|
||||
// MessageClient has a 100 KiB ceiling. Keep headroom for transport metadata and
|
||||
// force transcript pagination instead of depending on an edge-sized message.
|
||||
const val MAX_MESSAGE_BYTES = 64 * 1024
|
||||
|
||||
// Bound recursive JSON parsing at the untrusted Data Layer boundary.
|
||||
const val MAX_JSON_DEPTH = 32
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class WearRpcMethod {
|
||||
@SerialName("proxy.status")
|
||||
ProxyStatus,
|
||||
|
||||
@SerialName("sessions.list")
|
||||
SessionsList,
|
||||
|
||||
@SerialName("chat.history")
|
||||
ChatHistory,
|
||||
|
||||
@SerialName("chat.send")
|
||||
ChatSend,
|
||||
|
||||
@SerialName("chat.abort")
|
||||
ChatAbort,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class WearEventType {
|
||||
@SerialName("chat")
|
||||
Chat,
|
||||
|
||||
@SerialName("connection")
|
||||
Connection,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed interface WearMessage {
|
||||
val version: Int
|
||||
|
||||
@Serializable
|
||||
@SerialName("request")
|
||||
data class Request(
|
||||
override val version: Int = WearProtocol.VERSION,
|
||||
val requestId: String,
|
||||
val method: WearRpcMethod,
|
||||
val params: JsonObject = buildJsonObject {},
|
||||
) : WearMessage
|
||||
|
||||
@Serializable
|
||||
@SerialName("response")
|
||||
data class Response(
|
||||
override val version: Int = WearProtocol.VERSION,
|
||||
val requestId: String,
|
||||
val ok: Boolean,
|
||||
val result: JsonElement? = null,
|
||||
val error: WearRpcError? = null,
|
||||
) : WearMessage
|
||||
|
||||
@Serializable
|
||||
@SerialName("event")
|
||||
data class Event(
|
||||
override val version: Int = WearProtocol.VERSION,
|
||||
val sequence: Long,
|
||||
val event: WearEventType,
|
||||
val payload: JsonElement? = null,
|
||||
) : WearMessage
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class WearRpcError(
|
||||
val code: String,
|
||||
val message: String,
|
||||
)
|
||||
|
||||
enum class WearDecodeFailureReason {
|
||||
Empty,
|
||||
TooLarge,
|
||||
TooDeep,
|
||||
Malformed,
|
||||
UnsupportedVersion,
|
||||
InvalidEnvelope,
|
||||
}
|
||||
|
||||
sealed interface WearDecodeResult {
|
||||
data class Success(
|
||||
val message: WearMessage,
|
||||
) : WearDecodeResult
|
||||
|
||||
data class Failure(
|
||||
val reason: WearDecodeFailureReason,
|
||||
) : WearDecodeResult
|
||||
}
|
||||
|
||||
object WearProtocolCodec {
|
||||
private val json =
|
||||
Json {
|
||||
classDiscriminator = "type"
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
fun encode(message: WearMessage): ByteArray {
|
||||
requireValid(message)
|
||||
require(hasValidPayloadDepth(message)) {
|
||||
"Wear message exceeds JSON depth ${WearProtocol.MAX_JSON_DEPTH}"
|
||||
}
|
||||
val encoded = json.encodeToString(WearMessage.serializer(), message)
|
||||
require(!exceedsJsonDepth(encoded)) {
|
||||
"Wear message exceeds JSON depth ${WearProtocol.MAX_JSON_DEPTH}"
|
||||
}
|
||||
val bytes =
|
||||
encoded.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
require(bytes.size <= WearProtocol.MAX_MESSAGE_BYTES) {
|
||||
"Wear message exceeds ${WearProtocol.MAX_MESSAGE_BYTES} bytes"
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
private fun hasValidPayloadDepth(message: WearMessage): Boolean {
|
||||
val payloads =
|
||||
when (message) {
|
||||
is WearMessage.Request -> listOf(message.params)
|
||||
is WearMessage.Response -> listOfNotNull(message.result)
|
||||
is WearMessage.Event -> listOfNotNull(message.payload)
|
||||
}
|
||||
return payloads.all { element -> hasValidElementDepth(element, parentDepth = 1) }
|
||||
}
|
||||
|
||||
private fun hasValidElementDepth(
|
||||
element: JsonElement,
|
||||
parentDepth: Int,
|
||||
): Boolean {
|
||||
val pending = ArrayDeque<Pair<JsonElement, Int>>()
|
||||
pending.addLast(element to parentDepth)
|
||||
while (pending.isNotEmpty()) {
|
||||
val (current, parent) = pending.removeLast()
|
||||
val children =
|
||||
when (current) {
|
||||
is JsonArray -> current
|
||||
is JsonObject -> current.values
|
||||
else -> continue
|
||||
}
|
||||
val depth = parent + 1
|
||||
if (depth > WearProtocol.MAX_JSON_DEPTH) return false
|
||||
children.forEach { child -> pending.addLast(child to depth) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun decode(bytes: ByteArray): WearDecodeResult {
|
||||
if (bytes.isEmpty()) return WearDecodeResult.Failure(WearDecodeFailureReason.Empty)
|
||||
if (bytes.size > WearProtocol.MAX_MESSAGE_BYTES) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.TooLarge)
|
||||
}
|
||||
|
||||
val text =
|
||||
try {
|
||||
bytes.decodeToString(throwOnInvalidSequence = true)
|
||||
} catch (_: CharacterCodingException) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.Malformed)
|
||||
}
|
||||
if (exceedsJsonDepth(text)) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.TooDeep)
|
||||
}
|
||||
val root =
|
||||
try {
|
||||
json.parseToJsonElement(text).jsonObject
|
||||
} catch (_: SerializationException) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.Malformed)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.Malformed)
|
||||
}
|
||||
val version =
|
||||
(root["version"] as? JsonPrimitive)?.intOrNull
|
||||
?: return WearDecodeResult.Failure(WearDecodeFailureReason.Malformed)
|
||||
if (version != WearProtocol.VERSION) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.UnsupportedVersion)
|
||||
}
|
||||
val message =
|
||||
try {
|
||||
json.decodeFromJsonElement(WearMessage.serializer(), root)
|
||||
} catch (_: SerializationException) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.Malformed)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.Malformed)
|
||||
}
|
||||
if (!isValid(message)) {
|
||||
return WearDecodeResult.Failure(WearDecodeFailureReason.InvalidEnvelope)
|
||||
}
|
||||
return WearDecodeResult.Success(message)
|
||||
}
|
||||
|
||||
private fun exceedsJsonDepth(text: String): Boolean {
|
||||
var depth = 0
|
||||
var inString = false
|
||||
var escaped = false
|
||||
for (character in text) {
|
||||
if (inString) {
|
||||
when {
|
||||
escaped -> escaped = false
|
||||
character == '\\' -> escaped = true
|
||||
character == '"' -> inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
when (character) {
|
||||
'"' -> inString = true
|
||||
'{', '[' -> {
|
||||
depth += 1
|
||||
if (depth > WearProtocol.MAX_JSON_DEPTH) return true
|
||||
}
|
||||
'}', ']' -> depth -= 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun requireValid(message: WearMessage) {
|
||||
require(message.version == WearProtocol.VERSION) { "Unsupported Wear protocol version: ${message.version}" }
|
||||
require(isValid(message)) { "Invalid Wear protocol envelope" }
|
||||
}
|
||||
|
||||
private fun isValid(message: WearMessage): Boolean =
|
||||
when (message) {
|
||||
is WearMessage.Request -> message.requestId.isNotBlank()
|
||||
is WearMessage.Response ->
|
||||
message.requestId.isNotBlank() &&
|
||||
if (message.ok) {
|
||||
message.error == null
|
||||
} else {
|
||||
message.error != null && message.result == null && message.error.code.isNotBlank()
|
||||
}
|
||||
is WearMessage.Event -> message.sequence >= 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package ai.openclaw.wear.shared
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class WearProtocolTest {
|
||||
@Test
|
||||
fun roundTripsEveryEnvelopeKind() {
|
||||
val messages =
|
||||
listOf(
|
||||
WearMessage.Request(
|
||||
requestId = "req-1",
|
||||
method = WearRpcMethod.ChatHistory,
|
||||
params = buildJsonObject { put("sessionKey", "main") },
|
||||
),
|
||||
WearMessage.Response(
|
||||
requestId = "req-1",
|
||||
ok = true,
|
||||
result = buildJsonObject { put("count", 2) },
|
||||
),
|
||||
WearMessage.Response(
|
||||
requestId = "req-2",
|
||||
ok = false,
|
||||
error = WearRpcError(code = "unavailable", message = "Phone offline"),
|
||||
),
|
||||
WearMessage.Event(
|
||||
sequence = 7,
|
||||
event = WearEventType.Chat,
|
||||
payload = buildJsonObject { put("state", "delta") },
|
||||
),
|
||||
)
|
||||
|
||||
messages.forEach { message ->
|
||||
assertEquals(WearDecodeResult.Success(message), WearProtocolCodec.decode(WearProtocolCodec.encode(message)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun usesStableWireNamesAndPaths() {
|
||||
val methodNames =
|
||||
mapOf(
|
||||
WearRpcMethod.ProxyStatus to "proxy.status",
|
||||
WearRpcMethod.SessionsList to "sessions.list",
|
||||
WearRpcMethod.ChatHistory to "chat.history",
|
||||
WearRpcMethod.ChatSend to "chat.send",
|
||||
WearRpcMethod.ChatAbort to "chat.abort",
|
||||
)
|
||||
methodNames.forEach { (method, wireName) ->
|
||||
val request = WearMessage.Request(requestId = "req-1", method = method)
|
||||
val root = Json.parseToJsonElement(WearProtocolCodec.encode(request).decodeToString()).jsonObject
|
||||
assertEquals("request", root.getValue("type").jsonPrimitive.content)
|
||||
assertEquals(wireName, root.getValue("method").jsonPrimitive.content)
|
||||
}
|
||||
|
||||
val eventNames =
|
||||
mapOf(
|
||||
WearEventType.Chat to "chat",
|
||||
WearEventType.Connection to "connection",
|
||||
)
|
||||
eventNames.forEach { (event, wireName) ->
|
||||
val message = WearMessage.Event(sequence = 1, event = event)
|
||||
val root = Json.parseToJsonElement(WearProtocolCodec.encode(message).decodeToString()).jsonObject
|
||||
assertEquals("event", root.getValue("type").jsonPrimitive.content)
|
||||
assertEquals(wireName, root.getValue("event").jsonPrimitive.content)
|
||||
}
|
||||
|
||||
assertEquals("/openclaw/wear/v1/request", WearProtocol.REQUEST_PATH)
|
||||
assertEquals("/openclaw/wear/v1/response", WearProtocol.RESPONSE_PATH)
|
||||
assertEquals("/openclaw/wear/v1/event", WearProtocol.EVENT_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresUnknownFieldsWithinCurrentVersion() {
|
||||
val bytes =
|
||||
"""{"type":"request","version":1,"requestId":"req-1","method":"proxy.status","params":{},"future":true}"""
|
||||
.encodeToByteArray()
|
||||
|
||||
assertEquals(
|
||||
WearDecodeResult.Success(
|
||||
WearMessage.Request(requestId = "req-1", method = WearRpcMethod.ProxyStatus),
|
||||
),
|
||||
WearProtocolCodec.decode(bytes),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsMalformedUnsupportedAndInvalidMessages() {
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.Empty),
|
||||
WearProtocolCodec.decode(byteArrayOf()),
|
||||
)
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.Malformed),
|
||||
WearProtocolCodec.decode("not-json".encodeToByteArray()),
|
||||
)
|
||||
val invalidUtf8 =
|
||||
"""{"type":"request","version":1,"requestId":"""".encodeToByteArray() +
|
||||
byteArrayOf(0xc3.toByte(), 0x28) +
|
||||
"""","method":"proxy.status","params":{}}""".encodeToByteArray()
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.Malformed),
|
||||
WearProtocolCodec.decode(invalidUtf8),
|
||||
)
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.UnsupportedVersion),
|
||||
WearProtocolCodec.decode(
|
||||
"""{"type":"future-message","version":2,"futureRequiredField":true}"""
|
||||
.encodeToByteArray(),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.InvalidEnvelope),
|
||||
WearProtocolCodec.decode(
|
||||
"""{"type":"response","version":1,"requestId":"req-1","ok":false}""".encodeToByteArray(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsOversizedMessagesOnEncodeAndDecode() {
|
||||
val oversizedBytes = ByteArray(WearProtocol.MAX_MESSAGE_BYTES + 1)
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.TooLarge),
|
||||
WearProtocolCodec.decode(oversizedBytes),
|
||||
)
|
||||
|
||||
val oversizedMessage =
|
||||
WearMessage.Request(
|
||||
requestId = "req-1",
|
||||
method = WearRpcMethod.ChatSend,
|
||||
params = buildJsonObject { put("message", "x".repeat(WearProtocol.MAX_MESSAGE_BYTES)) },
|
||||
)
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
WearProtocolCodec.encode(oversizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsExcessiveJsonDepthBeforeParsing() {
|
||||
val nesting = WearProtocol.MAX_JSON_DEPTH + 1
|
||||
val deeplyNested =
|
||||
"""{"type":"request","version":1,"requestId":"req-1","method":"chat.send","params":{"payload":${"[".repeat(nesting)}0${"]".repeat(nesting)}}}"""
|
||||
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.TooDeep),
|
||||
WearProtocolCodec.decode(deeplyNested.encodeToByteArray()),
|
||||
)
|
||||
|
||||
val bracketsInString =
|
||||
WearMessage.Request(
|
||||
requestId = "req-2",
|
||||
method = WearRpcMethod.ChatSend,
|
||||
params = buildJsonObject { put("message", "[".repeat(WearProtocol.MAX_JSON_DEPTH + 1)) },
|
||||
)
|
||||
assertEquals(
|
||||
WearDecodeResult.Success(bracketsInString),
|
||||
WearProtocolCodec.decode(WearProtocolCodec.encode(bracketsInString)),
|
||||
)
|
||||
|
||||
var nestedPayload: JsonElement = JsonPrimitive(0)
|
||||
repeat(4_096) {
|
||||
nestedPayload = JsonArray(listOf(nestedPayload))
|
||||
}
|
||||
val deeplyNestedMessage =
|
||||
WearMessage.Request(
|
||||
requestId = "req-3",
|
||||
method = WearRpcMethod.ChatSend,
|
||||
params = buildJsonObject { put("payload", nestedPayload) },
|
||||
)
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
WearProtocolCodec.encode(deeplyNestedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodingIsDeterministic() {
|
||||
val message = WearMessage.Request(requestId = "req-1", method = WearRpcMethod.SessionsList)
|
||||
assertArrayEquals(WearProtocolCodec.encode(message), WearProtocolCodec.encode(message))
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1505,14 +1505,14 @@
|
||||
"./cli-entry": "./openclaw.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"android:assemble": "node scripts/run-android-gradle.mjs :app:assemblePlayDebug",
|
||||
"android:assemble": "node scripts/run-android-gradle.mjs :app:assemblePlayDebug :wear-shared:assembleDebug",
|
||||
"android:assemble:third-party": "node scripts/run-android-gradle.mjs :app:assembleThirdPartyDebug",
|
||||
"android:bundle:release": "bash -lc 'source ./scripts/lib/android-fastlane.sh && cd apps/android && run_android_fastlane android play_store_archive'",
|
||||
"android:format": "cd apps/android && ./gradlew :app:ktlintFormat :benchmark:ktlintFormat",
|
||||
"android:format": "cd apps/android && ./gradlew :app:ktlintFormat :benchmark:ktlintFormat :wear-shared:ktlintFormat",
|
||||
"android:install": "node scripts/run-android-gradle.mjs :app:installPlayDebug",
|
||||
"android:install:third-party": "node scripts/run-android-gradle.mjs :app:installThirdPartyDebug",
|
||||
"android:lint": "cd apps/android && ./gradlew :app:ktlintCheck :benchmark:ktlintCheck",
|
||||
"android:lint:android": "node scripts/run-android-gradle.mjs :app:lintPlayDebug :app:lintThirdPartyDebug",
|
||||
"android:lint": "cd apps/android && ./gradlew :app:ktlintCheck :benchmark:ktlintCheck :wear-shared:ktlintCheck",
|
||||
"android:lint:android": "node scripts/run-android-gradle.mjs :app:lintPlayDebug :app:lintThirdPartyDebug :wear-shared:lintDebug",
|
||||
"android:run": "node scripts/run-android-gradle.mjs :app:installPlayDebug -- adb shell am start -n ai.openclaw.app/.MainActivity",
|
||||
"android:run:third-party": "node scripts/run-android-gradle.mjs :app:installThirdPartyDebug -- adb shell am start -n ai.openclaw.app/.MainActivity",
|
||||
"android:release": "bash scripts/android-release.sh",
|
||||
@@ -1526,7 +1526,7 @@
|
||||
"android:release:signing:sync:push": "bash -lc 'source ./scripts/lib/android-fastlane.sh && cd apps/android && run_android_fastlane android signing_sync_push'",
|
||||
"android:release:upload": "bash scripts/android-release-upload.sh",
|
||||
"android:screenshots": "bash scripts/android-screenshots.sh",
|
||||
"android:test": "node scripts/run-android-gradle.mjs :app:testPlayDebugUnitTest",
|
||||
"android:test": "node scripts/run-android-gradle.mjs :app:testPlayDebugUnitTest :wear-shared:testDebugUnitTest",
|
||||
"android:test:integration": "node scripts/run-with-env.mjs OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
|
||||
"android:test:third-party": "node scripts/run-android-gradle.mjs :app:testThirdPartyDebugUnitTest",
|
||||
"android:version": "node --import tsx scripts/android-version.ts --json",
|
||||
|
||||
Reference in New Issue
Block a user