aboutsummaryrefslogtreecommitdiff
path: root/docs/TESTING.md
blob: 6063740a5849f7a93cf2b3a7c5c1e685b2799f8a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
<!--
  Author:  cirakg & claude ai
  Summary: Test strategy, how to run every check in Docker, current results, and the real-watch checklist.
-->
# Testing

The full test plan is in [README section 17](../README.md#17-test-plan). This document tracks what exists, how to run it, and the latest results. Every check runs in Docker; `make ci` runs them all in order and stops at the first failure.

## Test layers

| Layer | Command | What it proves |
| --- | --- | --- |
| Python tool tests | `make test` (first part) | The tools that judge everything else are correct: test-log parser, screenshot checker, budget checker, product rule, link checker, linter, icon generator. Includes Hypothesis fuzzing of the log parser and integration checks against the real docs and sources |
| Monkey C unit tests | `make test` (second part) | Garmin's Run No Evil tests, run in the headless simulator on each test device; judged from the console output, because `monkeydo` always exits with 1 |
| Static checks | `make lint` | Project policy (headers, no logging, no network, no secret-like literals), ruff and strict mypy on the tools, the product rule for all 72 products, the resource XML against the SDK's `resources.xsd` with the settings cross-check (`tools/check_resources.py`), and strict debug and release builds with zero compiler warnings |
| Screenshots | `make screenshots` | Each test device's screen matches its approved image in `test/golden/` (at most 0.01 % of pixels may differ by more than 24 levels, less than one changed character) |
| Performance and memory | `make perf`, `make memtest` | First-frame time and memory use within `tools/budgets.json` ([PERFORMANCE.md](PERFORMANCE.md)) |
| Packaging | `make package` | The signed `.iq` builds for all 72 products with zero warnings |
| Docs | `make docs` | No broken links or anchors, no Markdown lint issues, every Mermaid diagram renders |

Test devices: the Forerunner 70 by default; with `ALL=1` (`TEST_DEVICES`): Forerunner 255S (weakest: 218 px round MIP, buttons only, 512 KB), Venu Sq 2 (rectangular AMOLED), Forerunner 70 (your watch) and Forerunner 965 (454 px round AMOLED).

## Test vectors

`make vectors` regenerates `test/data/Vectors.mc` with `tools/gen_vectors.py`. Published vectors (FIPS 180-4, RFC 2202, 4226, 4231, 4648, 5869, 6238, 7914, NIST SP 800-38A) are copied from the documents and recomputed with Python's standard library, or with openssl for AES, before they are written; the generator stops on any disagreement. Seeded extra cases cover SHA padding boundaries (55 to 256 bytes), every digit count, odd periods, times around 2038 and 2100, and Steam codes. The Monkey C code under test never generates its own expected values.

## Results

### Review of 0.9.0, 2026-10-26 (unreleased)

An independent review of 0.9.0's changes (b8addc8, 096b5a4, 404b505, 140a666) and a watchdog test of the PIN unlock found and fixed:

- After a correct PIN, promoting pending entries and applying waiting "Require PIN" changes ran in one event; 16 waiting changes tripped the watchdog on the Forerunner 70. It is now a job (`ChangeQueue.LockJob`), and `make watchdog` has nine vault cases (19 items in total; PERFORMANCE.md).
- The list title decrypted every waiting change at each start; a 1 KB replacement was enough to trip at two passes. Changes are now counted from their stored action codes.
- A replacement was only checked after the PIN; it is now checked when it arrives, and its material is stored apart (`qm.<n>`).
- "Confirm all" applied changes that arrived after the menu was read; Confirm all and Reject all now act only on what the menu showed.
- On touch screens START could submit a legacy PIN early; "Change PIN" with a legacy PIN asked twice; a lock applied when a PIN is set didn't refresh the glance; the results of applied locks were lost.

Rejected: a tap opening the focused row instead of the tapped one (not reproducible in the simulator; watch for it in round 2), and the 250 ms ticker on a locked code card (cost negligible next to the screen).

Unit tests: 176 per device, including `test/settings/ChangeQueueTest.mc`. Two Forerunner 70 goldens changed (`list_locked`, `list_reset`: the title says "Results ready (menu)" after the lock applied at the PIN).

### Real-watch fixes, 2026-10-25 (version 0.9.0)

The first real-watch round on the Forerunner 70 (docs/YOUR_STEPS.md parts 3 and 4, 2026-10-21) reported: no touch scrolling outside the menus, a hard-to-use PIN keypad, "Wrong PIN" once before a correct PIN opened the code, more digits typed than dots shown, the QR progress starting at 90 %, and Manage entry's Require PIN not applying. All are fixed (CHANGELOG 0.9.0). Unit tests: 168 per device.

The UI flow now has two scenarios, 42 to 44 screens per watch:

- **Main** (existing build, extended): Steam and Library details and the menu's text screens (results, entry IDs, clock status; About on touch-only watches), each scrolled with a swipe on touch screens, plus the benchmark (saved for review only).
- **Extra** (`uitest-extra.jungle`, `Fixtures.loadExtra`, light theme): a Require PIN from the Manage form queued while locked and applied at the next PIN; UPC-A and Code 39 cards that must decode; the code error screen; the QR encoding progress, which must start below 30 % and only rise (Forerunner 70: 0, 11, 14, 22, 24, 42, 54, 63, 73, 85, 95 %); a legacy 6-digit PIN unlocked with OK, then a forced new 4-digit PIN with a mismatch; auto-lock closing a locked code card; Change PIN, Lock now, Reset PIN and creating a PIN; and the glance without a code.

What the new flow found and what was fixed:

- "Entry IDs" with 110 entries tripped the watchdog (all headers read in one event); it now reads 10 per step and shows "Reading…". The debug benchmark times at most 20 headers.
- Auto-lock didn't close a locked code card (README 7.5); it does now.
- The list said "Confirm in menu" for a queued Require PIN that applies by itself; locks are no longer counted there.
- A locked code card showed the barcode icon instead of the padlock.
- Without the OK key the keypad's bottom row looked like a missing key; it is now empty, 0, delete, as on a phone.
- The Reset PIN question was cut mid-word; it is now "Erase locked entries?".
- Venu X1 text screens lost their last line to the rounded corners; the bottom margin is now a tenth of the height.
- On button-only watches the PIN selector's lower arrow touched the hint.
- On the Forerunner 70 and 965 the round edge cut the glance title "OTP WALLET"; it falls back to "OTP Wallet".
- The warning red was too faint on the light theme; it is darker there.
- Not fixed: on the Instinct 3 AMOLED the system's round sub-screen covers the end of the glance title.

Verification: `make screenshots TEST_DEVICES=fr70 SPOT_DEVICES=` passes (7 min 51 s). The goldens of all seven watches were approved in one run that passed on every device; only the Forerunner 70 was re-verified afterwards, because it is the watch that matters now. `make test`, `make lint`, `make test-tools` and `make watchdog` pass.

### M8, 2026-10-21 (polish and release candidate, version 0.8.0)

- **Unit tests:** 163 per device on the Forerunner 255S, Venu Sq 2, Forerunner 70 and 965, all passing. New ones:
  - **Import:** packed items round-trip every field and wipe the parsed secret (`importItemsPackAndUnpack`).
  - **QR:** the packed mask scorer gives every mask the same penalty as the per-module rules, for versions from 1 to 21 and every level (`qrPackedScoresMatchReference`). segno's automatic version and mask choice still matches (`qrMatchesSegno`).
  - **Cache and drawing:** the new cache format holds widths over 255; a 0.7 cache is refused (`codeCachePackedFormat`). The drawn runs are exactly the dark modules (`codeRunsCoverExactlyTheDarkModules`).
  - **Glance:**
    - The sealed item gives the entry's codes (TOTP with SHA-256, Steam) and refuses tampering and other keys.
    - The About scan picks the first favourite TOTP or Steam entry without the PIN, and removes the item when the mark or the option goes.
    - The favourite actions check the mark and queue for locked entries.
- **Barcode decoding:** 222 encoder dumps per device decode with zxing-cpp at 3 sizes.
- **Screenshots:** 21 screens on 7 watches (18 on the touch-only ones) match their goldens, including the new glance with the favourite's code (768 147 for GitHub at the frozen time, the value Python's `hmac` computes). The glance is a second launch, because the simulator doesn't return to it when the app exits.
- **Watchdog stress (`make watchdog`, new):**
  - On the Forerunner 255S and 70, the largest code of every 2D symbology is encoded, packed, turned into runs and drawn with two steps per event, and the last code on the screen must decode.
  - It also passed on the Forerunner 965 and the Venu X1, which have the largest screens. The X1 draws a version 39 QR from a bitmap.
  - Writing it found watchdog trips in every 2D encoder with large inputs (see PERFORMANCE.md), all fixed.
