Skip to content

Commit aad062e

Browse files
committed
review: address SHIP-WITH-NITS findings on the mock host
1 parent 0edecc0 commit aad062e

8 files changed

Lines changed: 85 additions & 14 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,18 @@ jobs:
171171
npm run build --prefix js/packages/truapi
172172
173173
- name: Install wasm-pack
174-
run: cargo install wasm-pack --locked
174+
run: cargo install wasm-pack --version 0.15.0 --locked
175175

176176
- name: Build WASM
177177
run: npm run build:wasm --prefix js/packages/truapi-host-wasm
178178

179179
- name: Test
180180
run: bun test
181181
working-directory: js/packages/truapi-host-wasm
182+
# Fail loudly if the WASM artifact is missing so the bridge test can't
183+
# silently skip green after a build:wasm drift.
184+
env:
185+
REQUIRE_WASM: "1"
182186

183187
playground:
184188
name: Playground (build + lint)

js/packages/truapi-host-wasm/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ const provider = await createWebWorkerProvider(new HostWorker(), callbacks, {
4343
`@parity/truapi-host-wasm/web` also exports `createIframeHost` for the protocol-iframe
4444
MessageChannel handshake.
4545

46+
## Testing — `createMockHost`
47+
48+
`@parity/truapi-host-wasm/web` exports `createMockHost`, an in-memory implementation of the
49+
full generated `HostCallbacks` surface — the JS sibling of `truapi-platform`'s `MockPlatform`.
50+
Feed its callbacks to `createWebWorkerProvider` to run the **real WASM core** against a mocked
51+
OS seam: storage is in-memory, permissions answer from a fixed policy, navigation and
52+
notifications are recorded, and the chain connection is silent (or replays canned frames).
53+
54+
```ts
55+
import {
56+
createMockHost,
57+
mockRuntimeConfig,
58+
} from "@parity/truapi-host-wasm/web";
59+
60+
const mock = createMockHost(); // optional MockHostConfig
61+
const provider = await createWebWorkerProvider(
62+
new HostWorker(),
63+
mock.callbacks,
64+
{
65+
runtimeConfig: mockRuntimeConfig(),
66+
},
67+
);
68+
// the real WASM core now talks to a mocked OS; assert via mock.navigations(), etc.
69+
```
70+
71+
Coverage of the callback surface is `tsc`-enforced: the callbacks object is typed
72+
`Required<HostCallbacks>`, so a capability added to the generated surface fails the type
73+
check until the mock covers it. `wasm-bridge.test.ts` drives the real WASM core against the
74+
mock headlessly (no browser, no worker). Public API: `createMockHost`, `mockRuntimeConfig`,
75+
`MockHost`, `MockHostConfig`, `PermissionPolicy`.
76+
4677
## Publishing
4778

4879
The npm publish workflow is not wired yet. A release-process discussion is needed before adding a

js/packages/truapi-host-wasm/src/web/create-mock-host.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("createMockHost callbacks", () => {
2020
const { callbacks } = createMockHost();
2121
const key: CoreStorageKey = {
2222
tag: "PermissionAuthorization",
23-
value: { storageKey: "cam" },
23+
value: { productId: "p", request: { tag: "Device", value: "Camera" } },
2424
};
2525
await callbacks.writeCoreStorage(key, new Uint8Array([9]));
2626
expect(await callbacks.readCoreStorage(key)).toEqual(new Uint8Array([9]));

js/packages/truapi-host-wasm/src/web/create-mock-host.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ export function createMockHost(config: MockHostConfig = {}): MockHost {
130130
: `core:${key.tag}`;
131131
const granted = (policy: PermissionPolicy): boolean => policy === "allow-all";
132132

133-
const callbacks: HostCallbacks = {
133+
// `Required<HostCallbacks>` (not bare `HostCallbacks`): every optional callback
134+
// must be present, so a capability added to the generated surface fails `tsc`
135+
// here until the mock covers it. This is the load-bearing coverage guarantee.
136+
const callbacks: Required<HostCallbacks> = {
134137
// ProductStorage
135138
async read(key) {
136139
return storage.get(productKey(key));
@@ -196,7 +199,10 @@ export function createMockHost(config: MockHostConfig = {}): MockHost {
196199
await new Promise<never>(() => {});
197200
}
198201
},
199-
// The mock holds no real transport; releasing the lease is a no-op.
202+
// The mock holds no real transport, so releasing the lease is a no-op.
203+
// Note: a Silent connection whose `responses()` stream is already parked
204+
// stays parked after close() — tests that need the stream to terminate use
205+
// `chainClosed` (or scripted frames), not close().
200206
close() {},
201207
};
202208
},

js/packages/truapi-host-wasm/src/web/wasm-bridge.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ const wasmUrl = new URL("../../dist/wasm/web/truapi_server_bg.wasm", import.meta
1212
const glueUrl = new URL("../../dist/wasm/web/truapi_server.js", import.meta.url);
1313
const built = existsSync(wasmUrl);
1414

15+
// The `host-wasm` CI job builds the WASM first and sets REQUIRE_WASM=1, so a
16+
// missing artifact (a silent `build:wasm` path/output drift) fails loudly here
17+
// instead of skipping green. A plain local `bun test` leaves REQUIRE_WASM unset
18+
// and skips this suite cleanly on a fresh checkout.
19+
if (process.env.REQUIRE_WASM === "1" && !built) {
20+
throw new Error(
21+
`REQUIRE_WASM=1 but the WASM artifact is missing at ${wasmUrl.pathname} — run \`npm run build:wasm\` first.`,
22+
);
23+
}
24+
1525
const suite = built ? describe : describe.skip;
1626

1727
suite("real WASM core ↔ createMockHost bridge", () => {

rust/crates/truapi-platform/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,20 @@ exposed through the trait signatures below.
3333

3434
`Platform` is a blanket-implemented supertrait that combines the capability
3535
traits above.
36+
37+
## Mock platform (`mock` feature)
38+
39+
The `mock` feature — a dev-dependency, excluded from the default and production
40+
builds — provides `MockPlatform`, a config-driven, in-memory implementation of all
41+
the capability traits above. It is the canonical seam mock: plug it into the real
42+
`truapi-server` core (via `TrUApiCore::from_platform_with_config`) to exercise the
43+
production dispatcher without a device or a paired wallet.
44+
45+
`MockConfig` drives its behavior — `PermissionPolicy` (`AllowAll`/`DenyAll`, device
46+
and remote separately), `ChainBehavior` (`Silent` | `Scripted` | `Closed` |
47+
`ConnectError`), `MockFaults` for fault injection, and confirmation control — and it
48+
records what the core asked the device to do (navigations, notifications,
49+
confirmations, auth-state transitions, sent RPC) for assertions. Storage is
50+
namespaced in-memory (`product:` / `core:`); preimages round-trip via submit → lookup.
51+
See the `from_mock_platform_*` through-core tests in `truapi-server` for end-to-end
52+
usage.

rust/crates/truapi-platform/src/mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,8 @@ impl JsonRpcConnection for MockConnection {
462462
}
463463
}
464464

465+
// No real transport to release. A `None` (silent) `responses()` stream stays
466+
// pending after close(); scripted/`Closed` behaviors terminate on their own.
465467
fn close(&self) {}
466468
}
467469

rust/crates/truapi-server/src/core.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ mod tests {
382382
response.payload.value
383383
};
384384

385-
// Default mock supports the feature: [Ok 0x00][V1 0x00][supported=1].
385+
// Default mock supports the feature: [V1 0x00][Ok 0x00][supported=1].
386386
assert_eq!(dispatch(MockPlatform::new()), vec![0x00, 0x00, 0x01]);
387387
// A configured "unsupported" answer flows through the same dispatcher.
388388
let unsupported = MockPlatform::with_config(MockConfig {
@@ -441,15 +441,15 @@ mod tests {
441441
key: "k".into(),
442442
value: vec![1, 2, 3],
443443
});
444-
// Ok 0x00, V1 0x00.
444+
// V1 0x00, Ok 0x00.
445445
assert_eq!(
446446
run_request(&core, "local_storage_write", write.encode()),
447447
vec![0x00, 0x00]
448448
);
449449

450450
let read =
451451
HostLocalStorageReadRequest::V1(v01::HostLocalStorageReadRequest { key: "k".into() });
452-
// Ok 0x00, V1 0x00, Some 0x01, compact-len(3) 0x0c, bytes.
452+
// V1 0x00, Ok 0x00, Some 0x01, compact-len(3) 0x0c, bytes.
453453
assert_eq!(
454454
run_request(&core, "local_storage_read", read.encode()),
455455
vec![0x00, 0x00, 0x01, 0x0c, 1, 2, 3]
@@ -462,7 +462,7 @@ mod tests {
462462
vec![0x00, 0x00]
463463
);
464464

465-
// After clear the read misses: Ok 0x00, V1 0x00, None 0x00.
465+
// After clear the read misses: V1 0x00, Ok 0x00, None 0x00.
466466
let read_again =
467467
HostLocalStorageReadRequest::V1(v01::HostLocalStorageReadRequest { key: "k".into() });
468468
assert_eq!(
@@ -483,7 +483,7 @@ mod tests {
483483
let request =
484484
|| HostDevicePermissionRequest::V1(v01::HostDevicePermissionRequest::Camera).encode();
485485

486-
// AllowAll (default): Ok 0x00, V1 0x00, granted=1.
486+
// AllowAll (default): V1 0x00, Ok 0x00, granted=1.
487487
let allow = make_mock_core(MockConfig::default());
488488
assert_eq!(
489489
run_request(&allow, "permissions_request_device_permission", request()),
@@ -501,9 +501,10 @@ mod tests {
501501
);
502502
}
503503

504-
/// Preimage submit flows through the core's confirm gate to the platform:
505-
/// the default mock auto-confirms (Ok envelope), and a `confirm = false`
506-
/// mock is rejected by the core before reaching the platform (Err envelope).
504+
/// Preimage submit flows through the core's confirm gate: the default mock
505+
/// auto-confirms (Ok envelope), and a `confirm = false` mock is rejected by
506+
/// the core after the confirmation prompt but before the preimage backend
507+
/// (`PreimageHost::submit_preimage`) is reached (Err envelope).
507508
#[test]
508509
fn from_mock_platform_preimage_submit_through_core() {
509510
use truapi::versioned::preimage::RemotePreimageSubmitRequest;
@@ -523,8 +524,8 @@ mod tests {
523524
assert_eq!(ok_payload.first(), Some(&0x00));
524525
assert_eq!(ok_payload.get(1), Some(&0x00));
525526

526-
// confirm_user_actions = false: the core rejects before the platform
527-
// (V1 index 0x00, Err 0x01).
527+
// confirm_user_actions = false: the core rejects after the confirmation
528+
// prompt, before the preimage backend (V1 index 0x00, Err 0x01).
528529
let rejected = make_mock_core(MockConfig {
529530
confirm_user_actions: false,
530531
..Default::default()

0 commit comments

Comments
 (0)