@mentra/crust 3.2.1-dev.314 → 3.3.0-beta.1000
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/android/build.gradle +27 -9
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +76 -22
- package/android/src/main/java/com/mentra/crust/http/NativeHttpFailure.kt +31 -0
- package/android/src/main/java/com/mentra/crust/http/NativeHttpRequests.kt +86 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +70 -60
- package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +65 -8
- package/android/src/main/java/com/mentra/crust/jsc/JSDeliveryDiagnostics.kt +44 -0
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +74 -10
- package/android/src/main/java/com/mentra/crust/navigation/NavigationSessionGuard.kt +74 -0
- package/android/src/main/java/com/mentra/crust/preview/PixelCopyPreview.kt +76 -0
- package/android/src/main/java/com/mentra/crust/services/RuntimeRecoveryService.kt +12 -0
- package/android/src/test/java/com/mentra/crust/http/NativeHttpFailureTest.kt +44 -0
- package/android/src/test/java/com/mentra/crust/http/NativeHttpRequestsTest.kt +133 -0
- package/android/src/test/java/com/mentra/crust/jsc/FetchBridgeTest.kt +199 -0
- package/android/src/test/java/com/mentra/crust/jsc/HttpRequestTest.kt +32 -0
- package/android/src/test/java/com/mentra/crust/jsc/JSDeliveryDiagnosticsTest.kt +54 -0
- package/android/src/test/java/com/mentra/crust/jsc/QuickJsLifecycleTest.kt +87 -0
- package/android/src/test/java/com/mentra/crust/navigation/NavigationSessionGuardTest.kt +68 -0
- package/build/Crust.types.d.ts +25 -11
- package/build/Crust.types.d.ts.map +1 -1
- package/build/Crust.types.js.map +1 -1
- package/build/CrustModule.d.ts +11 -2
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +3 -1
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +20 -8
- package/build/CrustModule.web.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/nativeHttpRequest.d.ts +13 -0
- package/build/nativeHttpRequest.d.ts.map +1 -0
- package/build/nativeHttpRequest.js +51 -0
- package/build/nativeHttpRequest.js.map +1 -0
- package/build/nativeHttpRequest.test.d.ts +2 -0
- package/build/nativeHttpRequest.test.d.ts.map +1 -0
- package/build/nativeHttpRequest.test.js +88 -0
- package/build/nativeHttpRequest.test.js.map +1 -0
- package/ios/Crust.podspec +1 -1
- package/ios/CrustModule.swift +22 -7
- package/ios/PermissionStatusReceipt.swift +105 -0
- package/ios/Resources/startup.js +154 -68
- package/ios/Source/JSCPolyfillBridge.swift +31 -14
- package/package.json +2 -2
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +2 -0
- package/plugin/build/withAndroid.js +39 -0
- package/src/Crust.types.ts +26 -12
- package/src/CrustModule.ts +11 -1
- package/src/CrustModule.web.ts +18 -7
- package/src/index.ts +1 -0
- package/src/nativeHttpRequest.test.ts +94 -0
- package/src/nativeHttpRequest.ts +74 -0
- package/android/src/internal/AndroidManifest.xml +0 -9
- package/android/src/internal/java/com/mentra/crust/receivers/CaptionsTesterIncidentReceiver.kt +0 -46
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package com.mentra.crust.http
|
|
2
|
+
|
|
3
|
+
import java.io.IOException
|
|
4
|
+
import java.net.ServerSocket
|
|
5
|
+
import java.util.concurrent.CountDownLatch
|
|
6
|
+
import java.util.concurrent.TimeUnit
|
|
7
|
+
import okhttp3.Call
|
|
8
|
+
import okhttp3.Callback
|
|
9
|
+
import okhttp3.OkHttpClient
|
|
10
|
+
import okhttp3.Protocol
|
|
11
|
+
import okhttp3.Request
|
|
12
|
+
import okhttp3.Response
|
|
13
|
+
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
14
|
+
import okio.Timeout
|
|
15
|
+
import org.junit.Assert.*
|
|
16
|
+
import org.junit.Test
|
|
17
|
+
|
|
18
|
+
class NativeHttpRequestsTest {
|
|
19
|
+
private val request = Request.Builder().url("https://example.com/report").build()
|
|
20
|
+
|
|
21
|
+
private class PendingCall(private val request: Request) : Call {
|
|
22
|
+
var callback: Callback? = null
|
|
23
|
+
var cancelled = false
|
|
24
|
+
var onEnqueue: (() -> Unit)? = null
|
|
25
|
+
override fun request() = request
|
|
26
|
+
override fun execute(): Response = error("Test only exercises asynchronous calls")
|
|
27
|
+
override fun enqueue(responseCallback: Callback) {
|
|
28
|
+
callback = responseCallback
|
|
29
|
+
onEnqueue?.invoke()
|
|
30
|
+
}
|
|
31
|
+
override fun cancel() {cancelled = true}
|
|
32
|
+
override fun isExecuted() = callback != null
|
|
33
|
+
override fun isCanceled() = cancelled
|
|
34
|
+
override fun timeout() = Timeout.NONE
|
|
35
|
+
override fun clone(): Call = PendingCall(request)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Test fun `register before enqueue and cancel only the requested call`() {
|
|
39
|
+
val calls = mutableListOf<PendingCall>()
|
|
40
|
+
val owner = NativeHttpRequests(Call.Factory { req -> PendingCall(req).also {calls.add(it)} })
|
|
41
|
+
owner.enqueue("one", request, {}, {throw it})
|
|
42
|
+
calls[0].onEnqueue = {assertEquals(2, owner.activeCount)}
|
|
43
|
+
owner.enqueue("two", request, {}, {throw it})
|
|
44
|
+
assertEquals(2, owner.activeCount)
|
|
45
|
+
owner.cancel("one")
|
|
46
|
+
assertTrue(calls[0].cancelled)
|
|
47
|
+
assertFalse(calls[1].cancelled)
|
|
48
|
+
assertEquals(1, owner.activeCount)
|
|
49
|
+
owner.cancel("not-owned")
|
|
50
|
+
assertEquals(1, owner.activeCount)
|
|
51
|
+
owner.cancelAll()
|
|
52
|
+
assertTrue(calls[1].cancelled)
|
|
53
|
+
assertEquals(0, owner.activeCount)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test fun `completion removes the active request and preserves the response`() {
|
|
57
|
+
val call = PendingCall(request)
|
|
58
|
+
val owner = NativeHttpRequests(Call.Factory {call})
|
|
59
|
+
var result: NativeHttpRequests.Result? = null
|
|
60
|
+
call.onEnqueue = {assertEquals(1, owner.activeCount)}
|
|
61
|
+
owner.enqueue("complete", request, {result = it}, {throw it})
|
|
62
|
+
call.callback!!.onResponse(call, Response.Builder().request(request).protocol(Protocol.HTTP_1_1)
|
|
63
|
+
.code(200).message("OK").header("Content-Type", "application/json").body("{}".toResponseBody()).build())
|
|
64
|
+
assertEquals("{}", result!!.body)
|
|
65
|
+
assertEquals("application/json", result!!.headers["content-type"])
|
|
66
|
+
assertEquals(0, owner.activeCount)
|
|
67
|
+
owner.cancel("complete")
|
|
68
|
+
assertFalse(call.cancelled)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test fun `network failure and synchronous enqueue failure leave no registry entries`() {
|
|
72
|
+
val call = PendingCall(request)
|
|
73
|
+
val owner = NativeHttpRequests(Call.Factory {call})
|
|
74
|
+
var failures = 0
|
|
75
|
+
owner.enqueue("failed", request, {}, {failures++})
|
|
76
|
+
call.callback!!.onFailure(call, IOException("offline"))
|
|
77
|
+
assertEquals(0, owner.activeCount)
|
|
78
|
+
call.onEnqueue = {throw IllegalStateException("dispatcher closed")}
|
|
79
|
+
owner.enqueue("enqueue-failed", request, {}, {failures++})
|
|
80
|
+
assertEquals(0, owner.activeCount)
|
|
81
|
+
assertEquals(2, failures)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test fun `duplicate IDs cannot replace existing ownership and destruction prevents later requests`() {
|
|
85
|
+
val calls = mutableListOf<PendingCall>()
|
|
86
|
+
val owner = NativeHttpRequests(Call.Factory {req -> PendingCall(req).also {calls.add(it)} })
|
|
87
|
+
var failures = 0
|
|
88
|
+
owner.enqueue("one", request, {}, {failures++})
|
|
89
|
+
owner.enqueue("one", request, {}, {failures++})
|
|
90
|
+
assertEquals(1, failures)
|
|
91
|
+
assertEquals(1, owner.activeCount)
|
|
92
|
+
assertNull(calls[1].callback)
|
|
93
|
+
owner.cancelAll()
|
|
94
|
+
assertTrue(calls[0].cancelled)
|
|
95
|
+
owner.enqueue("later", request, {}, {failures++})
|
|
96
|
+
assertEquals(2, failures)
|
|
97
|
+
assertEquals(2, calls.size)
|
|
98
|
+
assertEquals(0, owner.activeCount)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test fun `cancel interrupts an actual OkHttp response body and closes the server socket`() {
|
|
102
|
+
val client = OkHttpClient()
|
|
103
|
+
val owner = NativeHttpRequests(client)
|
|
104
|
+
val bodyStarted = CountDownLatch(1)
|
|
105
|
+
val socketClosed = CountDownLatch(1)
|
|
106
|
+
val failed = CountDownLatch(1)
|
|
107
|
+
ServerSocket(0, 1, java.net.InetAddress.getLoopbackAddress()).use {server ->
|
|
108
|
+
val thread = Thread {
|
|
109
|
+
server.accept().use {socket ->
|
|
110
|
+
socket.soTimeout = 5_000
|
|
111
|
+
val reader = socket.getInputStream().bufferedReader()
|
|
112
|
+
while (!reader.readLine().isNullOrEmpty()) {}
|
|
113
|
+
socket.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n".toByteArray())
|
|
114
|
+
socket.getOutputStream().flush()
|
|
115
|
+
bodyStarted.countDown()
|
|
116
|
+
if (socket.getInputStream().read() == -1) socketClosed.countDown()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
thread.isDaemon = true
|
|
120
|
+
thread.start()
|
|
121
|
+
val req = Request.Builder().url("http://127.0.0.1:${server.localPort}/report").build()
|
|
122
|
+
owner.enqueue("stalled-body", req, {fail("Cancelled body must not complete")}, {failed.countDown()})
|
|
123
|
+
assertTrue("server started body", bodyStarted.await(5, TimeUnit.SECONDS))
|
|
124
|
+
owner.cancel("stalled-body")
|
|
125
|
+
assertTrue("native request rejected", failed.await(5, TimeUnit.SECONDS))
|
|
126
|
+
assertTrue("socket closed", socketClosed.await(5, TimeUnit.SECONDS))
|
|
127
|
+
assertEquals(0, owner.activeCount)
|
|
128
|
+
thread.join(5_000)
|
|
129
|
+
}
|
|
130
|
+
client.dispatcher.executorService.shutdownNow()
|
|
131
|
+
client.connectionPool.evictAll()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.util.concurrent.LinkedBlockingQueue
|
|
5
|
+
import java.util.concurrent.TimeUnit
|
|
6
|
+
import kotlin.math.PI
|
|
7
|
+
import kotlin.math.roundToInt
|
|
8
|
+
import kotlin.math.sin
|
|
9
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
10
|
+
import okhttp3.Protocol
|
|
11
|
+
import okhttp3.Request
|
|
12
|
+
import okhttp3.Response
|
|
13
|
+
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
14
|
+
import okhttp3.mockwebserver.Dispatcher
|
|
15
|
+
import okhttp3.mockwebserver.MockResponse
|
|
16
|
+
import okhttp3.mockwebserver.MockWebServer
|
|
17
|
+
import okhttp3.mockwebserver.RecordedRequest
|
|
18
|
+
import okio.Buffer
|
|
19
|
+
import okio.ByteString.Companion.decodeBase64
|
|
20
|
+
import org.junit.After
|
|
21
|
+
import org.junit.Assert.*
|
|
22
|
+
import org.junit.Before
|
|
23
|
+
import org.junit.Test
|
|
24
|
+
import org.junit.runner.RunWith
|
|
25
|
+
import org.robolectric.RobolectricTestRunner
|
|
26
|
+
import org.robolectric.RuntimeEnvironment
|
|
27
|
+
import org.robolectric.annotation.Config
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Miniapp `fetch` end to end on Android's real stack: the shipped startup.js
|
|
31
|
+
* polyfill in QuickJS, [JSCRuntime]'s dispatcher and `__deliver` path, this
|
|
32
|
+
* bridge, and OkHttp against a local server. Each miniapp script reports
|
|
33
|
+
* through a `test` dispatch, which the runtime forwards to `onOutbound`.
|
|
34
|
+
*/
|
|
35
|
+
@RunWith(RobolectricTestRunner::class)
|
|
36
|
+
@Config(sdk = [28], manifest = Config.NONE)
|
|
37
|
+
class FetchBridgeTest {
|
|
38
|
+
private val packageName = "com.mentra.fetchtest"
|
|
39
|
+
private val results = LinkedBlockingQueue<Map<*, *>>()
|
|
40
|
+
private lateinit var server: MockWebServer
|
|
41
|
+
private lateinit var runtime: JSCRuntime
|
|
42
|
+
|
|
43
|
+
/** A 24 kHz 16-bit sine followed by every byte value: mostly not valid UTF-8. */
|
|
44
|
+
private val pcm = ByteArray(4800 * 2 + 256).also { out ->
|
|
45
|
+
for (i in 0 until 4800) {
|
|
46
|
+
val sample = (sin(2 * PI * 220 * i / 24000) * 20000).roundToInt()
|
|
47
|
+
out[2 * i] = sample.toByte()
|
|
48
|
+
out[2 * i + 1] = (sample shr 8).toByte()
|
|
49
|
+
}
|
|
50
|
+
for (value in 0 until 256) out[9600 + value] = value.toByte()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Valid UTF-8 that is still binary data: a BOM, NUL and DEL must all survive. */
|
|
54
|
+
private val utf8Bytes = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) +
|
|
55
|
+
"{\"a\":\"é😀\"}".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00, 0x7F)
|
|
56
|
+
|
|
57
|
+
// crust's build.gradle ships this file as the runtime's startup.js asset.
|
|
58
|
+
private val polyfill: String by lazy { File("../../jspolyfill/assets/startup.js").readText() }
|
|
59
|
+
|
|
60
|
+
@Before fun setUp() {
|
|
61
|
+
server = MockWebServer().apply {
|
|
62
|
+
dispatcher = object : Dispatcher() {
|
|
63
|
+
override fun dispatch(request: RecordedRequest): MockResponse = when (request.path) {
|
|
64
|
+
"/reply.pcm" -> response("application/octet-stream").setBody(Buffer().write(pcm))
|
|
65
|
+
"/utf8.bin" -> response("application/octet-stream").setBody(Buffer().write(utf8Bytes))
|
|
66
|
+
"/data.json" -> response("application/json").setBody("{\"message\":\"héllo 😀 中文\"}")
|
|
67
|
+
"/upload" -> response("text/plain").setBody("ok")
|
|
68
|
+
else -> MockResponse().setResponseCode(404)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
start()
|
|
72
|
+
}
|
|
73
|
+
runtime = JSCRuntime.shared(RuntimeEnvironment.getApplication())
|
|
74
|
+
JSCPolyfillBridge.install(runtime)
|
|
75
|
+
runtime.onOutbound = { message ->
|
|
76
|
+
if (message.payload["iface"] == "test") {
|
|
77
|
+
results.add((message.payload["args"] as List<*>).first() as Map<*, *>)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@After fun tearDown() {
|
|
83
|
+
runtime.kill(packageName)
|
|
84
|
+
runtime.onOutbound = null
|
|
85
|
+
server.shutdown()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test fun `arrayBuffer returns the exact bytes of a binary response`() {
|
|
89
|
+
val result = runMiniapp(
|
|
90
|
+
"""
|
|
91
|
+
const response = await fetch(BASE + "/reply.pcm")
|
|
92
|
+
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
93
|
+
return {status: response.status, type: response.headers.get("content-type"), bytes: Array.from(bytes)}
|
|
94
|
+
""",
|
|
95
|
+
)
|
|
96
|
+
assertEquals(200, result["status"])
|
|
97
|
+
assertEquals("application/octet-stream", result["type"])
|
|
98
|
+
assertArrayEquals(pcm, bytes(result["bytes"]))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test fun `arrayBuffer returns the exact bytes of a body that is valid UTF-8`() {
|
|
102
|
+
val result = runMiniapp(
|
|
103
|
+
"""
|
|
104
|
+
const response = await fetch(BASE + "/utf8.bin")
|
|
105
|
+
return {bytes: Array.from(new Uint8Array(await response.arrayBuffer()))}
|
|
106
|
+
""",
|
|
107
|
+
)
|
|
108
|
+
assertArrayEquals(utf8Bytes, bytes(result["bytes"]))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test fun `responses are sent as text only when that text re-encodes to the same bytes`() {
|
|
112
|
+
val text = JSCPolyfillBridge.fetchResult(responseWith(utf8Bytes))
|
|
113
|
+
assertEquals(String(utf8Bytes, Charsets.UTF_8), text.getString("body"))
|
|
114
|
+
assertFalse(text.has("bodyBase64"))
|
|
115
|
+
|
|
116
|
+
val empty = JSCPolyfillBridge.fetchResult(responseWith(ByteArray(0)))
|
|
117
|
+
assertEquals("", empty.getString("body"))
|
|
118
|
+
|
|
119
|
+
// Truncated, overlong, surrogate and out-of-range sequences, then raw PCM.
|
|
120
|
+
for (binary in listOf(byteArrayOf(0x41, 0xE2.toByte(), 0x82.toByte()), byteArrayOf(0xC0.toByte(), 0x80.toByte()),
|
|
121
|
+
byteArrayOf(0xED.toByte(), 0xA0.toByte(), 0x80.toByte()), byteArrayOf(0xF4.toByte(), 0x90.toByte(), 0x80.toByte(), 0x80.toByte()), pcm)) {
|
|
122
|
+
val result = JSCPolyfillBridge.fetchResult(responseWith(binary))
|
|
123
|
+
assertFalse(result.has("body"))
|
|
124
|
+
assertArrayEquals(binary, result.getString("bodyBase64").decodeBase64()!!.toByteArray())
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test fun `text and json decode a UTF-8 response`() {
|
|
129
|
+
val result = runMiniapp(
|
|
130
|
+
"""
|
|
131
|
+
const text = await (await fetch(BASE + "/data.json")).text()
|
|
132
|
+
const json = await (await fetch(BASE + "/data.json")).json()
|
|
133
|
+
return {text, message: json.message}
|
|
134
|
+
""",
|
|
135
|
+
)
|
|
136
|
+
assertEquals("{\"message\":\"héllo 😀 中文\"}", result["text"])
|
|
137
|
+
assertEquals("héllo 😀 中文", result["message"])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test fun `binary request bodies reach the server byte for byte`() {
|
|
141
|
+
val result = runMiniapp(
|
|
142
|
+
"""
|
|
143
|
+
const backing = Uint8Array.from({length: 600}, (_, i) => (i * 37 + 11) & 255)
|
|
144
|
+
const response = await fetch(BASE + "/upload", {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {"Content-Type": "audio/L16; rate=24000"},
|
|
147
|
+
body: backing.subarray(44),
|
|
148
|
+
})
|
|
149
|
+
return {status: response.status, text: await response.text()}
|
|
150
|
+
""",
|
|
151
|
+
)
|
|
152
|
+
assertEquals(200, result["status"])
|
|
153
|
+
assertEquals("ok", result["text"])
|
|
154
|
+
val upload = server.takeRequest()
|
|
155
|
+
val expected = ByteArray(600) { ((it * 37 + 11) and 255).toByte() }.copyOfRange(44, 600)
|
|
156
|
+
assertArrayEquals(expected, upload.body.readByteArray())
|
|
157
|
+
assertEquals("audio/L16; rate=24000", upload.getHeader("Content-Type"))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test fun `string request bodies keep their UTF-8 text and content type`() {
|
|
161
|
+
runMiniapp(
|
|
162
|
+
"""
|
|
163
|
+
await fetch(BASE + "/upload", {method: "POST", headers: {"Content-Type": "application/json"}, body: '{"t":"é😀"}'})
|
|
164
|
+
return {}
|
|
165
|
+
""",
|
|
166
|
+
)
|
|
167
|
+
val upload = server.takeRequest()
|
|
168
|
+
assertEquals("{\"t\":\"é😀\"}", upload.body.readUtf8())
|
|
169
|
+
assertEquals("application/json; charset=utf-8", upload.getHeader("Content-Type"))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun runMiniapp(body: String): Map<*, *> {
|
|
173
|
+
val script =
|
|
174
|
+
"""
|
|
175
|
+
const BASE = "${server.url("/").toString().removeSuffix("/")}"
|
|
176
|
+
const report = (value) => __mentraSendOneShot("test", "result", [value])
|
|
177
|
+
;(async () => {
|
|
178
|
+
$body
|
|
179
|
+
})().then(report, (error) => report({error: String(error && (error.message || error))}))
|
|
180
|
+
""".trimIndent()
|
|
181
|
+
assertTrue("spawn failed", runtime.spawn(packageName, polyfill, script))
|
|
182
|
+
val result = results.poll(15, TimeUnit.SECONDS) ?: throw AssertionError("miniapp did not report a result")
|
|
183
|
+
assertNull("miniapp script threw", result["error"])
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun bytes(value: Any?): ByteArray =
|
|
188
|
+
(value as List<*>).map { (it as Number).toInt().toByte() }.toByteArray()
|
|
189
|
+
|
|
190
|
+
private fun response(contentType: String) = MockResponse().setHeader("Content-Type", contentType)
|
|
191
|
+
|
|
192
|
+
private fun responseWith(body: ByteArray): Response = Response.Builder()
|
|
193
|
+
.request(Request.Builder().url("https://example.com/body").build())
|
|
194
|
+
.protocol(Protocol.HTTP_1_1)
|
|
195
|
+
.code(200)
|
|
196
|
+
.message("OK")
|
|
197
|
+
.body(body.toResponseBody("application/octet-stream".toMediaType()))
|
|
198
|
+
.build()
|
|
199
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
import okhttp3.Request
|
|
4
|
+
import okio.Buffer
|
|
5
|
+
import org.junit.Assert.*
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class HttpRequestTest {
|
|
9
|
+
private fun request(method: String, body: String? = null): Request =
|
|
10
|
+
JSCPolyfillBridge.buildHttpRequest(method, "https://example.com/stream", mapOf("Content-Type" to "application/json"), body)
|
|
11
|
+
|
|
12
|
+
@Test fun `bodyless post put and patch produce valid zero byte requests`() {
|
|
13
|
+
for (method in listOf("post", "PUT", "PATCH")) for (body in listOf(null, "")) {
|
|
14
|
+
val request = request(method, body)
|
|
15
|
+
assertEquals(method.uppercase(), request.method)
|
|
16
|
+
assertNotNull(request.body)
|
|
17
|
+
assertEquals(0L, request.body!!.contentLength())
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test fun `get head and delete can omit a body`() {
|
|
22
|
+
for (method in listOf("GET", "HEAD", "DELETE")) assertNull(request(method).body)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test fun `nonempty body and content type survive request construction`() {
|
|
26
|
+
val request = request("POST", "{\"status\":\"stopped\"}")
|
|
27
|
+
val buffer = Buffer()
|
|
28
|
+
request.body!!.writeTo(buffer)
|
|
29
|
+
assertEquals("{\"status\":\"stopped\"}", buffer.readUtf8())
|
|
30
|
+
assertEquals("application/json; charset=utf-8", request.body!!.contentType().toString())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class JSDeliveryDiagnosticsTest {
|
|
7
|
+
@Test fun `queue wait and evaluate duration are distinct`() {
|
|
8
|
+
var now = 100L
|
|
9
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
10
|
+
val diagnostic = JSDeliveryDiagnostics(7, 2, { now }, { events += it })
|
|
11
|
+
diagnostic.entry()
|
|
12
|
+
now = 110
|
|
13
|
+
diagnostic.queued()
|
|
14
|
+
now = 310
|
|
15
|
+
diagnostic.started()
|
|
16
|
+
now = 350
|
|
17
|
+
diagnostic.finished()
|
|
18
|
+
|
|
19
|
+
assertEquals(listOf("native-entry", "queued", "started", "finished"), events.map { it["phase"] })
|
|
20
|
+
assertEquals(200L, events.last()["queueWaitMs"])
|
|
21
|
+
assertEquals(40L, events.last()["evaluateMs"])
|
|
22
|
+
assertEquals(250L, events.last()["elapsedMs"])
|
|
23
|
+
assertEquals(7L, events.last()["deliveryId"])
|
|
24
|
+
assertEquals(2L, events.last()["contextId"])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test fun `failure includes exception identity without payload or message`() {
|
|
28
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
29
|
+
val diagnostic = JSDeliveryDiagnostics(1, 4, { 0L }, { events += it })
|
|
30
|
+
val error = IllegalStateException("private-token user transcript", IllegalArgumentException("private-source"))
|
|
31
|
+
diagnostic.entry()
|
|
32
|
+
diagnostic.queued()
|
|
33
|
+
diagnostic.started()
|
|
34
|
+
diagnostic.finished(error)
|
|
35
|
+
|
|
36
|
+
assertEquals("evaluate-failed", events.last()["phase"])
|
|
37
|
+
assertEquals("IllegalStateException", events.last()["errorClass"])
|
|
38
|
+
assertEquals("IllegalArgumentException", events.last()["causeClass"])
|
|
39
|
+
assertFalse(events.toString().contains("private-"))
|
|
40
|
+
assertFalse(events.toString().contains("transcript"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test fun `missing context and rejected executor have explicit terminal events`() {
|
|
44
|
+
for (reason in listOf("context-missing", "executor-rejected")) {
|
|
45
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
46
|
+
val diagnostic = JSDeliveryDiagnostics(9, null, { 0L }, { events += it })
|
|
47
|
+
diagnostic.entry()
|
|
48
|
+
diagnostic.dropped(reason)
|
|
49
|
+
assertEquals(reason, events.last()["phase"])
|
|
50
|
+
assertNull(events.last()["contextId"])
|
|
51
|
+
assertNull(events.last()["evaluateMs"])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
import com.dokar.quickjs.QuickJs
|
|
4
|
+
import com.dokar.quickjs.QuickJsException
|
|
5
|
+
import com.dokar.quickjs.binding.function
|
|
6
|
+
import java.util.concurrent.Executors
|
|
7
|
+
import kotlinx.coroutines.Dispatchers
|
|
8
|
+
import kotlinx.coroutines.runBlocking
|
|
9
|
+
import kotlinx.coroutines.withTimeout
|
|
10
|
+
import org.junit.Assert.*
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs the real QuickJS JNI library (through the desktop JVM artifact, see
|
|
15
|
+
* build.gradle) the way [JSCRuntime] uses it: one instance per miniapp, Kotlin
|
|
16
|
+
* bindings called from JS, and independent close orders.
|
|
17
|
+
*/
|
|
18
|
+
class QuickJsLifecycleTest {
|
|
19
|
+
private fun create() = QuickJs.create(Dispatchers.Unconfined)
|
|
20
|
+
|
|
21
|
+
@Test fun `closing one instance keeps another instance's callbacks working`() = runBlocking {
|
|
22
|
+
var calls = 0
|
|
23
|
+
val survivor = create()
|
|
24
|
+
survivor.function("hostCallback") { calls++; 42 }
|
|
25
|
+
val other = create()
|
|
26
|
+
assertEquals(42, survivor.evaluate<Int>("hostCallback()"))
|
|
27
|
+
other.close()
|
|
28
|
+
withTimeout(1000) { assertEquals(42, survivor.evaluate<Int>("hostCallback()")) }
|
|
29
|
+
assertEquals(2, calls)
|
|
30
|
+
repeat(20) {
|
|
31
|
+
create().close()
|
|
32
|
+
assertEquals(42, survivor.evaluate<Int>("hostCallback()"))
|
|
33
|
+
}
|
|
34
|
+
val workers = Executors.newFixedThreadPool(4)
|
|
35
|
+
try {
|
|
36
|
+
(1..40).map { workers.submit { create().close() } }.forEach { it.get() }
|
|
37
|
+
assertEquals(42, survivor.evaluate<Int>("hostCallback()"))
|
|
38
|
+
} finally {
|
|
39
|
+
workers.shutdownNow()
|
|
40
|
+
}
|
|
41
|
+
survivor.close()
|
|
42
|
+
val fresh = create()
|
|
43
|
+
fresh.function("hostCallback") { 7 }
|
|
44
|
+
assertEquals(7, fresh.evaluate<Int>("hostCallback()"))
|
|
45
|
+
fresh.close()
|
|
46
|
+
val older = create()
|
|
47
|
+
val newer = create()
|
|
48
|
+
newer.function("hostCallback") { 9 }
|
|
49
|
+
older.close()
|
|
50
|
+
assertEquals(9, newer.evaluate<Int>("hostCallback()"))
|
|
51
|
+
newer.close()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test fun `a binding exception is a catchable JS Error carrying the message`() = runBlocking {
|
|
55
|
+
val qjs = create()
|
|
56
|
+
qjs.function("__dispatch") { throw MentraJSDispatchError("INVALID_ARGS", "iface") }
|
|
57
|
+
val caught = qjs.evaluate<String>(
|
|
58
|
+
"try { __dispatch(); 'no throw' } catch (e) { (e instanceof Error) + '|' + e.message }",
|
|
59
|
+
)
|
|
60
|
+
assertEquals("true|INVALID_ARGS: iface", caught)
|
|
61
|
+
qjs.close()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test fun `non-ASCII strings cross the bridge unchanged`() = runBlocking {
|
|
65
|
+
val text = "héllo 😀 中文 \u0000 end"
|
|
66
|
+
var received: String? = null
|
|
67
|
+
val qjs = create()
|
|
68
|
+
qjs.function("hostText") { text }
|
|
69
|
+
qjs.function("hostEcho") { args -> received = args[0] as String; args[0] }
|
|
70
|
+
assertEquals(text, qjs.evaluate<String>("hostEcho(hostText())"))
|
|
71
|
+
assertEquals(text, received)
|
|
72
|
+
qjs.close()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test fun `unusual thrown values fail as exceptions, not crashes`() = runBlocking {
|
|
76
|
+
val qjs = create()
|
|
77
|
+
for (source in listOf("throw undefined", "throw null", "throw new Error('%s%n%s%n')")) {
|
|
78
|
+
try {
|
|
79
|
+
qjs.evaluate<Any?>(source)
|
|
80
|
+
fail("$source did not throw")
|
|
81
|
+
} catch (_: QuickJsException) {
|
|
82
|
+
}
|
|
83
|
+
assertEquals(2, qjs.evaluate<Int>("1 + 1"))
|
|
84
|
+
}
|
|
85
|
+
qjs.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.mentra.crust.navigation
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class NavigationSessionGuardTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun cachedCallbacksAreRejectedUntilTheCurrentInitialRouteIsReady() {
|
|
10
|
+
val guard = NavigationSessionGuard()
|
|
11
|
+
val session = guard.begin()
|
|
12
|
+
|
|
13
|
+
assertTrue(guard.acceptsCallback(session))
|
|
14
|
+
assertFalse(guard.acceptsLocation(session))
|
|
15
|
+
assertFalse(guard.acceptRouteUpdate(session, "cached-route"))
|
|
16
|
+
assertFalse(guard.acceptsProgress(session, "cached-route"))
|
|
17
|
+
|
|
18
|
+
assertTrue(guard.activateInitialRoute(session, "current-route"))
|
|
19
|
+
assertTrue(guard.acceptsProgress(session, "current-route"))
|
|
20
|
+
|
|
21
|
+
guard.enableLocationCallbacks(session)
|
|
22
|
+
assertTrue(guard.acceptsLocation(session))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun stoppedAndSupersededSessionsCannotMutateCurrentState() {
|
|
27
|
+
val guard = NavigationSessionGuard()
|
|
28
|
+
val oldSession = guard.begin()
|
|
29
|
+
assertTrue(guard.activateInitialRoute(oldSession, "old-route"))
|
|
30
|
+
|
|
31
|
+
guard.invalidate()
|
|
32
|
+
assertFalse(guard.acceptsCallback(oldSession))
|
|
33
|
+
guard.enableLocationCallbacks(oldSession)
|
|
34
|
+
assertFalse(guard.acceptsLocation(oldSession))
|
|
35
|
+
assertFalse(guard.activateInitialRoute(oldSession, "late-route"))
|
|
36
|
+
assertFalse(guard.acceptRouteUpdate(oldSession, "late-reroute"))
|
|
37
|
+
assertFalse(guard.acceptsProgress(oldSession, "old-route"))
|
|
38
|
+
|
|
39
|
+
val currentSession = guard.begin()
|
|
40
|
+
assertTrue(guard.activateInitialRoute(currentSession, "current-route"))
|
|
41
|
+
assertTrue(guard.acceptsProgress(currentSession, "current-route"))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun rerouteMovesProgressIdentityToTheNewRoute() {
|
|
46
|
+
val guard = NavigationSessionGuard()
|
|
47
|
+
val session = guard.begin()
|
|
48
|
+
assertTrue(guard.activateInitialRoute(session, "initial-route"))
|
|
49
|
+
|
|
50
|
+
assertTrue(guard.acceptRouteUpdate(session, "rerouted-route"))
|
|
51
|
+
assertFalse(guard.acceptsProgress(session, "initial-route"))
|
|
52
|
+
assertTrue(guard.acceptsProgress(session, "rerouted-route"))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun delayedWorkCapturedBeforeStopCannotRunInTheNextSession() {
|
|
57
|
+
val guard = NavigationSessionGuard()
|
|
58
|
+
val oldWork = guard.begin()
|
|
59
|
+
assertTrue(guard.captureActive() == oldWork)
|
|
60
|
+
|
|
61
|
+
guard.invalidate()
|
|
62
|
+
assertTrue(guard.captureActive() == null)
|
|
63
|
+
val nextSession = guard.begin()
|
|
64
|
+
|
|
65
|
+
assertFalse(guard.acceptsCallback(oldWork))
|
|
66
|
+
assertTrue(guard.acceptsCallback(nextSession))
|
|
67
|
+
}
|
|
68
|
+
}
|
package/build/Crust.types.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ export type CrustModuleEvents = {
|
|
|
14
14
|
onHeading: (params: HeadingPayload) => void;
|
|
15
15
|
phone_notification: (event: PhoneNotificationEvent) => void;
|
|
16
16
|
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void;
|
|
17
|
-
captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void;
|
|
18
17
|
};
|
|
19
18
|
export type NavOffRoutePayload = {
|
|
20
19
|
/** Approximate perpendicular distance in meters from the route. */
|
|
@@ -107,6 +106,8 @@ export type NavManeuverPayload = {
|
|
|
107
106
|
};
|
|
108
107
|
export type NavErrorPayload = {
|
|
109
108
|
message: string;
|
|
109
|
+
/** True only when native navigation has ended; absent for recoverable reroute errors. */
|
|
110
|
+
terminal?: boolean;
|
|
110
111
|
};
|
|
111
112
|
export type InstalledApp = {
|
|
112
113
|
packageName: string;
|
|
@@ -133,16 +134,6 @@ export type PhoneNotificationDismissedEvent = {
|
|
|
133
134
|
packageName: string;
|
|
134
135
|
notificationId: string;
|
|
135
136
|
};
|
|
136
|
-
export type CaptionsTesterIncidentEvent = {
|
|
137
|
-
action?: string;
|
|
138
|
-
timestamp?: number;
|
|
139
|
-
failure_code?: string;
|
|
140
|
-
failure_message?: string;
|
|
141
|
-
test_run_id?: string;
|
|
142
|
-
scenario_name?: string;
|
|
143
|
-
source?: string;
|
|
144
|
-
[key: string]: unknown;
|
|
145
|
-
};
|
|
146
137
|
export type CrustViewProps = {
|
|
147
138
|
url: string;
|
|
148
139
|
onLoad: (event: {
|
|
@@ -150,4 +141,27 @@ export type CrustViewProps = {
|
|
|
150
141
|
}) => void;
|
|
151
142
|
style?: StyleProp<ViewStyle>;
|
|
152
143
|
};
|
|
144
|
+
/** Passive OS observations for the currently executing Mentra App. */
|
|
145
|
+
export type AppPermissionStatus = {
|
|
146
|
+
rawStatus: string | null;
|
|
147
|
+
status: "granted" | "denied" | "blocked" | "unavailable" | "unknown";
|
|
148
|
+
};
|
|
149
|
+
export type AppPermissionSnapshot = {
|
|
150
|
+
schemaVersion: 1;
|
|
151
|
+
observedAt: string;
|
|
152
|
+
identity: {
|
|
153
|
+
pid: number;
|
|
154
|
+
uid: number;
|
|
155
|
+
bundleId: string;
|
|
156
|
+
bundlePath: string;
|
|
157
|
+
executablePath: string;
|
|
158
|
+
version: string;
|
|
159
|
+
build: string;
|
|
160
|
+
};
|
|
161
|
+
locationServicesEnabled: boolean;
|
|
162
|
+
permissions: Record<"location" | "camera" | "microphone" | "bluetooth" | "localNetwork", AppPermissionStatus>;
|
|
163
|
+
};
|
|
164
|
+
export type AppPermissionReceipt = AppPermissionSnapshot & {
|
|
165
|
+
requestId: string;
|
|
166
|
+
};
|
|
153
167
|
//# sourceMappingURL=Crust.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAE,SAAS,EAAC,MAAM,cAAc,CAAA;AAEtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC9C,aAAa,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACnD,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,IAAI,CAAA;IACvD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,IAAI,CAAA;IACrD,UAAU,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACnD,UAAU,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACnD,SAAS,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAA;IAC3C,kBAAkB,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAC3D,4BAA4B,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"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"}
|