- **Budgets:** start-up 1 to 17 ms; memory at the first frame 34.4 % (255S) and 22.9 % (768 KB watches); glance 37 %; the 100-entry import peaks at 45.3 % on the 255S.
- **Tools:** 75 Python tests, among them the watchdog check of the product list, the glance memory budget and barcode reading from screenshots.

### M7, 2026-10-20 (phone settings forms, version 0.7.0)

Monkey C unit tests: 154 per device (34 new, in `test/settings/`), all passing on the four test devices.

- **Add OTP:** every type (TOTP, HOTP with its counter, Steam ignoring algorithm, digits and period), and each validation message of README 10.4: Base32 character and position, `=` padding, missing secret, digits, period, algorithm, type, counter, control characters, length in bytes, a missing name, the warning below 80 bits. After a problem only the secret is cleared; after success the whole form, with the switch off.
- **Add code:** a valid payload for each of the 12 symbologies (2D codes come out cached and decodable) and an invalid one for each, with the encoder's message (check digit, length, character and position, capacity). Options and "Fix if wrong" are stored as sent, and codes too large for the screen are added with the README 12.2 warning.
- **Manage entry:** every action on unlocked entries; locked entries' changes queued even while unlocked, then applied by confirmation; "Require PIN" applied at once or queued; unknown, malformed, pending and damaged IDs; a queued change whose entry vanished; changes arriving while the queue is applied; tampered queue items dropped; the 16-item limit.
- **Idempotency:** the same Add OTP, Manage and Import form twice is applied once; any change makes a new request.
- **The whole run:** all forms at once in README order; the About catalog, its 800-character cut and the storage line; "Encrypt names" on and off, with locked entries converted after the PIN; work detected only when needed.
- **The contract with `properties.xml`:** every key the forms read exists and every form default equals its property's default (`settingsDefaultsMatchProperties`). The phone's side, all 13 resource files against the SDK's `resources.xsd` plus the settings, property and string cross-check, runs in `make lint` (`tools/check_resources.py`, 4 Python tests of its own).

