Back to Body Journal
SOURCE / PINNED RELEASE

Made of little things.

Body Journal

Release
e8049f8da4e2…
Author-recorded commit
61c04bf0e8aa…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7

Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

.agents/skills/napplet-test/SKILL.md
---
name: napplet-test
description: Use to verify a napplet before publishing - build, run protocol conformance with napplet-conformance (real Chromium, reference shell), interpret failures, audit the OUTBOX-first boundaries and forbidden browser-authority surfaces, confirm the single-file artifact, run the applet UI checks (visual direction, four frame sizes, chosen theme policy, minimum notice), scenario smoke tests, Paja preview, and CI wiring. Run after napplet-build, before shipping.
---

# Testing a Napplet

Conformance proves the build speaks NIP-5D inside a real `sandbox="allow-scripts"` iframe driven by a reference shell, catching malformed envelopes, manifest problems, boot failures, and forbidden browser-authority references locally. Truth: NIP-5D (<https://github.com/nostr-protocol/nips/pull/2303>) and the NAPs (<https://github.com/napplet/naps>). A check that fails a spec-faithful napplet but passes only this toolchain is a tooling bug — flag it; do not work around it.

## Step 1 — Build, then test the build

```bash
pnpm verify              # template guidance test + type-check + single-file build
pnpm test:conformance    # napplet-conformance ./dist → exit 0 conformant, 1 non-conformant, 2 usage/runtime error
npx napplet-conformance ./dist        # package-manager agnostic
napplet-conformance --ui ./dist       # live visual report
```

Conformance runs against `./dist` (prefers `dist/index.html`). Never test source.

## Step 2 — Interpret failures (fix the napplet, not the check)

- **Boot failure** — the runtime did not inject `window.napplet` before app code ran, or a top-level throw blocked boot.
- **Malformed envelope** — a message that is not a valid `{ type: "domain.action", … }` for its NAP; re-check the SDK call arguments.
- **Manifest problem** — check the standalone HTML SHA-256 `x`, nonempty plain-text content and required `R` domains. Only named events carry `d`; `O` is optional. Older `path`/aggregate manifests remain readable. See docs/napplet-manifest.md; do not rewrite signed events or weaken host verification.
- **Forbidden global** — the bundle references `fetch`, `localStorage`, `window.nostr`, `XMLHttpRequest`, `WebSocket`, … (static scan; unreachable references flag too). Replace with `resource` / `storage` / `outbox`, or prove and document a tooling false positive.

## Step 3 — Boundary audit

| Check | Expected |
| --- | --- |
| Normal social reads / publishes | `outbox`, `common`, `lists`, `count`, `dm` — not default `relay` |
| Any `relay.subscribe` / `relay.publish` | A comment or spec note naming the relay-local escape hatch |
| Signing / encryption | No `window.nostr`, keys, local signing, app-owned encryption |
| Relay routing | No NIP-65 resolver, relay pool, WebSocket client, fanout policy in the napplet |
| Network and media bytes | No `fetch`, XHR, WebSocket, external `<img src>`; `resource` instead |
| Persistence | No `localStorage`, IndexedDB, cookies; `storage` instead |
| Optional-domain gating | `window.napplet?.domain` after injection; never wait on optional `shell.ready()` / `shell.supports()` or service probes |
| `requires` / `optional` | Bare domain names for every used integration; required and optional lists match the design and deployment event |

```bash
grep -RInE "fetch\\s*\\(|XMLHttpRequest|WebSocket|localStorage|sessionStorage|indexedDB|document\\.cookie|window\\.nostr|<img[^>]+src=['\\\"]?https?:|<script[^>]+src=['\\\"]?https?:|<link[^>]+href=['\\\"]?https?:" src dist index.html
```

Any hit in authored or bundled napplet code is removed or explained as a tooling false positive before shipping.

## Step 4 — Artifact

`dist/index.html` is one self-contained file: inline JS, folded assets, no external `<script src>` (opaque-origin `srcdoc` has nothing to fetch). `artifactMode: 'single-file'` produces it; verify by inspecting the file, not by trusting the config.

### Check the event that will be published

Run `soyli publish --dry-run --json` and inspect the final manifest template, not only the Vite config or runtime preview. Compare its `R` and `O` domains with the design and SDK usage, including optional theme, keyboard, persistence and social integrations. After signing, confirm the same declarations remain in the signed event. This is an authoring verification step following [NIP-5D, Manifest and Required and Optional Capabilities](https://github.com/nostr-protocol/nips/pull/2303); declarations do not grant runtime permissions.

If the build declares capabilities but the deployment drops or reclassifies them, report a publishing-tool defect. Legacy serialization cannot preserve the required/optional distinction; use current-format tooling when that distinction is needed. Do not claim the integration is complete from a successful preview alone.

## Step 5 — Visual and responsive checks (napplet-ui)

Use the actual hosted frame at 200×160, 320×560, 900×600 and full screen, plus
the touch checks in docs/napplet-mobile.md. Check the main interaction, UI/HUD,
legibility, focus, reduced motion, state feedback and any minimum-size notice.
Large frames should have intentional composition or useful additional workspace.

Compare with the project brief using docs/napplet-visual-design.md: remove unintended
demo styling. Switch host light/dark and remove the optional theme domain.
App-owned colors remain intact; host-matched surfaces update consistently; hybrids
change only chosen regions. Inspect CSS and rendered game/artwork colors. Record
frames, theme policy, interactions and gaps, including actual cover/clip review.

## Step 6 — Scenario smoke tests

- **Signed-out:** `identity.getPublicKey()` returns `""`; publish / list / DM actions degrade with an inline state.
- **OUTBOX options:** only current fields — `getEvent`: `author`, `relays`, `timeoutMs`; `query` / `subscribe`: `authors`, `relays`, `limit`, `timeoutMs`; `publish`: `relays`, `toOutbox`, `toInboxes`. No `strategy`, subscribe `live`, publish `timeoutMs`, or `outbox.eose`.
- **Optional domain absent:** remove it from the mock runtime; the affordance hides or falls back, nothing else breaks.
- **Keyboard actions:** for maximum compatibility, check that every keyboard-bound action is registered through [NAP-KEYS](https://github.com/napplet/naps/pull/9), including app-local shortcuts and game controls. Test the shell-assigned binding and a changed binding, confirm the registered action runs without duplicate local hotkey handling, and exercise the fallback with `keys` absent. Ordinary text input still works. This is a non-normative authoring check; the living proposal defines protocol conformance.
- **Theme change:** emit a theme change; verify the chosen policy. App-owned art stays intact, opted-in UI updates coherently, and gameplay/state does not reset.
- **Escape hatch:** if `relay` is used, exercise the relay-local behavior and teardown.
- **Teardown:** subscriptions, key bindings, and object URLs are released when the view goes away.

## Step 7 — Runtime preview

Use `soyli dev` and its printed host URL, then `soyli check`. Stop the preview session you started. Separately load the same artifact in an independent shell such as Paja. Record missing runtime support as unverified, not a passing conformance result.

## Step 8 — CI

```yaml
- run: pnpm build
- run: npx napplet-conformance ./dist   # non-zero exit fails the job
```

Cache Playwright's Chromium. Review actual checks, skipped cases, final manifest and tested behavior before publication. A startup pass alone is not feature or independent-shell acceptance.

## Optional controller input

NAP-GAMEPAD is not implemented by this soyLI host or its pinned SDK. Do not import an unavailable gamepad module or bypass the sandbox with native controller APIs. Keep keyboard/pointer controls and consult docs/napplet-controllers.md for the existing host-remapped controller helper. Recheck the living proposal and target runtime before claiming native GAMEPAD support.

## soyLI manifest metadata

Before publication or changing capabilities/discovery, read docs/napplet-manifest.md. Save metadata in napplet.json; the publisher creates the signed event. Advertise only implemented intents and optional integrations, and verify their absent-domain fallbacks.