···11+# MM-145 Test Analysis and Human Test Plan
22+33+## Coverage Validation
44+55+**Automated Criteria:** 9 | **Covered:** 9 | **Missing:** 0
66+77+### Covered
88+99+| Criterion | Test File | Verifies |
1010+|-----------|-----------|----------|
1111+| MM-145.AC1.1 `get_or_create_returns_valid_multibase` | `apps/identity-wallet/src-tauri/src/device_key.rs:283` | Calls `get_or_create()`, asserts multibase starts with `'z'`, decodes via `multibase::decode` and asserts decoded length is 33 bytes (compressed P-256 SEC1 point). |
1212+| MM-145.AC1.2 `get_or_create_is_idempotent` | `apps/identity-wallet/src-tauri/src/device_key.rs:295` | Calls `get_or_create()` twice, asserts both `multibase` and `key_id` are identical across calls. |
1313+| MM-145.AC1.3 `key_id_has_did_key_prefix` | `apps/identity-wallet/src-tauri/src/device_key.rs:307` | Calls `get_or_create()`, asserts `key_id` starts with `"did:key:z"`. |
1414+| MM-145.AC3.1 `sign_returns_64_bytes` | `apps/identity-wallet/src-tauri/src/device_key.rs:318` | Ensures key exists, calls `sign(b"test payload")`, asserts result length is exactly 64 bytes. |
1515+| MM-145.AC3.2 `sign_is_deterministic` | `apps/identity-wallet/src-tauri/src/device_key.rs:326` | Calls `sign()` twice with identical data, asserts both signatures are byte-identical (RFC 6979). |
1616+| MM-145.AC3.3 `sign_before_generate_returns_key_not_found` | `apps/identity-wallet/src-tauri/src/device_key.rs:338` | Deletes Keychain entry to simulate fresh state, calls `sign()`, asserts `Err(DeviceKeyError::KeyNotFound)`. |
1717+| MM-145.AC4.1 `device_key_error_serializes_as_code` | `apps/identity-wallet/src-tauri/src/device_key.rs:351` | Serializes `KeyGenerationFailed`, `KeyNotFound`, `KeychainError` to JSON; asserts `{ "code": "SCREAMING_SNAKE_CASE" }` format. |
1818+| MM-145.AC4.1 `device_public_key_serializes_camel_case` | `apps/identity-wallet/src-tauri/src/device_key.rs:371` | Constructs `DevicePublicKey`, serializes to JSON, asserts `key_id` appears as `keyId` and snake_case `key_id` is absent. |
1919+| MM-145.AC5.1 `create_account_uses_device_key_public_key` | `apps/identity-wallet/src-tauri/src/lib.rs:339` | Calls `device_key::get_or_create()` (the same function `create_account` uses), asserts multibase format and idempotency. |
2020+2121+### Missing
2222+2323+(none)
2424+2525+**Result: PASS**
2626+2727+---
2828+2929+## Human Test Plan
3030+3131+### Prerequisites
3232+3333+- macOS Ventura (13) or later with Xcode installed (latest stable)
3434+- iOS Simulator platform installed (Xcode > Settings > Platforms > iOS)
3535+- Cocoapods installed (`sudo gem install cocoapods`)
3636+- Nix dev shell active from workspace root: `nix develop --impure --accept-flake-config`
3737+- Frontend dependencies installed: `cd apps/identity-wallet && pnpm install`
3838+- Xcode project generated: `cargo tauri ios init` (with PATH patched per CLAUDE.md instructions)
3939+- For physical device tests (AC2.1, AC2.2): an Apple Developer account and a physical iOS device with Secure Enclave
4040+- Automated tests passing: `cargo test -p identity-wallet -- --test-threads=1`
4141+4242+### Phase 1: Key Generation (Simulator)
4343+4444+| Step | Action | Expected |
4545+|------|--------|----------|
4646+| 1.1 | From `apps/identity-wallet/`, run `cargo tauri ios dev` to launch the app in the iOS Simulator | App builds and opens in Simulator; Vite HMR connects successfully |
4747+| 1.2 | In a Svelte component or browser console, call `getOrCreateDeviceKey()` (import from `$lib/ipc.ts`) | Promise resolves with `{ multibase: "z...", keyId: "did:key:z..." }` where `multibase` starts with `z` and `keyId` starts with `did:key:z` |
4848+| 1.3 | Call `getOrCreateDeviceKey()` a second time | Returns the exact same `multibase` and `keyId` values as step 1.2 |
4949+| 1.4 | Force-quit the Simulator app (swipe up in app switcher), relaunch, call `getOrCreateDeviceKey()` again | Returns the exact same `multibase` and `keyId` values as steps 1.2/1.3 (key persists in macOS Keychain used by Simulator) |
5050+5151+### Phase 2: Secure Enclave Persistence (Physical Device)
5252+5353+| Step | Action | Expected |
5454+|------|--------|----------|
5555+| 2.1 | Build and deploy to a physical iOS device via `cargo tauri ios dev` (connected USB or same network). Call `getOrCreateDeviceKey()` and record the returned `multibase` string | Returns a `DevicePublicKey` with multibase starting with `z` |
5656+| 2.2 | Force-kill the app from the iOS multitasking view (swipe up). Wait 5 seconds. Relaunch the app from the home screen | App opens normally |
5757+| 2.3 | Call `getOrCreateDeviceKey()` again | Returns the identical `multibase` and `keyId` from step 2.1 (SE key tag and Keychain metadata survived cold restart) |
5858+| 2.4 | Attempt to call `SecKey.external_representation()` on the SE private key (requires a debug breakpoint or test harness in the SE code path) | Returns `None` / nil -- the Secure Enclave hardware refuses to export the private key. This is a hardware-level guarantee confirming the non-extractable property (AC2.2) |
5959+6060+### Phase 3: Signing (Simulator)
6161+6262+| Step | Action | Expected |
6363+|------|--------|----------|
6464+| 3.1 | In the running Simulator app, call `signWithDeviceKey(new Uint8Array([1, 2, 3]))` from a Svelte component or browser console via `ipc.ts` | Promise resolves with a `Uint8Array` of exactly 64 bytes (raw r\|\|s ECDSA signature) |
6565+| 3.2 | Call `signWithDeviceKey(new Uint8Array([1, 2, 3]))` again with the same input | Returns the exact same 64-byte array (RFC 6979 deterministic on simulator path) |
6666+| 3.3 | On a fresh Simulator install (delete the app, reinstall), call `signWithDeviceKey(new Uint8Array([1, 2, 3]))` before calling `getOrCreateDeviceKey()` | Promise rejects with `{ code: "KEY_NOT_FOUND" }` |
6767+6868+### Phase 4: TypeScript IPC Contract (Simulator)
6969+7070+| Step | Action | Expected |
7171+|------|--------|----------|
7272+| 4.1 | In a Svelte component, import `getOrCreateDeviceKey` from `$lib/ipc.ts` and call it | TypeScript compiler accepts the call with no type errors; result type is `DevicePublicKey` with fields `multibase: string` and `keyId: string` |
7373+| 4.2 | In a Svelte component, import `signWithDeviceKey` from `$lib/ipc.ts` and call with `new Uint8Array([1,2,3])` | TypeScript compiler accepts the call; result type is `Promise<Uint8Array>`. At runtime, the resolved value is a real `Uint8Array` (not a number array) |
7474+| 4.3 | Verify the `ipc.ts` `signWithDeviceKey` function converts `Uint8Array` to `number[]` before calling `invoke()` and converts the `number[]` response back to `Uint8Array` | Inspect lines 103-106 of `apps/identity-wallet/src/lib/ipc.ts`: `Array.from(data)` on input, `new Uint8Array(bytes)` on output (workaround for tauri#10336) |
7575+| 4.4 | Trigger `KEY_NOT_FOUND` per step 3.3, catch the rejected promise, confirm the error object has shape `{ code: "KEY_NOT_FOUND" }` matching the `DeviceKeyError` type definition in `ipc.ts` | Error code is one of the 5 values in the `DeviceKeyError.code` union type |
7676+7777+### End-to-End: Account Creation with Device Key
7878+7979+**Purpose:** Validates that `create_account` correctly wires `device_key::get_or_create()` as the public key source for the relay request, and that the full onboarding flow works through the frontend.
8080+8181+| Step | Action | Expected |
8282+|------|--------|----------|
8383+| E2E.1 | Start the relay locally (`cargo run -p relay` or via Nix) with a valid admin token configured | Relay running at `http://localhost:8080` |
8484+| E2E.2 | Create a claim code via the relay admin API (POST to `/v1/admin/claim-codes` with the admin token) | Receive a claim code string |
8585+| E2E.3 | Launch the identity-wallet app in the Simulator via `cargo tauri ios dev` | App opens to the Welcome screen |
8686+| E2E.4 | Navigate through onboarding: Welcome > tap Start > enter the claim code from E2E.2 > enter an email > enter a handle > tap Create | App transitions from Loading screen. The Rust backend calls `device_key::get_or_create()` to obtain the multibase key, then POSTs to `/v1/accounts/mobile` with `devicePublicKey` set to that multibase value |
8787+| E2E.5 | Verify the relay received the account creation request | Check relay logs or query the relay's account list -- the account exists with the device public key matching the multibase from `getOrCreateDeviceKey()` |
8888+| E2E.6 | Verify Keychain tokens were stored | In the Simulator app, the `create_account` success path stored `device-token` and `session-token` in Keychain. A subsequent authenticated relay call using these tokens should succeed |
8989+9090+### End-to-End: Error Mapping through Frontend
9191+9292+**Purpose:** Validates that relay error codes propagate correctly through the Rust backend to the TypeScript frontend as typed error objects.
9393+9494+| Step | Action | Expected |
9595+|------|--------|----------|
9696+| ERR.1 | Submit the onboarding form with an already-redeemed claim code | Frontend catches `{ code: "EXPIRED_CODE" }` and navigates back to the claim code screen with an appropriate error message |
9797+| ERR.2 | Submit the onboarding form with an email already registered on the relay | Frontend catches `{ code: "EMAIL_TAKEN" }` and navigates back to the email screen |
9898+| ERR.3 | Submit the onboarding form with a handle already taken on the relay | Frontend catches `{ code: "HANDLE_TAKEN" }` and navigates back to the handle screen |
9999+| ERR.4 | Submit the onboarding form while the relay is stopped | Frontend catches `{ code: "NETWORK_ERROR", message: "..." }` |
100100+101101+### Human Verification Required
102102+103103+| Criterion | Why Manual | Steps |
104104+|-----------|------------|-------|
105105+| MM-145.AC1.4 (Key persists across app restarts) | Requires real app lifecycle (cold restart) that `cargo test` cannot simulate; the automated `get_or_create_is_idempotent` test covers in-process idempotency but not cross-process persistence | Phase 1 steps 1.3-1.4 (Simulator) and Phase 2 steps 2.1-2.3 (physical device) |
106106+| MM-145.AC2.1 (SE key survives cold restart on real device) | Requires physical iOS device with Secure Enclave hardware; the macOS/Simulator software path used by `cargo test` does not exercise the SE code path | Phase 2 steps 2.1-2.3 |
107107+| MM-145.AC2.2 (SE private key non-extractable) | Hardware guarantee of Apple Secure Enclave; cannot be verified in software-only tests; requires physical device | Phase 2 step 2.4 |
108108+| MM-145.AC4.2 (Frontend IPC type correctness) | Validates real Tauri IPC serialization/deserialization round-trip between Rust and TypeScript at runtime; `cargo test` only tests Rust serde, not the full IPC bridge | Phase 4 steps 4.1-4.4 and Phase 3 step 3.3 |
109109+110110+### Traceability
111111+112112+| Acceptance Criterion | Automated Test | Manual Step |
113113+|----------------------|----------------|-------------|
114114+| MM-145.AC1.1 (valid multibase output) | `get_or_create_returns_valid_multibase` in `device_key.rs:283` | Phase 1 step 1.2 |
115115+| MM-145.AC1.2 (idempotent across calls) | `get_or_create_is_idempotent` in `device_key.rs:295` | Phase 1 step 1.3 |
116116+| MM-145.AC1.3 (did:key prefix) | `key_id_has_did_key_prefix` in `device_key.rs:307` | Phase 1 step 1.2 |
117117+| MM-145.AC1.4 (persists across restarts) | -- | Phase 1 step 1.4, Phase 2 steps 2.1-2.3 |
118118+| MM-145.AC2.1 (SE persistence on real device) | -- | Phase 2 steps 2.1-2.3 |
119119+| MM-145.AC2.2 (SE non-extractable guarantee) | -- | Phase 2 step 2.4 |
120120+| MM-145.AC3.1 (64-byte signature) | `sign_returns_64_bytes` in `device_key.rs:318` | Phase 3 step 3.1 |
121121+| MM-145.AC3.2 (deterministic signing) | `sign_is_deterministic` in `device_key.rs:326` | Phase 3 step 3.2 |
122122+| MM-145.AC3.3 (KeyNotFound before generate) | `sign_before_generate_returns_key_not_found` in `device_key.rs:338` | Phase 3 step 3.3 |
123123+| MM-145.AC4.1 (error/struct serialization) | `device_key_error_serializes_as_code` + `device_public_key_serializes_camel_case` in `device_key.rs:351,371` | Phase 4 steps 4.1-4.4 |
124124+| MM-145.AC4.2 (frontend IPC types correct) | -- | Phase 4 steps 4.1-4.4 |
125125+| MM-145.AC5.1 (create_account uses device key) | `create_account_uses_device_key_public_key` in `lib.rs:339` | E2E steps E2E.4-E2E.5 |