Scripted UI flow (end-to-end): the UI-test build fills in the Add OTP, Add code and Manage entry forms next to the import, as the phone would, so they take the real path: properties, `onSettingsChanged`-style processing after the first frame, the timer-driven job and the results screen. 20 screens per watch (17 on the touch-only ones), on seven watches:

- `otp_form`: the TOTP added through the form shows 309 848 and next 489 290, the RFC 6238 values for its fake key at the frozen time, computed in Python.
- `code_form`: the Code 128 card added through the form decodes from the screenshot on every watch.
- `queue`, `queue_results` (watches with buttons): the Manage form's edit of the locked Bank entry waits under "Phone changes"; "Confirm all" asks for the PIN and then applies it.

The flow caught one watchdog trip on the Forerunner 255S that the unit tests can't: hashing the 100-entry import form by copying it into one buffer and zeroing it. The fields now go to the native HMAC one by one.

### M6d, 2026-10-20 (Aztec; M6 complete, version 0.6.0)

Monkey C unit tests: 120 per device.

1. **Reference comparison.** 24 Aztec symbols are identical to aztec_code_generator: compact (15 x 15 up) and full range, including sizes with reference grids, and error correction of 10, 23, 36 and 50 %. The payloads are of one character class each, for which both encoders produce the same bits; README 12.4 explains why mixed content can't be compared exactly.
2. **Decoding.** 8 Aztec codes are decoded with zxing-cpp: mixed case with digits and punctuation (Binary Shift and mode latches), UTF-8, a URL, and a long payload.
3. **Screenshots.** The UI flow opens an Aztec card after the PDF417 card, and its screenshot decodes on all seven watches.

With that, all 12 symbologies of README 12.1 pass checks 1 to 3. The UI flow shows 16 screens per watch (15 on touch-only watches), five of them barcodes that must decode.

### M6c, 2026-10-20 (Data Matrix and PDF417, in 0.6.0)

Monkey C unit tests: 117 per device.

