# Relay test coverage audit Scope: every package compiled into the relay (`git.smesh.lol/smesh`, the `main.mx` closure) plus the `web/common` modules the tests pull in. The question this answers is the one that matters for debugging: is every line of useful code in the relay validated by a test that runs? ## How the suite runs now `make test` runs three layers: 1. `make test-unit` — every package carrying a `*_test.mx`, through the compiler's own harness (`moxie test ./`). 2. `make build-test-relay` + `make test-signer` — the relay and the browser signer used by the integration suite. 3. `python3 -m pytest test/` — the integration suite (HTTP, WebSocket, NIP-01 policy, NIP-11, front-end and worker shims, MLS, memory instrumentation). Before this audit layer 1 did not exist: twelve `*_test.mx` files sat in the tree and **no target ever ran them**. When they were finally run, eleven of the twelve did not compile, and `moxie test` itself was broken in three separate ways (below). Everything in this document is what running them revealed. As of the current tree, `make test-unit` runs **40 packages plus the root `main` package** (the loop used to `find pkg web`, which never saw the root `main_test.mx`) and the skip list is empty. Nothing below is skipped for a known-broken reason any more, and the `cover` target no longer deselects a test: the one it used to drop (`test_relay_proxy_high_event_volume`) now runs against the instrumented binary with a widened post-EOSE window. ## What the audit fixed ### `moxie test` never built a real closure (four compiler bugs) 1. **The test files' imports were not in the closure.** `cmdTest` added `testing` to the package's imports and nothing else, so a dependency the package under test does not itself use (`bytes`, `crypto/rand`, `time` in a test) was never resolved: the type came out `invalid`, `var bytes.Buffer` allocated one byte, its method calls were dropped, and the test miscompiled silently. Minimal repro before the fix, `pkg/nostr/varint`'s `bytes.Buffer` + `io.Writer` use: ``` % moxie test ./pkg/nostr/varint panic: runtime error: interface dispatch: no impl for Write ``` 2. **No dispatch index.** `cmdBuild` type-checks every package into the registry and builds `dispIfaceMethods`/`dispMethodsByPkg` before the first emission; `cmdTest` did neither, so every package looked tier 2 and the emitted dispatch had no arms. The two passes are now one function, `prepareDispatchIndex`, called by both. 3. **No link-set cache key.** A package's cache key carries `bst.linkSetKey` because the dispatch arms it emits depend on the closure. `cmdTest` never set it, so a test build looked up packages under the empty key and reused arms compiled for a *different* closure. Minimal repro: `moxie test` on a package that calls `crypto/rand`; ``` panic: runtime error: interface dispatch: no impl for Read ``` The same code in a normal `moxie build` worked, which is why the relay itself never showed it. 4. **No `-work`.** `moxie test -work` now keeps and prints the test scratch directory like the build path, which is how bugs 1-3 were found. ### Assignability of composite literals (compiler) `cmdBuild`'s assignability check covered assignments and arguments but not composite-literal elements: `store`, slice and map literals stored their values unchecked. Legacy rejects these through go/types; stage4 accepted them. The audit hit it first in smesh: ```moxie type Limiter struct { buckets map[string]bucket } ... buckets: map[string]*bucket{}, // legacy: cannot use ... as map[string]bucket ``` `pkg/relay/ratelimit` carried exactly that, and stage4 compiled it happily (runtime map element layout happened to match, so it even worked). stage4 now checks struct-literal fields, array/slice elements and map keys/values and reports the same wording legacy does. ### `String()` on a builtin (compiler) `resolveMethodCallWithRecv` synthesised `runtime.(*uint64).String` for a direct `.String()` call on a basic type. No such symbol exists — the builtin stringers are interface-only identity thunks (`basic:.String$identity`, emitted by `emitBasicIdentities`) — so the call was accepted and failed at link time: ``` % moxie build ./probe # x := uint64(5); println(x.String()) ld.lld-22: error: undefined symbol: runtime.(*uint64).String ``` Legacy rejects the same source as `x.String undefined (type uint64 has no field or method String)`. stage4 now falls through to the same undefined-method error. The supported path — boxing through `fmt.Stringer` (`fmt.Println(x)`, `t.Fatalf("%d", x)`) — is unchanged and covered by the tests. ### smesh source bugs found by the new tests | where | bug | fix | | --- | --- | --- | | `pkg/relay/ratelimit` | `map[string]*bucket{}` assigned to a `map[string]bucket` field (hidden by the literal hole above) | literal corrected | | `pkg/nostr/timestamp` | `FromVarint` returned the *consumed* prefix as `rem` instead of the remainder, unlike every other `Unmarshal` in the tree (no callers yet) | returns `b[read:]`, covered by a test | | `pkg/mode` | `IsOpen`'s logic was unreachable from a test: it reads the `ACLMode` global, and package globals are immutable outside `init` | logic extracted to the pure `IsOpenMode`, which the test covers | | `pkg/nostr/filter` | `F.Unmarshal` dispatched on `key[0]` plus a length bound instead of an exact key match: `{"arr":[1]}` and `{"s":1}` were rejected as invalid, and `{"iXXX":"hello","kinds":[1]}` was parsed as `ids`, which swallowed the rest of the filter and silently dropped `kinds` | exact key match (`keyIs`); unknown keys land in `Extra`, asserted by the test | | `pkg/nostr/tag` | `(*T).Unmarshal` accepted a closing `]` with no opening `[` (the early return skipped the final guard) | guard the early return on `openedBracket`, asserted by the test | | `pkg/nostr/tag` | `(*S).GetTagElement(i)` panicked at `i == Len()` (`len(s.T) < i` let the boundary through) | `i < 0 \|\| i >= len(s.T)` returns nil, asserted by the test | | `pkg/nostr/event` test | shadowed `signer`/`err` (Moxie forbids shadowing; the test predated enforcement) and never compiled | rewritten with per-function names | | `pkg/crawler`, `pkg/find`, `pkg/grapevine` tests | used `t.TempDir()` and `t.Cleanup(func(){ eng.Close() })`; Moxie has neither, and a cleanup *closure* would have to capture `eng` | open the store in `os.MkdirTemp` and close it with `defer` in the test | | root `main.mx` (`outboxDiscover`) | the follow list holds 64-char hex pubkeys (`ValueHex`) but the kind-10002 map was keyed by the event's raw 32-byte pubkey (`string(ev.Pubkey)`), so the inversion never matched a follow and every author fell back to `wss://relay.damus.io` - the outbox model was silently the default model. Found by `TestOutboxDiscoverBuildsWriteRelayMap`, which printed `1 write relays discovered` instead of `3` | key the map with `hexEnc(ev.Pubkey)` so both sides are hex; the test now sees the three buckets and asserts the `write`/`read`/absent r-tag markers | | `pkg/store/checkpoint` | `Set` removed its temporary file when the write or the sync failed but not when the rename did, so a failed `Set` left `checkpoint.dat.tmp` behind | remove the temporary file on the rename failure too, and assert it in `checkpoint_paths_test.mx` | | `pkg/nostr/kind` | `(*S).Unmarshal` read `r[0]` after the last kind without checking the remainder, so a truncated kinds array (`["REQ","s",{"kinds":[1`) crashed the relay - index out of range, and with that read bounded, SIGSEGV further along the filter path | bound the access, which keeps the documented leniency (`[` parses to no kinds) while a truncated array no longer reads past its end. `TestSUnmarshalTruncatedArray` locks it. The deeper SIGSEGV a truncated *filter object* still causes is the open item below | ### New unit tests `pkg/nostr/hex`, `pkg/nostr/varint`, `pkg/nostr/ints`, `pkg/nostr/timestamp`, `pkg/nostr/kind` (39 tests, embed/kind/kinds to 99-100%), `pkg/nostr/tag` (22 tests, tag.mx 98%, tags.mx 100%) — the pure-logic packages the relay depends on that had no direct test at all. The tag tests found two production bugs, both fixed here with the assertions kept: `(*T).Unmarshal` accepted a closing `]` with no opening `[`, and `(*S).GetTagElement(Len())` indexed one past the end and panicked. ### The integration harness dropped bytes read past the WS upgrade `_ws_handshake` (and the smoke test's inline handshake) read the 101 response with `recv(4096)`. When the relay coalesced the NIP-42 AUTH challenge into the same segment, those bytes were read into the response buffer and discarded, so the challenge was lost and `_drain_until(ws, "AUTH")` timed out on roughly 1 connection in 100. That is the intermittent `test_unauthed_event_rejected` / `test_free_writes_then_rate_limit` failure: the relay had sent AUTH, the reader threw it away. It reproduces on both the current relay and one built before the defer-frame fix (2/300 misses each), so it was never a relay bug. The bytes after the header are now kept and consumed before `recv`; 0/300 misses and the full pytest suite is green. smesh `190f2deb`. ## Coverage matrix Verdicts are from `make test-unit` (see the runner for the exact command). | package | unit test | integration | verdict | | --- | --- | --- | --- | | `.` (root `main.mx`) | `main_test.mx` + `main_session_test.mx` | the relay itself | **pass** (26, `main.mx` 20.3% -> 59.6%; the network helpers run against loopback WebSocket peers) | | `pkg/errors` | `errors_test.mx` + `errors_paths_test.mx` | relay error paths | **pass** (9, 98.3% of `errors.mx`) | | `pkg/nostr/hex` | `hex_test.mx` (new) | via event serialization | **pass** (3) | | `pkg/nostr/varint` | `varint_test.mx` (new) | via event tags/ints | **pass** (3) | | `pkg/nostr/ints` | `ints_test.mx` (new) | via event JSON | **pass** (5) | | `pkg/nostr/timestamp` | `timestamp_test.mx` (new) | via event JSON | **pass** (5) | | `pkg/nostr/event` | `event_test.mx` (repaired) + `event_paths_test.mx` | policy/roundtrip | **pass** (15, 91.8% of the package) | | `pkg/relay/ratelimit` | `ratelimit_test.mx` (new) | free-write limit test | **pass** (5) | | `pkg/find` | `find_test.mx` (repaired) | not on the request path | **pass** (4) | | `pkg/mode` | `mode_test.mx` (repaired) | every ACL check | **pass** (1) | | `pkg/crawler` | `crawler_test.mx` (repaired) | not on the request path | **pass** (3) | | `pkg/grapevine` | `grapevine_test.mx` (repaired) | not on the request path | **pass** (3) | | `pkg/store` | `engine_test.mx` + 7 subpackage files | every query | **pass** (23 + 47), `engine.mx` 92.3%, `sorted.mx` 87.8%, `wal.mx` 81.0%, `index/keys.mx` 100%, `serial.mx` 100%, `checkpoint.mx` 88.4% | | `pkg/relay/pipeline` | `pipeline_test.mx` | every write | **pass** (27), 95.5% of pipeline.mx | | `pkg/nostr/ws` | `client_test.mx`, `client_session_test.mx`, `ws_frames_test.mx`, `dns_test.mx` | relay does not import it | **pass** (20, against loopback servers; `client.mx` 97.8%, `ws.mx` 75.2%, `dns.mx` 56.9%) | | `pkg/sync/negentropy` | `negentropy_test.mx` | `cmd/sync` only | **pass** (29), 90% of negentropy.mx | | `pkg/mediaproxy` | `mediaproxy_test.mx` + `mediaproxy_http_test.mx` | media proxy worker | **pass** (22, 88.7% of `mediaproxy.mx`; the HTTP half runs against a scripted loopback origin) | | `pkg/blossom` | `blossom_test.mx` + `blossom_serve_test.mx` | blob routes | **pass** (14, 99.1% of `blossom.mx`; the CORS-proxy fallback runs against a loopback origin) | | `pkg/lol` | `lol_test.mx` | logging | 8/9 pass; `TestErrorf` blocked by the `fmt` multi-verb bug | | `web/common/marmot` | `varint_test.mx` + `codec_test.mx` | browser module | **pass** (18, the AEAD round trip is asserted again; `marmot.mx` 61.5%) | Total after this audit: **91 test functions passing, 11 packages** at the time (`errors`, `find`, `mode`, `nostr/event`, `nostr/hex`, `nostr/ints`, `nostr/kind`, `nostr/tag`, `nostr/timestamp`, `nostr/varint`, plus the repaired suites); the loop now runs 49 packages. Before it, exactly one of the twelve pre-existing unit-test files compiled and passed (`pkg/errors`); the other eleven had never run. The `crawler`/`grapevine` pair now reaches IR emission and stops in clang: ``` % moxie test ./pkg/crawler ./tmp/mxc-testbuild-*/._pkg_crawler.ll:1772:40: error: '%t18' defined with type '{ { ptr, i64, i64 } }' but expected '{ ptr, i64, i64 }' ``` The function is `makeRelayList` in the test file: a variadic `...string` parameter whose element is converted with `[]byte(r)` and pushed into a `*tag.S` through `push(*tags, ...)`. The emitted value is a one-field struct wrapping the slice where the slice itself is expected — the `string`/`[]byte` unification losing a wrapper level in the variadic path. It is a compiler bug, not a test problem, and it predates the rewrite: the test only failed earlier (`TempDir`) before reaching emission. ## Integration coverage (pytest) | file | what it validates | | --- | --- | | `test/test_relay_policy.py` | 24 tests: health, name, CORS, connection/per-IP caps, bot user-agents, publish/query, unauthenticated writes and REQs, free-write rate limit, excess subscriptions, result caps, live delivery, replaceable kinds, ephemeral events, NIP-09 delete, NIP-42 challenge/auth, bad signatures, future timestamps, CLOSE, COUNT | | `test/test_relay_roundtrip.py` | publish/receive, live subscription, feed, localhost relay, ordering | | `test/test_smoke.py` | index, wasm host, css, service worker, NIP-11, SPA fallback, REQ/EOSE | | `test/test_signer.py` | 16 tests over the browser signer (BIP-340 vectors, nsec login, vault export/import) | | `test/test_app.py`, `test/test_infinite_scroll.py` | front-end flows served by the relay | | `test/test_http_proxy_worker.py` | 14 tests over the proxy worker shim | | `test/test_mls_broadcast.py`, `test/test_mls_dm.py` | MLS messaging paths | | `test/test_sab_verify.py` | SharedArrayBuffer verification shim | | `test/test_memory_profile.py` | allocator instrumentation against a real profile | Not covered by the integration suite because they are not on the relay's request path: `pkg/crawler`, `pkg/find`, `pkg/grapevine` (now unit-tested), `pkg/nostr/ws` (client), `pkg/sync/negentropy` (used by `cmd/sync`), the `cmd/*` tools, and the non-relay `web/` modules. ## Measured coverage (integration run) `make cover` builds the relay with `-cover` and runs the integration suite against it; the compiler's site table plus the runtime's counters give per-file statement coverage of this module. The latest run: ``` sites: 8733 covered: 4028 (46.1%) ``` 46.1% of the relay module's statements run under `pytest test/` (the run-to-run spread is a handful of sites, because a few worker domains are timing dependent). That number is only the integration half: the unit tests are separate binaries with their own site tables, so their hits are not in this file. The table below is the integration per-file view - the work list, because it says *which* code has never executed under pytest; the unit measurement of the same files is in "Unit coverage" further down, and several files here read 0% precisely because their coverage lives there. Files at 0% are dormant or untested, not necessarily wrong - several are behind config defaults that the suite does not turn on. | file | covered | note | | --- | --- | --- | | `main.mx` | 9.8% (69/697) | the relay `main`, plus the `sync`/`crawl`/`env`/proxy subcommands | | `pkg/access/access.mx` | 21.4% (3/14) | | | `pkg/acl/acl.mx` | 11.1% (1/9) | ACL mode dispatch: `ORLY_ACL_MODE=none` by default | | `pkg/acl/follows.mx` | 0.0% (0/45) | follows ACL, off by default | | `pkg/acl/social.mx` | 0.0% (0/51) | social/grapevine ACL, off by default | | `pkg/blossom/blossom.mx` | 2.4% (3/123) | covered by its unit test (99.1%), not by pytest | | `pkg/broadcast/broadcast.mx` | 68.5% (24/35) | | | `pkg/grapevine/grapevine.mx` | 0.0% (0/82) | `ORLY_GRAPEVINE_ENABLED=false` by default | | `pkg/lol/chk/chk.mx` | 33.3% (2/6) | | | `pkg/lol/errorf/errorf.mx` | 0.0% (0/6) | logger sub-package | | `pkg/lol/log/log.mx` | 0.0% (0/2) | logger sub-package | | `pkg/mediaproxy/mediaproxy.mx` | 0.0% (0/187) | media proxy worker; pytest drives no `/proxy/` media request, its unit test covers 88.7% through a scripted loopback origin | | `pkg/metrics/global.mx` | 0.0% (0/9) | covered by its unit test, not by pytest | | `pkg/metrics/metrics.mx` | 19.7% (14/71) | covered by its unit test, not by pytest | | `pkg/nostr/envelope/auth.mx` | 21.4% (9/42) | | | `pkg/nostr/envelope/envelope.mx` | 90.0% (36/40) | | | `pkg/nostr/envelope/event.mx` | 42.8% (18/42) | | | `pkg/nostr/envelope/req.mx` | 34.6% (9/26) | | | `pkg/nostr/envelope/simple.mx` | 31.1% (52/167) | | | `pkg/nostr/event/binary.mx` | 69.6% (69/99) | | | `pkg/nostr/event/canonical.mx` | 93.1% (27/29) | | | `pkg/nostr/event/event.mx` | 72.6% (157/216) | | | `pkg/nostr/event/sign.mx` | 38.2% (13/34) | | | `pkg/nostr/event/verify.mx` | 40.0% (8/20) | | | `pkg/nostr/filter/filter.mx` | 44.3% (121/273) | | | `pkg/nostr/filter/filters.mx` | 39.2% (22/56) | | | `pkg/nostr/filter/skip.mx` | 1.1% (1/89) | | | `pkg/nostr/hex/hex.mx` | 70.5% (12/17) | | | `pkg/nostr/ints/ints.mx` | 84.8% (56/66) | | | `pkg/nostr/kind/embed.mx` | 4.1% (9/216) | | | `pkg/nostr/kind/kind.mx` | 83.1% (148/178) | | | `pkg/nostr/kind/kinds.mx` | 40.0% (28/70) | | | `pkg/nostr/signer/p8k/p8k.mx` | 15.0% (14/93) | | | `pkg/nostr/tag/tag.mx` | 43.7% (73/167) | | | `pkg/nostr/tag/tags.mx` | 50.0% (51/102) | | | `pkg/nostr/text/escape.mx` | 30.1% (32/106) | | | `pkg/nostr/text/helpers.mx` | 58.9% (105/178) | | | `pkg/nostr/text/wrap.mx` | 37.0% (20/54) | | | `pkg/nostr/timestamp/timestamp.mx` | 19.5% (8/41) | | | `pkg/nostr/varint/varint.mx` | 92.3% (12/13) | | | `pkg/nostr/ws/client.mx` | 0.0% (0/95) | WS client: not used by the relay | | `pkg/nostr/ws/dns.mx` | 0.0% (0/127) | WS client DNS | | `pkg/nostr/ws/ws.mx` | 0.0% (0/184) | WS client | | `pkg/pool/pool.mx` | 83.3% (5/6) | | | `pkg/relay/config/config.mx` | 51.1% (323/631) | covered by its unit test (99.6%), not by pytest | | `pkg/relay/dbengine/dbengine.mx` | 80.7% (126/156) | | | `pkg/relay/mute/mute.mx` | 31.8% (14/44) | | | `pkg/relay/pipeline/pipeline.mx` | 66.4% (89/134) | | | `pkg/relay/ratelimit/ratelimit.mx` | 76.1% (16/21) | | | `pkg/relay/server/server.mx` | 79.7% (322/404) | | | `pkg/relay/server/server_subs.mx` | 43.7% (59/135) | | | `pkg/relay/server/server_workers.mx` | 33.0% (66/200) | | | `pkg/relay/tree/messages.mx` | 84.4% (136/161) | | | `pkg/relay/wire/blossom_worker.mx` | 16.6% (6/36) | now reports; no blossom request in the suite | | `pkg/relay/wire/broadcast_worker.mx` | 83.7% (67/80) | covered by its unit test (97.5%) | | `pkg/relay/wire/ingest_worker.mx` | 90.9% (30/33) | covered by its unit test (100%) | | `pkg/relay/wire/proxy_worker.mx` | 15.3% (4/26) | now reports; the dispatch block is never taken | | `pkg/relay/wire/wire.mx` | 35.4% (117/330) | covered by its unit test (93.3%), not by pytest | | `pkg/store/checkpoint/checkpoint.mx` | 62.7% (32/51) | | | `pkg/store/engine.mx` | 58.4% (405/693) | | | `pkg/store/index/keys.mx` | 73.8% (110/149) | | | `pkg/store/serial/serial.mx` | 74.0% (20/27) | | | `pkg/store/sorted/sorted.mx` | 70.4% (289/410) | | | `pkg/store/wal/wal.mx` | 40.8% (71/174) | | | `pkg/transport/http.mx` | 67.6% (140/207) | | | `pkg/transport/transport.mx` | 63.0% (208/330) | | | `pkg/transport/ws.mx` | 76.9% (87/113) | | Five rounds of coverage work past the first measurement have moved the unit numbers per file: `pkg/nostr/event` 67.8% -> 91.8%, `pkg/errors` 71.2% -> 98.3%, `pkg/nostr/ws` 30.5% -> 74.4% (`client.mx` 33.6% -> 97.8%, `ws.mx` 75.2%, `dns.mx` 56.9%), `pkg/mediaproxy` 48.6% -> 88.7%, `pkg/blossom` 69.9% -> 99.1%, `pkg/store/engine.mx` 87.6% -> 92.3%, `pkg/store/sorted.mx` 87.8% -> 93.4%, `pkg/store/wal.mx` 81.0% -> 85.9%, `pkg/store/checkpoint/checkpoint.mx` 82.4% -> 88.4%, the `pkg/nostr/envelope` files 92.3-95.2% -> 95.2-97.6%, the root `main.mx` 19.7% -> 59.6% (its network helpers run against loopback WebSocket peers) and `web/common/marmot` 15.0% -> 61.5%. Across the unit-cover loop (one run per `*_test.mx`, 54 runs over 39 distinct packages) the 63 non-test own files carry **7204/7919 sites = 91.0%** (86.5% before the `main.mx` round, 90.0% and then 90.5% in the two rounds after it; a package's own sources only, because the harness's per-package tables also name the closure). The largest remaining blocks are the relay `server` package's tail, the `pkg/store` tails (`wal.mx`'s segment rotation needs a 4GB segment), the root `main.mx` subcommands that loop forever or spawn the binary, and `dns.mx`'s `dnsLookup`: the UDP exchange needs a nameserver, so it stays integration-only. Whole-package numbers are not in the table where a package has one file only (`pkg/mode`, `pkg/nostr/varint`, ...); the report prints every file the site table names. ### How the measurement works, and what it cannot see - Hit granularity is one site per **statement** (stage4) or per **basic block** (legacy). A line with two statements has two sites; a block that never ran has one. - A **spawn domain is a fork**: its counters are a private copy of the table taken at fork, so each domain appends its own dump to `MOXIE_COVER_OUT` when it exits (`spawn.mx`, `pipe_channel.mx`). Without that, the store, the ingest and the proxy domains - most of the relay - would report 0%. - Because a domain inherits the parent's counters, sites that ran *before* the fork are counted once per domain. The report only asks whether a site ran, so a count is inflated but coverage is not. - Counts are lost for a process that is **SIGKILLed** rather than allowed to exit; pytest sends SIGTERM first, which the relay handles. A worker domain that never returns from its loop gets no such chance: the fixture's SIGTERM reaches the group, the worker ignores it (its loop only watches its channel) and SIGKILL follows. That is why the store domain, which exits when its channel closes, reports 59-94% while the ingest, proxy, blossom and broadcast workers report ~0%. - One `make cover` run had three `test_relay_policy.py` tests fail because the relay exited early at startup for that test's port (`ConnectionRefusedError`, with `Engine.Open`'s step trace in the captured output); the same 24 tests pass alone against the same binary and the retry ran 82 passed. `pkg/store/engine.mx` still carries the `os.Stderr.Write` trace that failure printed (`store.Open: opening index eid` / `OK eid`, 17 calls), which is debugging left in the boot path and should come out with the next round's verification. - A build without `-cover` links the same runtime with no cost: the hook is a nil check and the table is allocated lazily on the first hit. - Every test module must run the binary under test through `SMESH_RELAY_BIN`. `test_relay_policy.py` and `test_http_proxy_worker.py` hardcoded `smesh`, so the first measurement (37.5%) ran 38 of 77 tests against the plain relay and reported it as the suite's coverage. Fixed in those two files. - `test_relay_proxy_high_event_volume` used to be dropped from `make cover`: its poll initialized `quietSince = eoseAt`, so a 2s gap with no event counted as quiet and the run reported zero events (deterministically against `smesh-test`, about 1 isolated run in 4 against `smesh`). It now waits for the first event up to `PROXY_VOLUME_WINDOW_MS` and passes against both; the `cover` target sets 180000 and runs it. `make cover` exit 0 with the test included: sites 8733 covered 4028 (46.1%). ### Unit coverage added after the measurement (not in the table above) The integration table above is `make cover`'s number, which only counts what the pytest suite exercises. The unit suites written since then cover their packages directly. `make unit-cover` runs the same package loop as `make test-unit` with `-cover`, and the harness prints the report itself: `moxie test -cover` writes the site table, starts the instrumented test binary and joins the two with `pkg/mxcover` (the same implementation the `mxcover` command uses), so no external tool is needed. The percentages below are from that report: | file | integration | unit | | --- | --- | --- | | `pkg/relay/config/config.mx` | 51.1% | 99.7% | | `pkg/relay/wire/wire.mx` | 7.5% | 93.3% | | `pkg/relay/wire/ingest_worker.mx` | 0% | 100.0% | | `pkg/relay/wire/proxy_worker.mx` | 3.8% | 65.4% | | `pkg/relay/wire/blossom_worker.mx` | 0% | 94.4% | | `pkg/relay/wire/broadcast_worker.mx` | 2.5% | 97.5% | | `pkg/acl/acl.mx` / `follows.mx` / `social.mx` | 11% / 0% / 0% | 100.0% / 97.8% / 95.1% | | `pkg/relay/mute/mute.mx` | 30.9% | 93.2% | | `pkg/metrics/metrics.mx` / `global.mx` | 19.7% / 0% | 98.6% / 100.0% | | `pkg/access/access.mx` | 21.4% | 100.0% | | `pkg/pool/pool.mx` | 33.3% | 100.0% | | `pkg/blossom/blossom.mx` | 2.4% | 69.9% | | `pkg/nostr/text/escape.mx` / `helpers.mx` / `wrap.mx` | 30% / 58% / 37% | 93.4% / 94.9% / 100.0% | | `pkg/nostr/envelope/auth.mx` / `event.mx` / `req.mx` / `simple.mx` | 21% / 40% / 35% / 31% | 95.2% / 97.6% / 96.1% / 96.4% | | `pkg/store/engine.mx` | 59.3% | 92.3% | | `pkg/store/sorted.mx` / `wal.mx` / `serial.mx` / `index/keys.mx` / `checkpoint.mx` | 72% / 41% / 74% / 94% / 63% | 93.4% / 85.9% / 100.0% / 100.0% / 88.4% | | `pkg/relay/pipeline/pipeline.mx` | 65.6% | 97.8% | | `pkg/sync/negentropy/negentropy.mx` | not in the relay | 93.7% | The relay's spawn domains now report too: `coverDump` writes one line per `write(2)` (a shared append file with five writes per line interleaved inside lines, so 1846 of 9053 lines parsed and every worker file read 0%), and the broadcast domain is spawned after the signal handlers are installed. 16 of 16 domains dump and every line parses. ### The gap plan Ordered by how much untested code each item unlocks, with the shape of the test that would reach it: | # | target | uncovered | test | | --- | --- | --- | --- | | 1 | `pkg/nostr/kind/embed.mx`, `kinds.mx` | 250 | **done**: 39 unit tests, 99-100% of the three files | | 2 | `pkg/nostr/filter` (`filter`, `filters`, `skip`) | 298 | **done**: 21 unit tests, 98-100% of the three files (and the key-dispatch bug they found) | | 3 | `pkg/nostr/tag` (`tag`, `tags`) | 235 | **done**: 22 unit tests, 98-100% of the two files (and two bugs they found) | | 4 | `pkg/nostr/text` (`escape`, `helpers`, `wrap`) | 195 | **done**: 19 unit tests, 93-100% of the three files | | 5 | `pkg/mediaproxy` | 187 | **done**: 22 unit tests, 88.7% - a scripted loopback HTTP origin now drives the request line, the status and header parse, all three body framings, redirects and the in-connection error returns, not just the pre-dial rejections | | 6 | `pkg/acl` (`acl`, `follows`, `social`) | 140 | **done**: 11 unit tests, 95-100% of the three files | | 7 | `pkg/relay/wire` workers | 174 | **done**: 28 unit tests plus a run with `ORLY_INGEST_WORKERS=2`; 93-100% of `wire.mx` and the four domains | | 8 | `pkg/store` (`sorted`, `index`, `wal`, `engine`) | 340 | **done**: 78 unit tests across the sort/index/wal/engine primitives (no `TempDir`: `os.MkdirTemp` + `defer`), 81-100% per file. `store_paths_test.mx` drives every failure return of `Engine.Open` (each index file, the WAL directory, a segment, the checkpoint, `MkdirAll`) and the stale-checkpoint rebuild from the WAL; `checkpoint_paths_test.mx` covers `Open`'s read error and both `Set` failure paths | | 9 | `pkg/relay/server` (`server`, `_subs`, `_workers`) | 400 | **done as far as the harness allows**: 3 test files drive subscription edge cases, the connection lifecycle and the worker restart; the rest needs the integration suite | | 10 | `pkg/relay/config` | 309 | **done**: 11 unit tests over `Load` with a controlled environment, 99.6% | | 11 | `main.mx` (`sync`, `crawl`, `env`, proxy subcommands) | 629 | **done as far as the harness allows**: `main_test.mx` covers the pure helpers, the relay database and `clog`, and `main_session_test.mx` drives `syncOnce`, `outboxDiscover`, `outboxBootstrapRelayLists`, `crawlRelay`, `crawlPublishBatch` and `crawlPass` against loopback WebSocket peers (13 new tests, 26 in the package, `main.mx` 20.3% -> 59.6%). The rest is `runRelay`/`runSync`/`runOutbox`/`runCrawl` (infinite loops), `spawn*` (they exec the binary), `handleProxy` (TLS fetch) and `outboxBootstrapFollows` (a fixed real seed list), so they stay integration-only | | 12 | `pkg/nostr/envelope` (`auth`, `simple`, `event`, `req`) | 230 | **done**: 20 unit tests, 96-98% of the four files, NIP-42 AUTH included; the truncated-input test covers every parser's rejection paths (and found the truncated kinds array that crashed the relay) | | 13 | `pkg/grapevine`, `pkg/nostr/ws`, `pkg/nostr/signer/p8k` | 250 | **done**: grapevine 3 tests, ws loopback tests (20, `client.mx` 33.6% -> 97.8%), p8k 9 tests including BIP-340 vector 0 (96.7%) | | 14 | logger, metrics, access, pool | 100 | **done**: metrics 9 tests (98.5-100%), access 4 (100%), pool 3 (100%) | ## Open items (each with its repro) Everything that this audit found and the tree still gets wrong. The items the audit found *and* fixed are in the next section; the ones that were on this list and are now gone from it (`range` over a string, `strconv` bitSize=64, the untyped-constant argument width, `mute.Load`, `error == syscall.Errno`, ws loopback dial, `pkg/lol`, the worker-domain coverage dump, the store/pipeline/ negentropy test rewrites) have their evidence recorded as new rows in "Compiler bugs this audit found and fixed". | item | evidence | next step | | --- | --- | --- | | direct messaging is parked in the app | the AEAD fix (below) made the MLS worker reach `generating new KPP`, where it then panicked in Moxie's codec: `codec bad type: kind=0 size=0`, `WASM fatal - restarting`. The entry points were gated off again (sidebar, `/msg` routes, `mlsWorkerTypes`, `M_SET_PUBKEY`, `build-mls-wasm` out of the default targets, `mls.wasm` not served) with the mls code, worker and host plumbing left in the tree. Verified with the headless boot probe: the boot list is `verify/store/rproxy/signer/app/profile/feed/notif/domain ok` (no mls entry), `/msg/` lands on the app shell, and the console has zero mls or panic lines | fix the codec bug in the row below, then restore the entry points and drop the two pytest skips together | | the wasm MLS worker panics generating its first KeyPackage | the app's DM page now opens and the worker starts (`[mls-worker] handleInit: generating new KPP`), then Moxie's codec aborts: `codec bad type: kind=0 size=0` and `WASM fatal - restarting`, so no kind-443 event is ever published and `test_mls_dm.py`/`test_mls_broadcast.py` stay skipped. Traced with a temporary ring buffer in `codecEncodeValue`: the eight instructions before the panic are `KindPointer sz=4` (wasm32) and the ninth is the invalid type, i.e. some **pointer-to-X whose element descriptor is nil** in a wasm build. The AEAD itself is fine now (`web/common/marmot`'s round trip runs and passes natively) | narrow the offending type by printing the parent chain with types (the codec has no names) or by dumping the wasm build's type descriptors for `web/common/mls`; then fix the emitter of the zeroed descriptor. Re-enable the two pytest modules when it lands | | dialing any `wss://` URL aborts the process | a 10-line program that calls `ws.Dial("wss://purplepag.es")` prints `trying wss://purplepag.es` and then dies: `panic: runtime error at 0x...: interface dispatch: no impl for Write` (in a `-cover` test binary the same call dies with `caught signal SIGSEGV`). `ws://` to the same host and port is fine, `wss://127.0.0.1:1` fails cleanly at the dial, and the same source built by the compiler from before this round's shift fix aborts identically, so it is not this round's change; the build log's `discover crypto/tls` lists every file, so the package is in the closure. Consequence: `smesh crawl`, `smesh sync wss://...` and the outbox bootstrap (which dials `wss://purplepag.es`) crash the process, and no test reaches them | the `*tls.Conn` that `tls.Client` returns is assigned to the `net.Conn` variable in `ws.Dial` (`pkg/nostr/ws/ws.mx`), and the following `conn.Write(req)` has no arm; the cached `crypto/tls` `.mxh` does list `method Conn Write []byte->int32,error` and the `net` `.mxh` lists the `Conn` interface, so it is the pairing that emits `dispIfaceMethods` that misses this one - instrument that build for the pair and lock it with a loopback TLS fixture in moxie's phase6 | The last row that was open before this one (a legacy-built spawn program that SEGVs after its first exchange) was fixed with the futex park: the legacy compiler passes a nil destination pointer for a discarded receive (`<-done` with no assignment) while stage4 passes a scratch, and the runtime wrote the delivered value through it. `recvStore`/`recvZero` now treat a nil destination as "discard", and `phase6` `6k` runs under both compilers. moxie `39b164ac`. `testing.T.TempDir`/`Cleanup` is a deliberate non-addition, not a defect: a cleanup list has to live in root-arena memory or in a sovereign holder, and a cleanup closure cannot capture its test's locals. `defer` is the Moxie shape, and `testing.Short()` was added because it is stateless. ### Spawn channel parking (runtime) L21 says the index word is simultaneously the state and the notification, and that the parking primitive is a futex on that word with FUTEX_WAIT's expected-value check closing the check-then-sleep race. The spawn ring path did not do that: `PipeChanSend`/`PipeChanRecv` slept 1ms and retried, and the only shared futex in the tree was `PipeWaitChildReady`. The ring path now parks. `ringbuf.mx` gained two sleeper flags in its header (wake elision: the uncontended publish stays syscall-free) and four helpers. A receiver waits on `writeIdx`, a writer on `readIdx`, each passing the value it just loaded as FUTEX_WAIT's expectation, so a publish that lands between the check and the sleep returns EAGAIN instead of being lost. Every wait is bounded at 20ms because a crashed peer can never issue the wake; the loops recheck ring state and peer liveness after each return. `ringSend`/`ringRecv` wake the other side after advancing their index, and `ringClose` wakes both so a close unparks immediately. `runtime_unix.mx` gained `futex.WaitUntilShared` /`WakeAllShared`, which use the non-private syscalls the fork boundary needs (the private flag keys the wait queue per mm, so a child's wake can never reach a parent's wait). Measured with `strace -f -e trace=futex` on `tests/data/futexpark/main.mx`: the child waits on the shared ring word with the current index as its expectation and no FUTEX_PRIVATE_FLAG, and both sides issue FUTEX_WAKE on the same address. A `select` over several channels deliberately keeps the bounded timer poll (`chan.mx`): it cannot park on one ring's index without going deaf on the others, and it must keep servicing timers while it waits. Parking is sound in the dedicated single-exchange domains, which is where the send/recv helpers run. Two tests are owed by the change and both live in `tests/data/futexpark` + `phase6` `6k` (both compilers): 200 park/publish rounds assert no wakeup is lost, and a parent parked on a ring whose child exits unpacks, reports the closed channel and finishes well inside a 3s bound. moxie `7def0c30`. ### Compiler bugs this audit found and fixed | bug | evidence | fix | | --- | --- | --- | | `select` bound the comma-ok name to the received value | `case ev, ok := <-events:` with `chan *T` typed `ok` as `*T`, so `if !ok` negated a pointer and clang rejected the module (`%t83 = xor ptr %t82, -1`); reduced to a ~40-line probe with one receive case and two closed-channel cases | the pair now extracts the value from the receive slot and `ok` from the tuple's `recvOk` slot (`pkg/nostr/ws` compiles and runs; previously it could not build) | | `Engine.GetByID` answered `event not found` for stored events | it scanned `MakeEid(hash,0)..MakeEid(hash,Max)`, 16 bytes of bounds over an index that compares only 11 (`EidCmpLen`), so the range was empty; `negentropy.Syncer.FindHave` calls it and dropped every event, sending nothing | point lookup through `getEventSerial` + `GetBySerial`, the shape `queryByIDs` was already moved to; the store tests now assert a stored event and an unknown id | | a delete tombstone was invisible to point lookups | `sorted.File.Get` read the on-disk record without consulting the delete set that `Scan` honours, so `QueryEvents(&filter.F{Ids: a.ID})` still returned a deleted event while a filter-less query hid it | `Get` skips tombstoned records in the main file, the .buf sidecar and the in-memory buffer; asserted before and after `Flush` | | a from-source build of `pkg/find`/`pkg/grapevine` failed while a cached one succeeded | stage4's `prepareDispatchIndex` pre-pass skipped a package whose `.mxh` was cached (`continue`) instead of loading it. Packages run in dependency order, so when `pkg/store` had to be re-type-checked while `event`/`filter` were still cache-only, its signature was generated before those exports were in the registry and `generateMxh` wrote `__any` for the unresolved types (`method Engine QueryEvents __any->__any,error`); `mxhLoaded` then made the correct export a no-op, so every caller saw `{ i64, ptr }` where a slice belongs. The whole "IR-emit bug" in crawler/grapevine was this plus one test bug | `prepareDispatchIndex` loads the cached `.mxh` (stage4-only code: legacy has no such pre-pass); `pkg/find` and `pkg/grapevine` build from source and their tests pass (4 and 3) | | stage4 accepted `push` on a non-slice | `*tags = push(*tags, tag.NewFromSlice(...))` on a `*tag.S` compiled and emitted IR clang rejects (`'{ { ptr, i64, i64 } }' but expected '{ ptr, i64, i64 }'`), which read as a variadic/IR-emit bug in `pkg/crawler` and `pkg/grapevine`. Legacy rejects it: "push on non-slice type" | stage4's `push` lowering now checks the operand is a slice or basic and reports legacy's error; the two test helpers pushed onto the struct instead of its `T` slice, and `pkg/crawler` comes off the skip list (3 tests pass) | | a duplicate top-level declaration was accepted silently | `func main()` twice in one file compiled and ran the first body while the second was dropped; legacy reports `main redeclared in this block`. It also made `moxie test` on a command-line tool run the tool and never the tests | `registerFunc` records every declared name and rejects a repeat with legacy's wording; `concatTestSources` renames the tested package's own `main` out of the way so the generated test runner owns the entry (`moxie test ./cmd/mxcover`: 9 tests) | | a duplicate *method* is still accepted silently | `func (t *T) M()` declared twice compiles and `x.M()` returns 1; legacy reports `method T.M already declared`. The fix above covers top-level functions only | extend the same `declared` record to receiver-qualified method names | | the package cache key ignored `-cover` | a plain `moxie test` after a `-cover` run reused the instrumented bitcode and dumped counters to stderr; a cover build could as easily have reused plain bitcode and reported nothing | `pkgCacheKey` salts the hash when `bst.cover` (legacy already had it: `compiler.Config` is part of the action ID) | | `moxie test` judged the tested package by its synthetic `main` wrapper | `moxie test ./pkg/nostr/kind` failed with 129 `package globals are immutable outside init` errors on code that a normal build compiles, because the harness compiles the package as `main` and `isUserPackagePath("main")` is user code while `.../pkg/nostr/kind` is exempt | `cmdTest` records the path a normal build would use (`bst.testSrcPkgPath`) and `restrictPath` maps the wrapper back for every restriction check; `testWrapsLibrary` likewise stops `init() is not allowed in main package` firing on a library's own `init()` | | `range` over a string loaded a 4-byte word per byte | the loop value was loaded as i32 for every element type, so `for i := 0; i < len(s); i++ { s[i] }` inside a range was fine but `for _, c := range s` read four bytes and sign-extended. Consequence: `pkg/blossom`'s `isHex` rejected every real 64-hex hash (all blob GET/HEAD answered 404) and `net/url.validOptionalPort` rejected `http://host:8080` as an invalid port | `_mxc_stage4/ir_iter.mx` `emitNextSlice` loads an i8 for a string element and `zext`s it to the tuple's i32 value slot; blossom's blob GET/HEAD now returns the blob and the `host:port` upstream test passes | | `strconv.ParseInt`/`ParseUint` returned `0, nil` for bitSize=64 | `ParseUint("123",10,64)` was `(0, err)` while `Atoi("123")` was 123: `maxVal := uint64(1)<= the operand width yields 0, and stage4's codegen let the x86 backend mask the count (64 became a shift by 0), so `maxVal` read 0 and every digit tripped the range check; on top of that `const maxUint64 = 1<<64 - 1` folded as a 64-bit *signed* value, so `maxUint64/10` was `0` instead of `1844674407370955161`. Consequence: `pipeline.isExpired` never returned true and NIP-40 expiration was unenforced, and every `Content-Length`/chunk-size body read as empty | `emitShift` materialises Go's shift rule (a constant count >= the width folds to 0; a dynamic count gets an explicit overshift select, with `ashr` capping at width-1 for the sign fill), and `ConstInt` carries an `Unsigned` mark so an untyped constant whose exact value is >= 2^63 folds its divide/remainder/right-shift unsigned. `ParseInt`/`ParseUint` at 64 bits, both range ends and out-of-range rejection now match Go; `pipeline`'s expiry assertion and `mediaproxy`'s decoded chunked body are asserted rather than commented | | an untyped constant argument to a typed parameter was emitted 32 bits wide | `func id(n int64) (r int64) { r = n; return }` then `id(-42)` printed `4294967254`; `var v int64 = -42` and `int64(-42)` were correct | call arguments are converted to the parameter's width (`callArgTexts`/`coerceIntArg`), so `id(-42)` is -42 | | `mute.Blacklist.Load` never loaded a real mute list | it accepted only a 32-byte raw or 64-byte hex p-tag, but `tag.Unmarshal` binary-optimises a 64-hex p-tag to 33 bytes and it stays 33 through the store round trip, so every real list was skipped and nothing was muted | accept the 33-byte binary form; `mute_test.mx` asserts a 33-byte p-tag is loaded | | stage4 compiled `error == syscall.Errno` to false | `var e error = syscall.Errno(syscall.EINPROGRESS); if e == syscall.EINPROGRESS` took the else arm while Go and legacy match, so `src/net/fd_unix.mx`'s `connect` treated a non-blocking connect's EINPROGRESS as fatal and `net.Dial` answered `connect: operation now in progress` for every TCP dial, loopback included | interface-vs-concrete `==`/`!=` boxes the concrete side and calls `runtime.interfaceEqual`; `net.Dial` reaches loopback and the `pkg/nostr/ws` loopback tests pass as written | | a stage4-built program printed a phantom `%!(NOVERB)` after every formatted value | `fmt.Sprintf("%d", int64(-42))` printed `-42%!(NOVERB)` where the same source built by legacy printed `-42`; the relay log carried `... interrupted system call%!(NOVERB)%!(NOVERB)` | a labelled `continue` in `fmt`'s scanner ran the inner loop's post statement when the format was exhausted; `buildBranch` now honours the label, and `pkg/lol`'s 9 tests pass | | worker-domain coverage was lost when a domain was killed | `ingest_worker.mx`, `broadcast_worker.mx`, `proxy_worker.mx` and `blossom_worker.mx` reported 0% while the store domain reported 52-66% | it was not a missing dump path: `coverDump` issued five `write(2)` calls per line onto one shared append file, so lines interleaved and only 1846 of 9053 parsed; each line is now assembled in one buffer and written once. The broadcast domain was also forked before `InitSignals()`, so the fixture's group SIGTERM killed it before it could dump - `server.InitSignals()` now runs before `broadcast.PreSpawn()`. 16 of 16 domains dump and every line parses | | a standard-library import path could resolve into a nested module | a build failed in `crypto/hkdf` with a clang error on `extractvalue {ptr,i64,i64} %t14` from `[32 x i8]`: `discoverPkg`/`resolveDir` looked for `` relative to the module first, so `crypto/hkdf` resolved to `web/common/crypto/hkdf` | both compilers resolve `root/src/` before the module-relative fallback (bilateral: `main.mx` and `legacy/loader/mxlist.go`) | | a re-exported constant lost its value through the `.mxh` round trip | `x := uint64(math.MaxUint64)` then `x == 0` was true, and every negative exported constant read back as 0: `ConstInt.String` printed the signed `-1` for the unsigned-marked maximum, and `ssaParseInt64` stops at any non-digit, so the reader saw `-1` and parsed 0; a value above int64 also wrapped without its unsigned mark, so `math.MaxUint64/10` folded as `-1/10 == 0`. In the store this was not cosmetic: `flushSidecars` seeds its minimum with that constant, so the minimum read 0 and `writeSidecars` set the checkpoint *back* to 0 on every incremental flush - `TestIncrementalRecovery` had been written around it with a manual `ckpt.Set` | `ConstInt.String` prints the exact unsigned decimal when the constant carries the unsigned mark, and the `.mxh` reader parses the sign and the full 64-bit range, marking a value above int64 unsigned. `uint64(math.MaxUint64)`, `var y uint64 = math.MaxUint64`, `math.MaxUint64/10` and `math.MaxUint64>>32` are all exact now, and `TestIncrementalRecovery` drives the real `quickCheckpoint` and asserts the checkpoint it writes before the crash and the one recovery advances it to | | the math arch asm was declared but not callable | `math.Ceil(2.1)` killed the process (`caught signal SIGSEGV`) while `math.Sqrt(4)` was 2; `math.Floor`/`Trunc`/`Exp`/`Hypot`/`Log`/`Max`/`Min`/`Modf` were equally unusable. `src/math/*_asm.mx` set `haveArch* = true` for amd64, and `sysroot/obj/asm_math_*.o` (ported from Go in `sysroot/obj/src/*_gas.s`) read the argument from `8(%rsp)` and wrote the result to `16(%rsp)` - Go's stack ABI, while the compiler emits the register convention its runtime asm uses (`moxie_longjmp` takes its pointer in `%rdi`). The call returned garbage and wrote 8 bytes above the caller's frame. Legacy never linked those objects, which is why the same source worked there | the seven `*_asm.mx` files and the unusable objects are deleted, and the portable files that define `haveArch* = false` are no longer arch-gated, so both compilers run the portable implementations | | every floating-point constant that went through the `.mxh` | six separate faults, each reproduced by comparing stage4 with legacy on one probe: `typeDescWrite` wrote every untyped kind as `int32`, so `const MaxFloat64 untyped_float ...` came back an integer constant; `ConstFloat.String` returned `"0.0"` for any constant folded from an expression, so `math.MaxFloat64` was imported as 0; `parseFloatLocal` had no hex-float support (`0x1p1023` parsed as 0) and built decimal exponents by repeated multiplication; `NormalizeLLVMFloat` appended `".0"` to `+Inf` and to `0x1p1023`, which clang rejects (`bitcast double +Inf.0`, `store double 0x1p1023.0`); `1 - 0x1p-52` folded in integer space because the fold tested the left operand first, so `math.MaxFloat64 = 0x1p1023*(1+(1-0x1p-52))` folded to 2^1023*2 and overflowed to `+Inf`; and a hexadecimal float needed the bit-pattern form LLVM accepts | untyped kinds have their own `.mxh` tokens and are mapped back; `ConstFloat.String` writes a text `strconv.ParseFloat` round-trips; hex floats are parsed with `strconv.ParseFloat` and emitted as `0xK...K`; Inf/NaN are emitted as their hexadecimal bits; an integer operand against a float operand is promoted before the fold | | an untyped numeric constant was stored or passed at its own width | `var a float64 = 4` stored the integer bit pattern 4 into a double slot (`a` printed `2e-323`, `math.Float64bits(a)` was 4, and `math.Sqrt(4)` answered 0 because `sqrt` reads `Float64bits`); `math.Sqrt(4)` passed the integer 4 where a double parameter expected it, so the callee read 0 and `Sqrt`/`Pow`/`Hypot` answered f(0) while `Exp(1)` answered 1; `x &^= 1<<(64-12-e) - 1` folded its mask at the untyped i32 default (0xFFFFFFFF), so `math.Modf(2.1)` returned 2.0999984 and `math.Ceil`/`Floor` returned x±1 | `emitStore` converts an untyped numeric constant to the destination's type, `checkCallArgs` converts an untyped numeric argument to the parameter's type, and a compound assignment gives its shift operand the assignment target's type | | the Node signer harness could not instantiate the signer wasm it is given | after the artifacts were refreshed, `make test-signer` failed at `WebAssembly.instantiate`: the module imports `bridge.channel_close`/`channel_recv`/`channel_send`, `bridge.signer_signal_ready` and `wasi_snapshot_preview1.fd_read`, and `web/_test/signertest/run.mjs` supplied none of them ("function import requires a callable"). The harness had not been run against a current artifact since the rebuild | the harness now supplies the spawn-channel ABI (close is a no-op, send/recv throw so a future spawn cannot silently do nothing), the readiness callback and an EOF `fd_read`; its wrong-password assertion checks the error the signer actually returns (`mgmtUnlockVault` surfaces the hash diagnostic instead of a bare `false`). `make test-signer` 108/108 | | an untyped constant that does not fit 32 bits | Moxie's int is 32 bits on every target, so these must be exact where a type is given and an error where the default type applies; stage4 evaluated them at the untyped 32-bit default and truncated. `0 - 9223372036854775807` printed `1`, `math.MaxUint64 / 10` printed its low half (`-1717986919`), `math.MaxUint64 >> uint32(32)` printed `-1`, and `10.0 >= 1 << 63` was true - which sent `math.Pow` down its overflow branch for every integer exponent (`math.Pow(2, 10)` = `+Inf`). Legacy rejects the untyped forms ("overflows int") and answers the typed ones exactly | the emitter materialises an unsigned-marked untyped integer at i64 and runs an untyped operation at the wider of its operands; `ssa_builder.mx` `checkDefaultRepr` reports an untyped constant that does not fit the type it takes (interface argument, `:=`, `var x = ...`) with go/types' verdict; `cvFloat` and the integer/float promotion use the unsigned value, and an integer literal whose exact value is >= 2^63 is marked unsigned like a folded one. Probes agree with legacy line for line, and `math.Pow(2,10)` is 1024 | a parameter named `b1` collided with a generated block label | LLVM gives blocks and values one namespace, and stage4 named its blocks `b1`, `b2`, ... and `_entry`, so `func f(b1 byte)` failed with "'%b1' is not a basic block" at `br label %b1` (a parameter named `_entry` failed with "unable to create block named '_entry'"). `pkg/store`'s test helper had been renamed to `k0..k3` to work around it | generated labels are dotted (`bb.1`, `bb.entry`), which no source identifier can spell; `tests/regressions/should_compile/block_label_names.mx` compiles and runs a `b1`/`b2`/`_entry` program, and the old compiler rejects it | a duplicate *method* was accepted silently | `func (t *T) M()` declared twice compiled and `x.M()` returned 1; legacy reports "method T.M already declared". The top-level redeclaration check covered functions only | the check is receiver-qualified (`method T.M`), so `T.M` and `U.M` coexist while a second `T.M` is rejected with legacy's wording; `tests/restrictions/reject_duplicate_method.mx` locks it | a package-level `var a, b T = x, y` gave every name one value | `var g1, g2 int32 = 3, 4` printed `4 4` and `var f1, f2 float64 = 1.5, 2.5` printed `2.5 2.5` in a stage4 build - the init list was built once and its result stored to both names. The same defect in the function-scope form is what broke `bytealg.HashStrBytes` (row below). legacy answers the integer case correctly (`3 4`) but prints `0 0` for the float pair, so the two compilers are wrong in different ways and stage4 is now the correct one; that legacy divergence is recorded in the open items | both loops that build package variable initializers (`__varinit` and the in-place path) build one value per name, as `buildLocalDecl` now does; `tests/regressions/should_compile/multi_value_var.mx` covers int32/uint32/float64/slice pairs and the loop-body shape | | `bytes.Contains`/`bytes.Index` missed a present needle | `bytealg.IndexRabinKarp` (the path `bytes.Index` hands over to once its brute scan has failed `4+i>>4` times) answered -1 for needles that are present - 23112 mismatches against a manual scan in a sweep over offsets and buffer sizes. The cause was one line: `HashStrBytes` computes its rolling factor with `var pow, sq uint32 = 1, PrimeRK`, and stage4 built a multi-value initializer list once and stored the *first* value to every name, so `sq` was 0, the factor came out `PrimeRK^14` instead of `PrimeRK^13`, and every rolling hash after the first was wrong | `buildLocalDecl` now builds one value per name for `var a, b T = x, y`; `HashStrBytes` returns `PrimeRK^13` and the sweep reports 0 mismatches. `src/internal/bytealg/bytealg_test.mx` runs the differential sweep (Index and LastIndexRabinKarp against a manual scan, present and absent needles) in `run_tests.sh`, and `tests/regressions/should_compile/multi_value_var.mx` covers the declaration shape | `secp256k1.ECDH`/`LiftX` accepted an x coordinate >= the field prime | `Signer.ECDHRaw(0xFF * 32)` returned a shared secret while `Signer.InitPub(0xFF * 32)` was rejected: `feFromBytes` copies 32 bytes into the limb representation without reducing, and `LiftX` lifted the non-canonical value. BIP-340 requires "fail if x >= p", and vector 14 of the vendored CSV (x = p+1) only passed verification by accident | `LiftX` rejects `x >= p` with `feCmp(x, _feP())`; `src/crypto/secp256k1/bip340_vectors_test.mx` lifts p, p+1 and 0xFF*32 and requires all three to fail (and the generator's x to succeed), ECDH must refuse the same key, and `p8k_test.mx` now asserts `ECDHRaw`/`ECDH` reject 0xFF*32 | stage4 accepted a method call on a non-function field | `sb.Info()` where `Info` is a `types.BasicInfo` (uint32) field of `*Basic` was accepted by stage4 and only failed when legacy compiled the same file (`cannot call sb.Info (variable of uint32 type types.BasicInfo)`) | fixed by the `reportUndefinedField` fallback in `ssa_builder.mx` `buildCall`: a selector that is not a method, interface dispatch, function field or generic instantiation is reported instead of dropped. Verified on the current tree: stage4 answers `; Info undefined (type main.Basic has no field or method Info)` and legacy answers `uint32 is not a function` | | legacy dropped every package-level float initializer | `var a1 float64 = 1.5` prints `0` under legacy while stage4 prints `1.5`; every package-level `float32`/`float64` variable is affected, single and multi-value. `LLVMConstRealGetDouble` returns its double in XMM0, but the purego build of go-llvm read the return register RAX, so the interp's `rawValue.set` stored zero for every float constant. legacy is the bootstrap compiler, so any package-level float variable in the compiler or stdlib was silently zero | the glue library gains `MoxieConstRealGetBits`, which returns the IEEE-754 bit pattern in RAX, and `Value.DoubleValue` reads that. `build.sh` now rebuilds the glue when a source is newer than the `.so` (the purego bindings resolve glue symbols with `mustSym`, so a stale `.so` is a hard failure). `tests/data/globalinit` plus a `phase6` check asserts single and multi-value float32/float64 package globals in both compilers. moxie `ea36dbac` | | `len()`/`cap()` on a channel was 0 | stage4's `len`/`cap` lowering had no `*TCChan` arm, so it fell through to "return zero" while legacy emitted `runtime.chanLen`/`runtime.chanCap`. A buffered channel created with `chan int32{4}` answered `cap == 0` under stage4 and `cap == 4` under legacy, so any code sizing work off `len`/`cap` read the wrong ring | stage4 now emits the same runtime calls, converting the runtime's i32 to the result width. `tests/data/chanlen` plus a `phase6` check asserts the capacity, the length of a new channel, and the length after a send and after a receive in both compilers. moxie `aeda3f73` | | `test_relay_proxy_high_event_volume` flaked and was deselected from `make cover` | the poll after EOSE initialized `quietSince = eoseAt`, so an empty 2s gap counted as "quiet" and the run finished with `eventCount: 0, gotEose: True`. That is the deterministic `smesh-test` failure and the stock flake (about 1 isolated run in 4): the worker lingers 25s after EOSE for exactly these stragglers, so the first result can land later | the poll only treats the stream as quiet once at least one event has arrived, and waits for the first event up to `PROXY_VOLUME_WINDOW_MS` (default 30s, longer than the linger). `make cover` sets `PROXY_VOLUME_WINDOW_MS=180000` and no longer deselects the test. Measured with the old poll against `smesh-test`: `eventCount 0`; with the fix it passes against both binaries. `make test` exit 0, `make cover` exit 0, sites 8728 covered 4026 (46.1%) at the time. smesh `9957fa8e` | | `testing.T.Fatal` did not abort the test | after a `Fatal` the rest of the test body ran, so a failing assertion could also crash later and hide which check failed. Root cause measured: **stage4 never emitted the per-call defer checkpoint**. `w()` sniffed an emitted segment for ` call `, but every call emitter writes the line in fragments (`e.w(" ")` then `e.w("call ")`), so the sniffer never fired and every frame's `JumpPC` stayed 0 - a `moxie_longjmp` jumped to address 0. stage4 also lowered explicit `panic("const")` to the abort-only `_panicstr` while legacy used the recoverable `_panic`, and the first testing-package attempt crashed because a deferred method value goes through a thunk that called a garbage pointer | join each call prefix into one segment so the checkpoint fires, suppressing it inside a multi-impl interface dispatch (the split broke the merge PHIs); lower explicit panic to `_panic`; declare `runtime._recover` as returning a string as legacy does; and make the recover handler a plain function over a package global instead of a deferred method. `tests/data/fatal` plus a `phase6` check asserts Fatal and Skip abort the body, the run continues, and each is reported. moxie `c7ca5f37`. Runtime-error panics (`nil` deref, index out of range) still abort without unwinding in **both** compilers - `runtimePanicAt` never consults the defer frame - which is a separate item | | `crawlPublishBatch` SIGSEGVs nondeterministically under `moxie test` | `crawlPublishBatch("ws://127.0.0.1:1", batch, out)` with any `*os.File` open panicked at a fixed low address in some runs and returned 0 in others; `ws.Dial` alone returned the refused error cleanly, `clog` alone was fine, and a trivial `func([][]byte) int32` was fine. It is the unchecked-defer-frame class: the function has a `defer`, stage4 never armed the frame's `JumpPC`, and a longjmp jumped to address 0 - the same defect that blocked `testing.T.Fatal` (moxie `c7ca5f37`). The exact panic site was not isolated | `TestCrawlPublishBatchDialError` drives the dial-error branch again and asserts it publishes nothing. 26 consecutive `moxie test` runs are clean (13 tests each) and `make test-unit` is green, so the branch is covered. smesh `387b902e` | | stage4 had no `clear` lowering at all | `clear(x)` compiled to nothing. `math/big`'s `basicMul` opens with `clear(z[0:len(x)+len(y)])` and then accumulates into `z`, so a reused accumulator kept the digits of its previous product: `big.Int.Exp(10,20)` was 10^20+10^5, `big.Rat.SetString("1.7976931348623157e308").String()` was wrong, and a 300-digit `Text(10)` divided by a stale zero and died with SIGFPE. `clear` was listed in the builtin switch with `close`/`delete`/`print` but the emitter had no arm for it, so the call was dropped silently - the same class as the missing `len(chan)` arm (row above) | `ir_call.mx` emits `llvm.memset.p0.(buf, 0, len*sizeof(elem))` for a slice and `runtime.hashmapClear` for a map, mirroring legacy's inline memset; `ssa_builder.mx` rejects anything else before the compileErrors gate so the failure is a failed build, not a printed line after linking. `tests/data/clearzero` + `phase6` `6j` (both compilers) assert whole/sub/prefix/suffix slices, multi-byte elements, nil slices, and maps; `tests/restrictions/reject_clear_non_slice.mx` locks the rejection; `src/math/big/natconv_test.mx` (+`run_tests.sh`) pins `Exp`, the 309-digit round trip and the `Rat` repro. moxie `7def0c30` | | stage4 typed a folded untyped int/float constant by its left operand | `1/2.0` folded to the float value 0.5 but kept the left operand's untyped-int type, so every use truncated it: `1/2.0` was 0, `7/2.0` was 3, `1/math.Ln2` was 1 and `math.Log2(10)` was 3.529996 instead of 3.321928 - which also mis-sized `math/big`'s base-10 buffer (the "leading two digits gone" symptom). The non-fold path already promoted correctly; only the SSA builder's fold took `xc.typ`. A first cut of the fix then let a constant shift adopt its count's type, which truncated `0x20000000000000 >> uint32(3)` - caught by the existing `shift_const_width` regression before it shipped | the fold promotes only when the left type is untyped, never for `OpShl`/`OpShr` (a shift's result type follows its left operand), and takes the folded value's kind for two untyped operands. `tests/data/constpromo` + `phase6` `6i` (both compilers) assert mixed int/float division, multiplication, addition and `math.Log2`. moxie `7def0c30` | | the base-10 divisor table cached arena allocations in a global | `var d big.Int; d.Exp(big.NewInt(2), big.NewInt(1074), nil); var r big.Rat; r.SetFrac(one, &d); r.RatString()` SIGFPEd in `bits.Div32` (legacy aborted with `divide by zero`), and `2^260.String()` crashed the same way while `2^250` was fine - the break lands exactly where `nat.itoa` enters its recursive branch. `divisors` reported a divisor with 256 bits and a nonzero top word, and the very next `convertWords` saw the same entry with a zero top word: the table was cached in the package global `cacheBase10`, but the nats it holds are allocated in the calling frame's arena, so the metadata outlived the words and the next conversion divided by a recycled buffer | the table is built per conversion in the caller's arena (`cacheBase10` and its `ndigits == 0` extension path are gone), which is sound because the divisions it feeds dominate the squarings that build it. `bits.Div32` also gained the `y == 0` guard `Div64` already had, so the next occurrence is a named panic instead of a hardware SIGFPE. `src/math/big/natconv_test.mx` `TestRatSmallestSubnormal` (1/2^1074, 326 bytes) plus a 250..1200-bit `2^k` sweep lock it; `run_tests.sh` runs the suite. moxie `7def0c30` | | untyped float constants were rounded where go/types is exact | `math.MaxFloat64 == 1.7976931348623157e308` was true in a stage4 build and false in a legacy build and in Go: `constEqual` compared the rounded `float64`, and the `.mxh` writer exported a folded constant as its shortest round-tripping decimal, so an importer re-read a value whose decimal is the literal on the other side of `==`. `0.1+0.2 == 0.3` was false where Go says true (constants are exact rationals), and `1.0/3.0 == 0.3333333333333333` was true where Go says false | `ConstFloat` carries its exact `*big.Rat`: literals parse it (decimal via `Rat.SetString`, C99 hex floats via a hand-rolled mantissa/exponent parse, which Go's `Rat` does not accept), every fold is `Rat` arithmetic with `V = Exact.Float64()` for codegen, comparisons use `Rat.Cmp`, and a sized conversion rounds as Go does. `.mxh` writes `Exact.RatString()` for folded constants (parsed back by `ExactFloatText`) while `String()` keeps the human decimal for diagnostics. `tests/data/exactfloat` + `phase6` `6l` (both compilers) assert the two `MaxFloat64` comparisons, the in-package form, and the two exact-arithmetic cases. moxie `7def0c30` | | a legacy-built spawn program SIGSEGVs after its first exchange | `spawn(worker, in, out)` with `in <- 21; <-out` printed `42` and then died in `runtime.memcpy(dst=0x0, size=24)`: `main`'s `<-done` is a discarded receive, the legacy compiler's codegen passes a nil destination for the unused operand (stage4 materialises a scratch), and `trySend` copied the delivered value straight through it. It reproduced with either compiler's runtime bake, so it was not a blob mismatch | `recvStore`/`recvZero` in `runtime.mx` make a nil receive destination the defined "discard" case and are used by `bufferPop`, `trySend`, `tryRecv`, `tryPipeRecv` and `PipeChanRecvRaw`; `phase6` `6k` now runs `tests/data/futexpark` under both compilers. moxie `39b164ac` | | `-cover` produced both halves of a measurement but nothing joined them | `make test-unit` could not report coverage at all: `moxie test -cover` wrote the site table and started the instrumented binary, and the only join was the standalone `mxcover` command, so the unit table above had to be produced by driving that command from a shell loop outside the compiler. A reviewer asking "what is the coverage?" had to know the recipe | the join lives in `pkg/mxcover` (a package, so both callers share one implementation); `moxie test -cover` now points `MOXIE_COVER_OUT` at a per-run counts file, runs the instrumented test binary and prints the totals, per-file table and uncovered list itself, and `mxcover` is a thin CLI over the same package. `make unit-cover` is the package loop with `-cover`, so the report comes from the harness. Legacy's harness mirrors the print (`legacy/cover`, with `go test ./cover` asserting the shared fixture); legacy's own `moxie test` still cannot build these packages in this tree, which is why `phase6` `6m` locks the mxcover command against the fixture and `6n` runs the parser units. moxie `421d93b6` | | `event.UnmarshalBinary` accepted a truncated frame | `io.Reader.Read` returns whatever it has with a nil error, and every fixed-size field was read with `r.Read`, so a frame cut short left the rest of the field zero-filled and decoded as a valid-looking event - an ID of 20 real bytes and 12 zeros, a zero-padded signature. The JSON path length-checks every field, so only the binary path was exposed. Found by the truncation test added with this round's coverage work (`pkg/nostr/event/event_paths_test.mx`), which asserts every strict prefix of an encoded event fails | every field read goes through `io.ReadFull` (a local `readFull` helper), which turns a partial read into `ErrUnexpectedEOF`. The new test file raises the package's own-file coverage from 67.8% to 91.8% and covers `Clone`/`Free`/`EstimateSize`, the JSON entry points, both `Verify` outcomes and the sort helpers. smesh `8037fb4f` | | the ChaCha20-Poly1305 AEAD dispatched to an asm implementation that does not exist | `web/common/crypto/chacha20poly1305.Seal` returned an all-zero buffer for every input, so `Open` always failed "message authentication failed": the vendored package's amd64 file declared `chacha20Poly1305Seal`/`Open` as external functions and dispatched to them whenever the CPU had SSSE3 (all of them); nothing in the tree implements them. Found by writing the RFC 8439 known-answer test for the new coverage pass - the bug had hidden behind passing integration tests because they never round-trip a ciphertext through this wrapper | the amd64 variant is deleted and the portable implementation is no longer arch-gated (the same shape as the math arch-asm row above). `src/vendor/golang.org/x/crypto/chacha20poly1305/chacha20poly1305_test.mx` is the RFC 8439 §2.8.2 known-answer vector plus round trips at eleven sizes, tamper/wrong-AAD/wrong-nonce rejection, short-key rejection and an XChaCha20 round trip, and `run_tests.sh` runs it (10 packages now). moxie `bf3d24f1` | | a compiler binary that does not match the tree poisoned the newer compiler's package cache | `compilerHash()` keyed the cache on the compiler sources under `MOXIEROOT` plus `src/runtime` - deliberately not on the running binary, so the self-host fixpoint can converge - so an older binary run against a tree whose sources changed computed the NEW source hash and wrote objects it compiled under the OLD sources into the directory the new compiler read. Measured while narrowing: after adding a field to a struct and rebuilding a dependent without `-a` the dependent was correct (the dep hashes are in the key); the compiler-binary mismatch is what remained | fold a content hash of the running binary into the cache DIRECTORY name only, never into generated code, so identical generations still emit identical binaries. `phase6` `cacheid:stage4` copies the compiler, appends a byte, and asserts the `mxc env` CACHE path differs while `mxc version` still prints the source hash. The fixpoint still converges (131/131). moxie `5e7dd69e` | | a shift's untyped left operand was typed by its count | `func f(c byte) (r uint32) { return 1 << (c % 32) }` answered 0 for c = 13 where legacy answered 8192, and `bytes.TrimRight([]byte("\r\n"), "\r\n")` returned `"\r\n"` unchanged: stage4 typed the shift as the *count's* type (uint8 for a byte count), so `emitShift`'s overshift select - Go makes a count >= the operand width shift to zero - rejected every count >= 8. The stdlib's `asciiSet` (`[8]uint32`) is built with `as[c/32] |= 1 << (c % 32)`, so every `bytes.Trim*` silently stopped trimming; the mediaproxy header loop then never stopped at the blank line and `fetchOnce` answered `read header: EOF` for every response once the peer closed. Found by the loopback HTTP origin added with this round's coverage work, after `strace` showed the server writing all 75 bytes and the client's single `read` receiving them | a shift whose left operand is untyped now takes `defaultReprType` (int32, or the widened 64-bit type when the constant does not fit) and stays marked context-dependent; the return statement, call arguments, `var` declarations and composite-literal elements join assignments, compound assignments and conversions as retype sites, so `return 1 << (c % 64)` from a uint64 function and `[]uint64{1 << (c % 64)}` are 64-bit shifts. `tests/data/shiftconst` + `phase6` `6o` assert the untyped and typed forms, the 64-bit return, argument, declaration, slice/map literal, `if` initializer and package variable in both compilers (legacy was already correct and is unchanged). moxie `8d35c38a` | | a reslice past the end gave the slice a negative length | `t := []byte{}[1:]` had `len(t) == -1` under both compilers (legacy's `createSliceBoundsCheck` is a documented no-op), so the tree's `for ; len(r) > 0; r = r[1:]` loops left the cursor at -1 and every `if len(r) == 0` guard that followed never fired: a REQ whose filter was cut short (`["REQ","s",{"kinds":[1`) read the byte after the input as a key and killed the relay with SIGSEGV. Found by the envelope truncated-input test. A first cut that clamped low/high/max broke the self-host chain (`codec default bad size`), and a rebuild ordering mistake made the low-only clamp look like it failed 10 regressions | clamp the low bound only - `low = umin(low, len)`, then `umin(low, high)` - so every valid slice (`0 <= low <= high <= len`, including `s[:cap(s)]`) is byte-identical and an exhausted or inverted range is empty; stage4 emits the two `icmp ult`/`select` pairs in `emitSliceOp` (`uminIpt`), legacy does the same in `clampSliceLow`. `tests/data/sliceres` + `phase6` `6p` cover the empty reslice, the consume loop, capacity extension, an inverted range and a cursor past the end in both compilers; the smesh envelope test feeds the crashing REQ again. `tests/run.sh --full` 146/146 with the fixpoint converged, `run_tests.sh` 10/10. moxie `dd8ce5e2` | | the ChaCha20-Poly1305 AEAD returned zeros in application builds | `Seal` was all zeros and `Open` answered `message authentication failed` when the package was a *dependency* of any `moxie build`, while the root-package RFC 8439 known-answer test passed. The doc first filed it as "a stdlib package is miscompiled when it is a dependency", but the root cause is the port, not the codegen: the Go `sliceForAppend` idiom splits one allocation into a `head` and a sub-slice `tail`, and Moxie relocates **each returned slice on its own** - the tail comes back as a copy, so the ciphertext and tag written through it never reached the returned buffer | rewrote `sealGeneric`/`openGeneric` aliasing-free (build the ciphertext and tag in their own buffers and concatenate; `Open` writes nothing before the tag verifies), removed the now-unused `sliceForAppend`, and made `chacha20`'s arch flag a function because legacy's go/types cannot fold `runtime.GOARCH == ...` in a constant expression. `tests/data/aeaddep` + `phase6` `6q` assert the RFC vector in an application build under both compilers; `web/common/marmot`'s round trip is un-skipped (18 tests). moxie `ac5b4093` |