mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
test(napplet): SDK wire-conformance audit + pinned conformance suite
Feature-by-feature audit of our edge layer against the canonical @napplet/nap@0.15.0 / @napplet/core@0.15.0 message types, plus a test suite that pins the codec to the SDK's exact wire so drift fails CI. Audit (plans/2026-06-21-napplet-sdk-conformance-audit.md) catalogs every domain with the verified wire shapes and ranks the inconsistencies found: - 🔴 shell handshake missing: SDK uses shell.ready -> shell.init{capabilities, services} and answers supports() locally; we model a shell.supports request the SDK never sends, so a real napplet's capability env stays empty. - 🔴 host drops id-less messages (shell.ready / inc.emit / keys.unregisterAction). - 🔴 keys.* rejects at the boundary (only client-side stubs). - 🔴 upload non-conformant (upload vs upload.upload; flat base64 vs request:{data:Blob}; and a Blob can't cross our JSON string bridge). - ◐ relay query/subscribe use only the first of filters[]; identity getList/getZaps/getBadges + onChanged; resource nostr: + cancel; relay.closed + live tail. Conformant and now pinned by NappletSdkConformanceTest: relay publish/publishEncrypted/query/subscribe + event/eose pushes, identity reads (method-specific result fields), storage get/set/remove/keys, resource.bytes, and the error convention. Gap guards assert the current (non-conformant) behavior so each flips intentionally when fixed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# Napplet SDK conformance audit — feature by feature
|
||||
|
||||
**Date:** 2026-06-21
|
||||
**Authoritative sources (verified, not from memory):**
|
||||
`@napplet/nap@0.15.0` (`dist/<domain>/types.d.ts` — the canonical wire message types),
|
||||
`@napplet/shim@0.16.0` (the SDK napplets bundle), `@napplet/core@0.15.0` (base envelope +
|
||||
shell handshake). Audited against branch `claude/awesome-pasteur-xwiwad`.
|
||||
|
||||
Our edge layer: `NappletProtocolJson` (codec), `NappletRequest`/`NappletResponse` (commons),
|
||||
`NappletHostActivity` (`SHIM_JS` + `shell.html` relay), `NappletBroker`, `NappletBrokerService`.
|
||||
|
||||
## Base envelope & error convention (verified)
|
||||
|
||||
- `NappletMessage` carries only **`type`** (`"domain.action"`). **There is no universal `id`** —
|
||||
request/response pairs add `id`; fire-and-forget and handshake/push messages have **no `id`**.
|
||||
- **No universal `ok`.** Each domain picks its own: `relay.publish`/`publishEncrypted`,
|
||||
`outbox.publish`, `upload`, `intent` use `ok: boolean`; identity/storage/query results omit `ok`
|
||||
and signal success by the data field's presence + an optional `error?: string`. The SDK shim
|
||||
rejects when `error` is present and otherwise reads the domain's data field.
|
||||
- **Ours:** we set `ok` on *every* result. Harmless (the SDK reads the data field and ignores the
|
||||
extra `ok`), and our own injected shim relies on `ok`. ✅ compatible, ⚠️ non-canonical.
|
||||
|
||||
## Transport (verified) — two architectural gaps
|
||||
|
||||
1. **Structured-clone objects, not strings.** `@napplet/core` posts cloneable **objects**. Our
|
||||
`shell.html` now bridges object↔string both ways (done earlier). ✅
|
||||
2. **🔴 The host drops id-less messages.** `NappletHostActivity.onShellMessage` does
|
||||
`id = optString("id").ifEmpty { return }` — so every message **without an `id`** is dropped:
|
||||
`shell.ready`, `inc.emit`, `keys.unregisterAction`. This silently breaks the shell handshake and
|
||||
all fire-and-forget messages. **Must fix** to forward/handle id-less messages.
|
||||
3. **🔴 Blob-carrying requests can't cross.** `upload.upload`'s request payload contains a `Blob`
|
||||
(`data: Blob | ArrayBuffer`). Our applet→native bridge does `JSON.stringify`, which turns a Blob
|
||||
into `{}`. Real-napplet uploads lose their bytes. Needs a Blob-aware request path.
|
||||
|
||||
## Shell handshake (verified) — 🔴 not implemented
|
||||
|
||||
The SDK does **not** send a `shell.supports` message. Instead:
|
||||
- napplet posts **`shell.ready`** (no payload), and
|
||||
- the shell replies **once** with **`shell.init`** = `{ capabilities: { domains: string[],
|
||||
protocols: Record<string,string[]> }, services: string[] }`.
|
||||
- `shell.supports(capability, protocol?)` is then answered **synchronously and locally** from that
|
||||
cached environment.
|
||||
|
||||
**Ours:** we implement a `shell.supports` *request* (`ShellSupports` → `Supported`) and our injected
|
||||
shim calls it async. A real napplet never sends `shell.supports`; it sends `shell.ready` — which our
|
||||
host **drops** (no id) — so its cached environment stays empty and `supports()` returns `false` for
|
||||
everything, likely making well-behaved napplets bail early. **Highest-impact gap.**
|
||||
Fix: host answers `shell.ready` with a `shell.init` carrying the declared domains.
|
||||
|
||||
## Per-domain conformance matrix
|
||||
|
||||
Legend: ✅ conformant · ◐ partial · 🔴 mismatch/missing · ➖ not modeled.
|
||||
|
||||
| Domain | SDK surface (wire) | Ours | Verdict |
|
||||
|---|---|---|---|
|
||||
| **shell** | `shell.ready`→`shell.init{capabilities,services}`; `supports()` local | `shell.supports` request→`{supported}` | 🔴 wrong model (no handshake) |
|
||||
| **identity** | `getPublicKey`→`{pubkey}`; `getProfile`→`{profile}`; `getRelays`→`{relays}`; `getFollows`/`getMutes`/`getBlocked`→`{pubkeys}`; `getList`→`{entries}`; `getZaps`→`{zaps}`; `getBadges`→`{badges}`; `onChanged` push | getPublicKey ✅; profile/relays/follows/mutes/blocked ✅ (exact fields); getList/getZaps/getBadges → Unsupported; onChanged no-op | ◐ reads ✅, push/3 methods missing |
|
||||
| **relay** | `publish{event}`→`{ok,event,eventId}`; `publishEncrypted{event,recipient,encryption}`; `query{filters[]}`→`{events}`; `subscribe{subId,filters,relay?}`; `close{subId}`; pushes `relay.event{subId,event,resources?}`, `relay.eose{subId}`, `relay.closed{subId,reason?}` | publish/publishEncrypted ✅ (read `event`); query ◐ (first filter only); subscribe ✅ + event/eose push ✅ (snapshot); close ✅ (no-op); `relay.closed` not emitted | ◐ strong; multi-filter + live tail + `relay.closed` open |
|
||||
| **storage** | `get`→`{value}`; `set`; `remove`; `keys`→`{keys}` (512 KB quota) | ✅ exact (`get/set/remove/keys`, `value`/`keys` fields) | ✅ (quota not enforced) |
|
||||
| **resource** | `bytes{url}`→`{blob,mime}`; `cancel`; https/blossom/nostr/data | bytes ✅ (shell builds Blob); https/data/blossom ✅; nostr ➖; cancel ➖ | ◐ nostr + cancel missing |
|
||||
| **keys** | `registerAction{action}`→`{actionId,binding?}`; `unregisterAction{actionId}`; pushes `keys.action{actionId}`, `keys.bindings{bindings[]}` | client-side no-op stubs only; **host rejects** `keys.*` (decodes to null) | 🔴 real napplets' `registerAction` rejects |
|
||||
| **upload** | `upload.upload{request:{data:Blob,mimeType?,...}}`→`{ok,uploadId,status,url?,sha256?,...}`; `upload.status` | type `upload` + `{bytes(base64),contentType}`→`{url}`; gateway null→Unsupported | 🔴 wrong type + shape + Blob transport |
|
||||
| **inc** | `inc.emit`; `inc.subscribe`/`.result`; `inc.unsubscribe`; `inc.event` push; channel mode (`inc.channel.*`) | ➖ (deferred; maps to null→denied) | ➖ not modeled |
|
||||
| **intent** | invoke a napplet by archetype | ➖ | ➖ |
|
||||
| **theme/notify/media/config/outbox/ifc/cvm** | further domains | ➖ | ➖ |
|
||||
|
||||
## Inconsistencies, ranked
|
||||
|
||||
1. **🔴 Shell handshake missing** (`shell.ready`→`shell.init`). Real napplets get an empty
|
||||
capability environment → `supports()` false → likely bail. *Fix: host emits `shell.init`.*
|
||||
2. **🔴 Id-less messages dropped** by the host. Breaks the handshake + every fire-and-forget
|
||||
(`inc.emit`, `keys.unregisterAction`). *Fix: forward/handle id-less messages.*
|
||||
3. **🔴 `keys.*` rejects** for real napplets (we only stub client-side). *Fix: decode
|
||||
`keys.registerAction`/`unregisterAction`, answer a stub `{actionId}`; later wire `keys.action`.*
|
||||
4. **🔴 `upload` non-conformant** (`upload` vs `upload.upload`, flat base64 vs `request:{data:Blob}}`,
|
||||
`{url}` vs rich `UploadResult`) AND the Blob can't cross our string bridge. *Fix: realign the
|
||||
wire + a Blob-aware request path when the gateway lands.*
|
||||
5. **◐ `relay.query`/`subscribe` use only the first of `filters[]`.** Multi-filter napplets get
|
||||
partial results. *Fix: honor all filters.*
|
||||
6. **◐ Identity `getList`/`getZaps`/`getBadges` + `onChanged`** unimplemented.
|
||||
7. **◐ `resource` `nostr:` scheme + `resource.cancel`** unimplemented.
|
||||
8. **◐ `relay.closed` push** not emitted; **live subscription tail** absent (snapshot only).
|
||||
9. **⚠️ Non-canonical `ok` on every result** (harmless, but not how the SDK signals success for
|
||||
identity/storage/query).
|
||||
|
||||
## What the conformance tests assert
|
||||
|
||||
`NappletSdkConformanceTest` (amethyst, JVM) pins the codec to the **exact SDK wire** for the
|
||||
methods we support, so a regression that drifts from `@napplet/nap` fails CI:
|
||||
|
||||
- **Requests:** the SDK's exact envelope (`relay.publish{event}`, `storage.get`, `identity.*`,
|
||||
`resource.bytes`, multi-`filters`) decodes to the right `NappletRequest`.
|
||||
- **Results:** `encodeResponse` emits the SDK's exact field names (`pubkey`, `profile`, `relays`,
|
||||
`pubkeys`, `entries`, `value`, `keys`, `events`, `event`/`eventId`, `bytes`/`mime`).
|
||||
- **Pushes:** `relay.event{subId,event}` / `relay.eose{subId}` match the SDK push shapes.
|
||||
- **Gap guards:** tests that *document current behavior* for the known gaps (`shell.ready`,
|
||||
`keys.registerAction`, `upload.upload`, `inc.emit` currently decode to `null`), each annotated
|
||||
with the audit item so the day we fix them the guard flips intentionally.
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.napplet
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Wire-contract conformance against `@napplet/nap@0.15.0` + `@napplet/core@0.15.0`. Each test
|
||||
* pins [NappletProtocolJson] to the SDK's *exact* request envelope and result field names, so a
|
||||
* regression that drifts from the published message types fails CI. The "gap guard" tests document
|
||||
* the known non-conformant surface (see `plans/2026-06-21-napplet-sdk-conformance-audit.md`): when
|
||||
* one of those is fixed, its guard flips intentionally and is updated alongside the fix.
|
||||
*
|
||||
* Field names are quoted from the canonical `<domain>/types.d.ts` message interfaces.
|
||||
*/
|
||||
class NappletSdkConformanceTest {
|
||||
private val json = Json
|
||||
|
||||
private fun result(
|
||||
requestType: String,
|
||||
response: NappletResponse,
|
||||
) = json.parseToJsonElement(NappletProtocolJson.encodeResponse(requestType, response)).jsonObject
|
||||
|
||||
private fun sampleEvent() =
|
||||
Event(
|
||||
id = "a".repeat(64),
|
||||
pubKey = "b".repeat(64),
|
||||
createdAt = 1_700_000_000L,
|
||||
kind = 1,
|
||||
tags = arrayOf(arrayOf("t", "x")),
|
||||
content = "gm",
|
||||
sig = "c".repeat(128),
|
||||
)
|
||||
|
||||
// ---------- relay (Relay*Message) ----------
|
||||
|
||||
@Test
|
||||
fun relayPublishRequestCarriesTheTemplateInTheEventField() {
|
||||
// RelayPublishMessage: { type:'relay.publish', id, event }
|
||||
val req = NappletProtocolJson.decodeRequest("""{"type":"relay.publish","id":"1","event":{"kind":1,"tags":[["t","x"]],"content":"gm"}}""")
|
||||
assertEquals(NappletRequest.Publish(1, arrayOf(arrayOf("t", "x")), "gm"), req)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayPublishEncryptedRequestMatches() {
|
||||
// RelayPublishEncryptedMessage: { type:'relay.publishEncrypted', id, event, recipient, encryption? }
|
||||
val req = NappletProtocolJson.decodeRequest("""{"type":"relay.publishEncrypted","id":"1","event":{"kind":4,"tags":[],"content":"hi"},"recipient":"pk","encryption":"nip04"}""")
|
||||
assertEquals(NappletRequest.PublishEncrypted(4, emptyArray(), "hi", "pk", "nip04"), req)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayQueryAndSubscribeReadTheFiltersArray() {
|
||||
// RelayQueryMessage: { type:'relay.query', id, filters: NostrFilter[] }
|
||||
val q = NappletProtocolJson.decodeRequest("""{"type":"relay.query","id":"1","filters":[{"kinds":[1],"authors":["aa"]}]}""") as NappletRequest.QueryEvents
|
||||
assertEquals(listOf(1), q.filter.kinds)
|
||||
assertEquals(listOf("aa"), q.filter.authors)
|
||||
|
||||
// RelaySubscribeMessage: { type:'relay.subscribe', id, subId, filters, relay? }
|
||||
val s = NappletProtocolJson.decodeRequest("""{"type":"relay.subscribe","id":"1","subId":"s1","filters":[{"kinds":[7]}]}""") as NappletRequest.Subscribe
|
||||
assertEquals(listOf(7), s.filter.kinds)
|
||||
// The subId the pushes are keyed by is read from the raw envelope by the service.
|
||||
assertEquals("s1", NappletProtocolJson.readSubId("""{"type":"relay.subscribe","id":"1","subId":"s1","filters":[{}]}"""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayPublishResultCarriesEventAndEventId() {
|
||||
// RelayPublishResultMessage: { type:'relay.publish.result', id, ok, event?, eventId?, error? }
|
||||
val o = result("relay.publish", NappletResponse.Published(sampleEvent(), listOf("wss://r")))
|
||||
assertEquals("relay.publish.result", o["type"]?.jsonPrimitive?.content)
|
||||
assertEquals(
|
||||
"a".repeat(64),
|
||||
o["event"]
|
||||
?.jsonObject
|
||||
?.get("id")
|
||||
?.jsonPrimitive
|
||||
?.content,
|
||||
)
|
||||
assertEquals("a".repeat(64), o["eventId"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayQueryResultCarriesEvents() {
|
||||
// RelayQueryResultMessage: { type:'relay.query.result', id, events, error? }
|
||||
val o = result("relay.query", NappletResponse.Events(listOf(sampleEvent())))
|
||||
assertEquals(1, o["events"]?.jsonArray?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayEventAndEosePushesMatchTheSdk() {
|
||||
// RelayEventMessage (PUSH): { type:'relay.event', subId, event }
|
||||
val ev = json.parseToJsonElement(NappletProtocolJson.encodeRelayEvent("s1", sampleEvent())).jsonObject
|
||||
assertEquals("relay.event", ev["type"]?.jsonPrimitive?.content)
|
||||
assertEquals("s1", ev["subId"]?.jsonPrimitive?.content)
|
||||
assertEquals(
|
||||
"a".repeat(64),
|
||||
ev["event"]
|
||||
?.jsonObject
|
||||
?.get("id")
|
||||
?.jsonPrimitive
|
||||
?.content,
|
||||
)
|
||||
|
||||
// RelayEoseMessage (PUSH): { type:'relay.eose', subId }
|
||||
val eose = json.parseToJsonElement(NappletProtocolJson.encodeRelayEose("s1")).jsonObject
|
||||
assertEquals("relay.eose", eose["type"]?.jsonPrimitive?.content)
|
||||
assertEquals("s1", eose["subId"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
// ---------- identity (Identity*Message) ----------
|
||||
|
||||
@Test
|
||||
fun identityGetPublicKeyKeepsItsOwnRequestAndPubkeyField() {
|
||||
assertEquals(NappletRequest.GetPublicKey, NappletProtocolJson.decodeRequest("""{"type":"identity.getPublicKey","id":"1"}"""))
|
||||
assertEquals("pk", result("identity.getPublicKey", NappletResponse.PublicKey("pk"))["pubkey"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun identityReadsDecodeGenericallyAndEncodeMethodSpecificFields() {
|
||||
// identity.getProfile/getRelays/getFollows/getMutes/getBlocked/getList → IdentityRead
|
||||
assertEquals(NappletRequest.IdentityRead("getProfile"), NappletProtocolJson.decodeRequest("""{"type":"identity.getProfile","id":"1"}"""))
|
||||
assertEquals(NappletRequest.IdentityRead("getList", "bookmarks"), NappletProtocolJson.decodeRequest("""{"type":"identity.getList","id":"1","listType":"bookmarks"}"""))
|
||||
|
||||
// Result field per @napplet/nap: getProfile→profile, getRelays→relays,
|
||||
// getFollows/getMutes/getBlocked→pubkeys, getList→entries.
|
||||
assertTrue(result("identity.getProfile", NappletResponse.Json("""{"name":"x"}""")).containsKey("profile"))
|
||||
assertTrue(result("identity.getRelays", NappletResponse.Json("""{"wss://r":{"read":true,"write":true}}""")).containsKey("relays"))
|
||||
assertTrue(result("identity.getFollows", NappletResponse.Json("""["aa"]""")).containsKey("pubkeys"))
|
||||
assertTrue(result("identity.getMutes", NappletResponse.Json("""["aa"]""")).containsKey("pubkeys"))
|
||||
assertTrue(result("identity.getBlocked", NappletResponse.Json("""["aa"]""")).containsKey("pubkeys"))
|
||||
assertTrue(result("identity.getList", NappletResponse.Json("""["aa"]""")).containsKey("entries"))
|
||||
}
|
||||
|
||||
// ---------- storage (Storage*Message) ----------
|
||||
|
||||
@Test
|
||||
fun storageWireTypesAndFieldsMatch() {
|
||||
// Wire types are storage.get/set/remove/keys (SDK functions getItem/setItem/removeItem/keys).
|
||||
assertEquals(NappletRequest.StorageGet("k"), NappletProtocolJson.decodeRequest("""{"type":"storage.get","id":"1","key":"k"}"""))
|
||||
assertEquals(NappletRequest.StorageSet("k", "v"), NappletProtocolJson.decodeRequest("""{"type":"storage.set","id":"1","key":"k","value":"v"}"""))
|
||||
assertEquals(NappletRequest.StorageRemove("k"), NappletProtocolJson.decodeRequest("""{"type":"storage.remove","id":"1","key":"k"}"""))
|
||||
assertEquals(NappletRequest.StorageKeys, NappletProtocolJson.decodeRequest("""{"type":"storage.keys","id":"1"}"""))
|
||||
|
||||
// StorageGetResultMessage.value, StorageKeysResultMessage.keys
|
||||
assertTrue(result("storage.get", NappletResponse.StorageValue("v")).containsKey("value"))
|
||||
assertEquals(2, result("storage.keys", NappletResponse.Strings(listOf("a", "b")))["keys"]?.jsonArray?.size)
|
||||
}
|
||||
|
||||
// ---------- resource (Resource*Message) ----------
|
||||
|
||||
@Test
|
||||
fun resourceBytesRequestAndResultMatch() {
|
||||
assertEquals(NappletRequest.ResourceBytes("https://x"), NappletProtocolJson.decodeRequest("""{"type":"resource.bytes","id":"1","url":"https://x"}"""))
|
||||
// The host emits base64 bytes + mime; shell.html rebuilds the Blob the SDK expects.
|
||||
val o = result("resource.bytes", NappletResponse.Bytes("Hi".encodeToByteArray(), "text/plain"))
|
||||
assertEquals("SGk=", o["bytes"]?.jsonPrimitive?.content)
|
||||
assertEquals("text/plain", o["mime"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
// ---------- error convention ----------
|
||||
|
||||
@Test
|
||||
fun failuresExposeAnErrorFieldTheSdkShimRejectsOn() {
|
||||
// The SDK shim rejects when `error` is present, regardless of domain.
|
||||
assertTrue(result("relay.publish", NappletResponse.Denied(NappletCapability.RELAY, "no")).containsKey("error"))
|
||||
assertTrue(result("identity.getProfile", NappletResponse.Unsupported("identity.getProfile")).containsKey("error"))
|
||||
assertTrue(result("relay.query", NappletResponse.Failed("boom")).containsKey("error"))
|
||||
}
|
||||
|
||||
// ---------- gap guards (documented non-conformance; flip these when fixed) ----------
|
||||
|
||||
@Test
|
||||
fun gapShellHandshakeIsNotHandled() {
|
||||
// SDK posts shell.ready (no id) and expects a shell.init reply; we don't model it yet.
|
||||
// Audit item #1. NOTE: our codec also doesn't recognize shell.ready as a request.
|
||||
assertNull(NappletProtocolJson.decodeRequest("""{"type":"shell.ready"}"""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gapKeysActionsAreNotHandledAtTheBoundary() {
|
||||
// SDK: keys.registerAction / keys.unregisterAction. We only stub them client-side, so a real
|
||||
// napplet's registerAction currently rejects. Audit item #3.
|
||||
assertNull(NappletProtocolJson.decodeRequest("""{"type":"keys.registerAction","id":"1","action":{"id":"a","label":"A"}}"""))
|
||||
assertNull(NappletProtocolJson.decodeRequest("""{"type":"keys.unregisterAction","actionId":"a"}"""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gapUploadWireIsNonConformant() {
|
||||
// SDK sends upload.upload with a Blob in request.data; we only decode a non-standard `upload`.
|
||||
// Audit item #4.
|
||||
assertNull(NappletProtocolJson.decodeRequest("""{"type":"upload.upload","id":"1","request":{"mimeType":"image/png"}}"""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gapIncIsNotModeled() {
|
||||
// inter-napplet comms (inc.emit / inc.subscribe / inc.event) are not modeled. Audit item #6/➖.
|
||||
assertNull(NappletProtocolJson.decodeRequest("""{"type":"inc.emit","topic":"t"}"""))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user