1. **Reference comparison** (`make codes`):
   - **Data Matrix:** 27 symbols identical to ppf.datamatrix in ASCII mode, from 10 x 10 to 144 x 144, including multi-region and 10-block interleaved sizes. For 1,100 digits at 88 x 88, the watch, ppf and zint (through zxing-cpp) agree bit for bit.
   - **PDF417:** 27 symbols identical to pdf417gen at fixed columns and levels. They cover all four text submodes, digit runs below and above 13, UTF-8 bytes, exact 6-byte groups, levels 0 to 6 and 1 to 12 columns.
2. **Decoding with zxing-cpp:**
   - 10 Data Matrix codes, including Base256 UTF-8, Upper Shift, 88 x 88 and 144 x 144.
   - 5 PDF417 codes laid out for the Forerunner 255S, Forerunner 70 and Venu Sq 2 screens.
3. **Screenshots.** The UI flow opens a Data Matrix card and a PDF417 card after the QR card. All decode on all seven watches, the PDF417 at 1 px per module on the 255S.

Found and fixed along the way:

- **ppf padding bug:** see README 12.4.
- **Compiler out of memory:** one long `&&` chain of `instanceof` tests in a test made the Monkey C compiler run out of memory (more than 6 GB), and splitting the chain fixed it.
- **Watchdog on the 255S:** packing an 86 x 87 PDF417 matrix for the cache in the same event as the encoder's last step tripped the watchdog. PDF417 rows are now stored once with a row height of 3, packing does 8 modules per loop, and the cache is saved in its own timer event.

### M6b, 2026-10-20 (QR, in 0.6.0)

Monkey C unit tests: 111 per device.

1. **Reference comparison with segno** (`make codes`).
   - **31 matrices identical module for module.** They cover versions 1 to 40 and levels L, M, Q and H, numeric, alphanumeric and UTF-8 byte data, forced versions and masks, and automatic choices. The watch picks the same version and mask as segno with `boost_error=False`.
   - **Two deliberate differences**, listed in README 12.4:
     - segno pads a stream that already ends on a codeword boundary with an extra zero byte; the watch follows ISO 18004.
     - segno uses a single mode per symbol; the watch segments optimally.
2. **Decoding.** 30 QR codes from the test run are decoded with zxing-cpp at 2 and 3 px per module. They include mixed segmented content, Unicode, emoji, a Wi-Fi string and all four levels.
3. **Screenshots.** The UI flow opens the fixture's QR card (version 6, its digit run in numeric mode), encoded on the watch as a job the first time (2.8 s in the simulated watches). Its screenshot decodes on all seven watches, as does the EAN-13 card's.

Also tested:

- **The encrypted matrix cache:** round trip; tampering ignored; cleared by update and remove; a locked entry's cache unreadable while locked.
- **Capacity errors.**

What the UI flow found: scoring the eight masks tripped the watchdog, first on the Forerunner 70 (2,500 modules per step) and then on the 255S (1,200). Each job step now works on at most 500 modules when scoring rows and 1,000 when scoring columns.

### M6a, 2026-10-20 (linear barcodes, in 0.6.0)

Monkey C unit tests: 105 per device, with the checks of README 12.4.

1. **Reference comparison.** 217 rows from zxing-cpp (`tools/gen_code_vectors.py`, `make codes`): fixed edge cases and seeded random payloads for every linear symbology. EAN-13, EAN-8, UPC-A, UPC-E, Code 39 (with and without the check character), ITF and Codabar match module for module. Code 128 matches in length. zxing-cpp 3's zint backend picks a different one of the equally short encodings; on 5,000 random strings the lengths were always equal.
2. **Decoding.** Every Code 128 reference row, plus 139 codes for seeded random payloads in all eight symbologies, is dumped by the test run. `tools/check_codes.py` renders each with its quiet zone at 1, 2 and 3 px per module and decodes it with zxing-cpp, on each test device: 169 codes, all decoded.
3. **Screenshots.** The UI flow opens the fixture's EAN-13 card. Its screenshot decodes to `5901234123457` on all seven watches, including the Forerunner 255S at 1 px per module.

Validation messages are tested for every symbology: digits only, lengths, check digits (add, verify, fix, off), UPC-E number system and canonical zero suppression, ITF parity, Code 39 characters, non-ASCII Code 128 input, and Codabar start and stop characters.

### M5, 2026-10-20 (version 0.5.0)

