mirror of
https://github.com/BrendanGreenlee/openclaw-android-heartbeat.git
synced 2026-08-17 16:49:14 +00:00
Import official OpenClaw Android app (apps/android @ 71a59512ba476df3328cf485d84748121cf341f2)
Pristine fork source for PROJ-0088. v1 will turn this into a WebView web shell; the WebSocket node infrastructure stays intact for v2 heartbeat.
This commit is contained in:
commit
7a380c40ed
656 changed files with 209982 additions and 0 deletions
|
|
@ -0,0 +1,276 @@
|
|||
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.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class WearProtocolTest {
|
||||
@Test
|
||||
fun realtimeTalkSnapshotCarriesAttemptCorrelation() {
|
||||
val snapshot =
|
||||
WearRealtimeTalkSnapshot(
|
||||
attemptId = "attempt-7",
|
||||
active = true,
|
||||
listening = true,
|
||||
status = WearRealtimeTalkStatus.LISTENING,
|
||||
)
|
||||
|
||||
assertEquals(snapshot, WearRealtimeTalkCodec.decode(WearRealtimeTalkCodec.encode(snapshot)))
|
||||
}
|
||||
|
||||
@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) },
|
||||
eventStreamId = "phone-process-1",
|
||||
eventSequence = 7,
|
||||
),
|
||||
WearMessage.Response(
|
||||
requestId = "req-2",
|
||||
ok = false,
|
||||
error = WearRpcError(code = "unavailable", message = "Phone offline"),
|
||||
),
|
||||
WearMessage.Event(
|
||||
streamId = "phone-process-1",
|
||||
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.AgentsList to "agents.list",
|
||||
WearRpcMethod.AgentsSelect to "agents.select",
|
||||
WearRpcMethod.ModelsList to "models.list",
|
||||
WearRpcMethod.ModelsSelect to "models.select",
|
||||
WearRpcMethod.GatewayConnect to "gateway.connect",
|
||||
WearRpcMethod.GatewayDisconnect to "gateway.disconnect",
|
||||
WearRpcMethod.ChatHistory to "chat.history",
|
||||
WearRpcMethod.ChatSend to "chat.send",
|
||||
WearRpcMethod.ChatAbort to "chat.abort",
|
||||
WearRpcMethod.TalkStart to "talk.start",
|
||||
WearRpcMethod.TalkStop to "talk.stop",
|
||||
)
|
||||
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",
|
||||
WearEventType.Resync to "resync",
|
||||
WearEventType.Talk to "talk",
|
||||
)
|
||||
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)
|
||||
assertEquals(10_000L, WearProtocol.RPC_REQUEST_TIMEOUT_MILLIS)
|
||||
assertEquals(15_000L, WearProtocol.REALTIME_AUDIO_PENDING_CHANNEL_TIMEOUT_MILLIS)
|
||||
assertTrue(
|
||||
WearProtocol.REALTIME_AUDIO_PENDING_CHANNEL_TIMEOUT_MILLIS >
|
||||
WearProtocol.RPC_REQUEST_TIMEOUT_MILLIS,
|
||||
)
|
||||
val realtimePath = WearProtocol.realtimeAudioChannelPath("attempt-7")
|
||||
assertEquals(
|
||||
"/openclaw/wear/v1/realtime/audio",
|
||||
WearProtocol.LEGACY_REALTIME_AUDIO_CHANNEL_PATH,
|
||||
)
|
||||
assertEquals(
|
||||
"/openclaw/wear/v1/realtime/audio/9804dc90c374fd8e83c9b95a75611f9bec6e0c6ecdcbed5319d6491208417521",
|
||||
realtimePath,
|
||||
)
|
||||
assertEquals(realtimePath, WearProtocol.realtimeAudioChannelPath("attempt-7"))
|
||||
assertTrue(WearProtocol.isRealtimeAudioChannelPath(realtimePath))
|
||||
assertTrue(WearProtocol.isAttemptScopedRealtimeAudioChannelPath(realtimePath))
|
||||
assertTrue(WearProtocol.isRealtimeAudioChannelPath(WearProtocol.LEGACY_REALTIME_AUDIO_CHANNEL_PATH))
|
||||
assertFalse(
|
||||
WearProtocol.isAttemptScopedRealtimeAudioChannelPath(
|
||||
WearProtocol.LEGACY_REALTIME_AUDIO_CHANNEL_PATH,
|
||||
),
|
||||
)
|
||||
assertFalse(WearProtocol.isRealtimeAudioChannelPath("$realtimePath/extra"))
|
||||
assertEquals("openclaw_phone_proxy_v1", WearProtocol.PHONE_CAPABILITY)
|
||||
assertEquals("openclaw_wear_companion_v1", WearProtocol.WATCH_CAPABILITY)
|
||||
assertEquals("gateway_offline", WearConnectionFailure.GatewayOffline.wireValue)
|
||||
assertEquals("incompatible", WearConnectionFailure.Incompatible.wireValue)
|
||||
assertEquals(
|
||||
WearConnectionFailure.Incompatible,
|
||||
WearConnectionFailure.fromWireValue("incompatible"),
|
||||
)
|
||||
assertEquals(null, WearConnectionFailure.fromWireValue("future-failure"))
|
||||
assertEquals("agent-controls", WearProxyCapability.AgentControls.wireValue)
|
||||
assertEquals("gateway-controls", WearProxyCapability.GatewayControls.wireValue)
|
||||
assertEquals("model-controls", WearProxyCapability.ModelControls.wireValue)
|
||||
assertEquals("session-selection-lookup", WearProxyCapability.SessionSelectionLookup.wireValue)
|
||||
assertEquals(
|
||||
"attempt-scoped-realtime-audio",
|
||||
WearProxyCapability.AttemptScopedRealtimeAudio.wireValue,
|
||||
)
|
||||
assertEquals(WearProxyCapability.AgentControls, WearProxyCapability.fromWireValue("agent-controls"))
|
||||
assertEquals(null, WearProxyCapability.fromWireValue("future-capability"))
|
||||
}
|
||||
|
||||
@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(),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.InvalidEnvelope),
|
||||
WearProtocolCodec.decode(
|
||||
"""{"type":"event","version":1,"streamId":"","sequence":1,"event":"connection"}"""
|
||||
.encodeToByteArray(),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
WearDecodeResult.Failure(WearDecodeFailureReason.InvalidEnvelope),
|
||||
WearProtocolCodec.decode(
|
||||
"""{"type":"response","version":1,"requestId":"req-1","ok":true,"eventSequence":-1}"""
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package ai.openclaw.wear.shared
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
class WearRealtimeTalkTest {
|
||||
@Test
|
||||
fun audioFramesRoundTripAndPreserveBoundaries() {
|
||||
val output = ByteArrayOutputStream()
|
||||
WearRealtimeAudioFraming.write(output, WearRealtimeAudioFrameType.INPUT_PCM, byteArrayOf(1, 2, 3, 4))
|
||||
WearRealtimeAudioFraming.write(output, WearRealtimeAudioFrameType.CLEAR_OUTPUT, byteArrayOf())
|
||||
val input = ByteArrayInputStream(output.toByteArray())
|
||||
|
||||
val audio = WearRealtimeAudioFraming.read(input)!!
|
||||
assertEquals(WearRealtimeAudioFrameType.INPUT_PCM, audio.type)
|
||||
assertArrayEquals(byteArrayOf(1, 2, 3, 4), audio.payload)
|
||||
assertEquals(WearRealtimeAudioFrameType.CLEAR_OUTPUT, WearRealtimeAudioFraming.read(input)!!.type)
|
||||
assertNull(WearRealtimeAudioFraming.read(input))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun rejectsOversizedFrameBeforeAllocatingPayload() {
|
||||
val output = ByteArrayOutputStream()
|
||||
DataOutputStream(output).apply {
|
||||
writeByte(WearRealtimeAudioFrameType.INPUT_PCM.wireValue)
|
||||
writeInt(WearProtocol.MAX_REALTIME_AUDIO_FRAME_BYTES + 1)
|
||||
}
|
||||
WearRealtimeAudioFraming.read(ByteArrayInputStream(output.toByteArray()))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue