@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.
Files changed (59) hide show
  1. package/README.md +27 -0
  2. package/android/build.gradle +27 -9
  3. package/android/src/main/AndroidManifest.xml +7 -0
  4. package/android/src/main/java/com/mentra/crust/CrustModule.kt +76 -22
  5. package/android/src/main/java/com/mentra/crust/http/NativeHttpFailure.kt +31 -0
  6. package/android/src/main/java/com/mentra/crust/http/NativeHttpRequests.kt +86 -0
  7. package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +70 -60
  8. package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +65 -8
  9. package/android/src/main/java/com/mentra/crust/jsc/JSDeliveryDiagnostics.kt +44 -0
  10. package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +74 -10
  11. package/android/src/main/java/com/mentra/crust/navigation/NavigationSessionGuard.kt +74 -0
  12. package/android/src/main/java/com/mentra/crust/preview/PixelCopyPreview.kt +76 -0
  13. package/android/src/main/java/com/mentra/crust/services/RuntimeRecoveryService.kt +12 -0
  14. package/android/src/test/java/com/mentra/crust/http/NativeHttpFailureTest.kt +44 -0
  15. package/android/src/test/java/com/mentra/crust/http/NativeHttpRequestsTest.kt +133 -0
  16. package/android/src/test/java/com/mentra/crust/jsc/FetchBridgeTest.kt +199 -0
  17. package/android/src/test/java/com/mentra/crust/jsc/HttpRequestTest.kt +32 -0
  18. package/android/src/test/java/com/mentra/crust/jsc/JSDeliveryDiagnosticsTest.kt +54 -0
  19. package/android/src/test/java/com/mentra/crust/jsc/QuickJsLifecycleTest.kt +87 -0
  20. package/android/src/test/java/com/mentra/crust/navigation/NavigationSessionGuardTest.kt +68 -0
  21. package/build/Crust.types.d.ts +25 -11
  22. package/build/Crust.types.d.ts.map +1 -1
  23. package/build/Crust.types.js.map +1 -1
  24. package/build/CrustModule.d.ts +11 -2
  25. package/build/CrustModule.d.ts.map +1 -1
  26. package/build/CrustModule.js.map +1 -1
  27. package/build/CrustModule.web.d.ts +3 -1
  28. package/build/CrustModule.web.d.ts.map +1 -1
  29. package/build/CrustModule.web.js +20 -8
  30. package/build/CrustModule.web.js.map +1 -1
  31. package/build/index.d.ts +1 -0
  32. package/build/index.d.ts.map +1 -1
  33. package/build/index.js +1 -0
  34. package/build/index.js.map +1 -1
  35. package/build/nativeHttpRequest.d.ts +13 -0
  36. package/build/nativeHttpRequest.d.ts.map +1 -0
  37. package/build/nativeHttpRequest.js +51 -0
  38. package/build/nativeHttpRequest.js.map +1 -0
  39. package/build/nativeHttpRequest.test.d.ts +2 -0
  40. package/build/nativeHttpRequest.test.d.ts.map +1 -0
  41. package/build/nativeHttpRequest.test.js +88 -0
  42. package/build/nativeHttpRequest.test.js.map +1 -0
  43. package/ios/Crust.podspec +1 -1
  44. package/ios/CrustModule.swift +22 -7
  45. package/ios/PermissionStatusReceipt.swift +105 -0
  46. package/ios/Resources/startup.js +154 -68
  47. package/ios/Source/JSCPolyfillBridge.swift +31 -14
  48. package/package.json +2 -2
  49. package/plugin/build/index.d.ts +2 -0
  50. package/plugin/build/index.js +2 -0
  51. package/plugin/build/withAndroid.js +39 -0
  52. package/src/Crust.types.ts +26 -12
  53. package/src/CrustModule.ts +11 -1
  54. package/src/CrustModule.web.ts +18 -7
  55. package/src/index.ts +1 -0
  56. package/src/nativeHttpRequest.test.ts +94 -0
  57. package/src/nativeHttpRequest.ts +74 -0
  58. package/android/src/internal/AndroidManifest.xml +0 -9
  59. 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
+ }
@@ -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;IAC9E,wBAAwB,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,mEAAmE;IACnE,sBAAsB,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;IACzC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,2FAA2F;IAC3F,cAAc,EAAE,MAAM,CAAA;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,2DAA2D;IAC3D,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,iDAAiD;IACjD,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,EAAE,MAAM,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG;IAC5C,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,kBAAkB,CAAA;KAAC,KAAK,IAAI,CAAA;IAC1D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAC7B,CAAA"}
1
+ {"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"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"}