Monkey C unit tests: 101 per device (29 new in `test/import/`):

- **JSON tokenizer:** every token kind; escapes including surrogate pairs; integers versus other numbers; 16 kinds of invalid document; incomplete input (8 cases); the depth limit; long strings (icons, including escaped quotes) skipped unread; long keys refused.
- **`otpauth://`:** URIs pasted run together; percent-encoded labels; a missing issuer; an issuer mismatch; `;` separators; `+` as a space; unknown parameters; HOTP with and without a counter; both Steam forms; nine error cases, each with its message.
- **Aegis:**
  - Database versions 1, 2 and 3, with groups listed after the entries.
  - totp, hotp and steam imported; motp and yandex skipped.
  - SHA-1, SHA-256 and SHA-512; 6, 7 and 8 digits; periods 20, 30, 50 and 60.
  - Favourites, icons, notes and Unicode names.
  - Encrypted exports refused, in both forms.
  - Bad entries (9 digits, a duplicated `secret` field, invalid Base32, a non-integer digit count), and non-Aegis, newer-version and too-deeply-nested documents.
  - An incomplete export.
- **Importer:**
  - The report text.
  - Duplicates of an earlier import, of a locked entry while the watch is locked, and within one import.
  - Possible replacements; "Require PIN" entries going pending.
  - Replace mode keeping manual entries.
  - A two-part JSON import, sealed while it waits, and the expiry of a waiting part.
  - Unusable text: empty, unknown, Google Authenticator links, encrypted, invalid.
  - A store that fills up mid-import, keeping only whole entries.
  - Fingerprints.
  - 100 entries.
- **Fuzz corpus** (`tools/gen_fuzz_corpus.py`, seed 4242, `make corpus`): 50 mutated Aegis documents and 50 mutated URIs.
  - The tokenizer always finishes.
  - The Aegis reader always ends in a definite state, with every item either valid or explained.
  - The full importer never stores an invalid entry: 21 entries were stored from the inputs that survived mutation.

Python tool tests: 55. The new ones cover the corpus generator, including that the committed corpus matches it.

Scripted UI flow: the UI-test build now puts an Aegis export of 100 entries into the Import form, as the phone would, and the flow starts with the results screen that opens after the import. 11 screens per watch (10 on the touch-only ones). Every watch reports the import's duration and peak memory, and the flow fails above 70 % of the watch-app memory.

### M4, 2026-10-19 (version 0.4.0)

`make ci` passed in full.

- **Monkey C unit tests:** 72 of 72 on each of the four test devices. The 7 new ones cover the code cache (same codes as the OTP module, expiry at the period end, the HOTP counter, invalidation, refusal while locked), code grouping, text helpers and the option defaults. One also opens the UI-test fixture's PIN wrap, which was computed in Python by `tools/gen_fixture_wrap.py`. That cross-checks the wrap format between two implementations.
- **Python tool tests:** 50 passed. They now include the UI-flow driver's parsing and the fixture-wrap generator, checked against RFC 5869 and against the constant in the app.
- **Scripted UI flow** (`make screenshots`, `tools/ui_flow.py`) on seven simulated watches: the Forerunner 255S, Venu Sq 2, Forerunner 70 and Forerunner 965, plus the spot checks fēnix 7, Instinct 3 AMOLED 45 mm and Venu X1. Each run does this:
  1. Opens the app from its glance.
  2. Opens a TOTP entry and a HOTP entry, and gets the next HOTP code.
  3. Opens the locked entry: PIN screen, a wrong PIN, the right PIN, then auto-lock closing it.
  4. Opens the menu and About (watches with buttons).
  5. Enters five wrong PINs until the 30 s wait appears.

  Ten screens per watch (nine on the touch-only Venu Sq 2 and Venu X1) match their approved images, and two runs give identical pixels.
- **Codes on screen:** the TOTP, HOTP and next codes in the screenshots match an independent Python computation for the frozen time 1800000026. GitHub 768 147, next 050 219; Google 2624 9947; Bank (SHA-512) 849 419; HOTP counters 5 and 6: 254 676 and 287 922.

Screen shapes: of the 72 target products, 69 have round screens and 3 rectangular ones (Venu Sq 2, Venu Sq 2 Music, Venu X1); both kinds are in the flow. None has a semi-round or semi-octagon screen. The code treats semi-round like round and other shapes like rectangles, untested on a real profile.

What the flow found and what changed because of it:

- **The watchdog is enforced in normal simulator runs** (unlike unit-test runs). Two things tripped it on the Forerunner 255S profile (120,000 bytecodes per event):
  - 200 PBKDF2 iterations per step. The inner loop now XORs 32-bit words, which makes it 2.3× faster, and a step is 100 iterations.
  - Two SHA-512 codes in one draw. Screens now compute at most one code per timer event, separately from drawing.
- **Opening the app needs care in the simulator.** It starts in the glance, and START alone doesn't open the app from there.
- **Taps:** on touch screens a tap first arrives as "select", so the keypad and the list didn't see where the finger was.
- **Venu X1:** this watch has no MENU button. The list now opens the menu on a long press.
- **Missing glyph:** the backspace glyph doesn't exist in the watch fonts, so the delete key is drawn.

### M3, 2026-10-19 (version 0.3.0)

`make ci` passed in full. Monkey C unit tests: 65 of 65 on each of the four test devices. The 34 new tests in `test/storage/` and `test/util/` cover:

- **Codec:** round trips of every TLV type, and rejection of malformed input (truncation, unknown types, wrong lengths, duplicate tags, invalid UTF-8).
- **Records:** round trips of TOTP, HOTP (64-bit counter), Steam and code entries, with and without encrypted names. Every single-byte change of a record is detected, as are wrong IDs, wrong keys and truncation. Entry validation and wiping are covered, and record sizes are measured.
- **Vault:** persistence across a restart, update, move, remove, the HOTP counter, damaged and swapped records, locked entries with the session locked, the pending queue, the 150-entry cap, and a full store at 67 fill levels with no half-written state.
- **PIN:** create, unlock and wrong PINs; malformed PINs; the delay schedule; lockout across an app restart on the trusted clock; uptime fallback with restart and timer reset; damaged lockout state; erase after 10 failures; PIN change (entries untouched) and reset; a tampered wrap; auto-lock with zeroed keys; wiped PBKDF2 buffers.
- **Migration:** fresh metadata, the v0 fixture to v1 (idempotent), and refusal of newer or damaged metadata without changing it.
- **UTF-8 validation:** overlong forms, surrogates, code points above U+10FFFF, truncation, NUL.

Python tool tests: 44 passed.

### M2, 2026-10-18 (version 0.2.0)

`make ci` passed in full. Monkey C unit tests: 31 of 31 on each of the four test devices, covering 57 hash vectors, 21 HMAC vectors, 5 PBKDF2, 3 HKDF, 2 AES-CBC (encrypt and decrypt), 12 Base32, 100 HOTP/TOTP and 12 Steam vectors, plus tamper detection on every byte of a sealed record, PKCS#7 edge cases, Base32 error positions, period boundaries, the 2038 widening and the job runner. Python tool tests: 44 passed.

Two things the simulator can't prove, kept for the real watch:

- **Watchdog.** The simulator didn't stop a 300,000-iteration loop in a test on the Forerunner 255S profile, whose budget is 120,000 bytecodes. Long work is split into jobs by design (`source/util/Jobs.mc`); the real limits are checked on your watch.
- **Speed.** See [PERFORMANCE.md](PERFORMANCE.md).

### M0, 2026-10-18 (version 0.1.0)

`make ci` passed in full:

- Python tool tests: 44 passed, with warnings treated as errors.
- Monkey C unit tests: the smoke test passed on all four test devices.
- Lint: 0 policy problems; ruff and mypy clean; 72 of 72 products meet the target rule; strict builds without warnings on all four test devices.
- Screenshots: all four devices match their approved images (0.00 % of pixels differ).
- Performance and memory: within budget on all four devices (numbers in [PERFORMANCE.md](PERFORMANCE.md)).
- Package: `otp-wallet-0.1.0.iq` built for all 72 products without warnings.
- Docs: 0 broken links, 0 lint issues, all diagrams render.

M0 has only the smoke test on the watch side; the real Monkey C suites start with M2 (crypto and OTP test vectors).

## Fake data only

Tests never use real secrets or personal data. Fixtures are generated or hand-written and labelled as fake.

## Real-watch checklist

Things the simulator can't verify, done by you on the Forerunner 70, with fake or test accounts only. 1.0.0 is tagged once every box is ticked (or a remaining item is recorded as a known limitation). The items were added milestone by milestone; M8 adds the release checks at the end. The same items in one step-by-step order, with fake test data and a results template: [YOUR_STEPS.md](YOUR_STEPS.md).

- [ ] M1: install the settings spike from the Store beta and run tests A to H in [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md); record the results there.
- [ ] M4: sideload a debug build (`make build DEVICE=fr70`, then copy `build/debug/fr70.prg` to `GARMIN/APPS` over USB). Open the menu, choose "Debug: benchmark", and send me the numbers. They set the real PBKDF2 iteration count and the SHA-512 cost on your watch.
- [ ] M4: with "Debug: demo entries", check the list, a code, the countdown ring, HOTP "next", and the PIN keypad with your finger. Also check a wrong PIN (vibration) and the 30 s wait after five wrong PINs.
- [ ] M4: check that leaving the app and coming back asks for the PIN again for the locked demo entry. Check that the glance in the glance loop shows the number of entries.
- [ ] M5: in Garmin Connect, paste a few `otpauth://` links of test accounts (not your real ones) into the Import fields, turn "Import now" on and save. Check the result on the watch and in the Result field. Also check that the text fields are emptied afterwards: this answers part of the M1 question.
- [ ] M6a: with "Debug: demo entries", open "Gym card" (an EAN-13). Scan it with your phone's camera, or a shop scanner if you can, indoors and outdoors. Tell me whether it reads.
- [ ] M6b: open "Member QR" (a demo entry) and scan it with your phone's camera. The first opening encodes it; note roughly how long that takes. The second opening should be instant (cache).
- [ ] M6c: scan the "Parcel" (Data Matrix) and "Ticket" (PDF417) demo cards with a phone app that reads them, for example the zxing-based "Barcode Scanner".
- [ ] M6d: scan the "Rail pass" (Aztec) demo card with a phone app that reads Aztec.
- [ ] M7, with the Store beta, using a test account (never a real one):
  - **Add OTP:** fill in the form with the account's Base32 secret, turn on "Add this entry" and save. The watch shows the results; the code must match the one in your phone's authenticator app. Check the form's Result field and whether the form is emptied on the phone.
  - **Add code:** add a test EAN-13 (for example `5901234123457`) and a short QR text; scan both from the watch.
  - **Manage entry:** with the IDs from the About group on the phone (or "Entry IDs" on the watch), rename one entry, move it to position 1 and delete it.
  - **Locked entries:** set "Require PIN" on an entry, then send "Don't require PIN" for it. The watch must not apply it until you open the menu, choose "Phone changes", "Confirm all" and enter the PIN.
  - **Repeats:** send the same Add OTP form twice (turn the switch on again without changing anything). The second time the result is "Already added".
  - **Options:** change Theme and Code size; the watch changes as soon as the phone saves.
  - **Limits:** does the About catalog (up to 800 characters) show completely on the phone?
- [ ] M8, with the 0.8.0 Store beta:
  - **Update keeps data:** install 0.8.0 over the 0.7 beta (or whichever you have); the entries, the PIN and the order must still be there. Open a 2D code you had opened before: it is re-encoded once (new cache format), then instant again.
  - **Glance:** mark a test TOTP entry as favourite (Manage entry, "Mark as favourite (glance)"), turn on "Show the favourite code in the glance" and open the app once. The glance must show the same code as the app and count down. Turn the option off on the phone: the glance must go back to "N entries" without opening the app. Lock the entry ("Require PIN"): it must leave the glance.
  - **Brightness:** open a barcode in a dim room. The screen should go to full brightness and stay bright for about a minute; a tap brightens it again. Turn "Full brightness while showing a barcode" off and check that it no longer does.
  - **Large code timing:** add a QR with about 300 characters of text (Add code) and note how long the first opening takes ("Encoding…"). On the Forerunner 70 it runs two steps per tick.
  - **Benchmark:** menu, "Debug: benchmark" on a sideloaded debug build; send me the numbers (they replace the simulator estimates in PERFORMANCE.md).
  - **Uninstall:** remove the app from the watch, install it again: it must start empty (no entries, no PIN).
  - **Scanning:** scan the demo cards (Gym card, Member QR, Parcel, Ticket, Rail pass) with your phone, indoors and outdoors; a shop scanner if you can.