Sixty `TODO("Not yet implemented")` bodies sat across four no-op repositories -- 41 of
45 on `NostrRepository`, 12 of 37 on `ChatRepository`, 6 of 7 on `MarmotRepository`,
1 of 1 on `SearchRepository`. f46a7c62 fixed the one that was firing and flagged the
rest for their own change; this is that change.
**Why a stub here is not one test's problem.** These are reached from view models that
call their repository inside `viewModelScope.launch(Dispatchers.IO)`, a coroutine nobody
awaits. The `NotImplementedError` escapes to the platform handler, where
kotlinx-coroutines-test's `ExceptionCollector` stashes it in `unprocessedExceptions` --
a `runBlocking` test has no `TestScope` registered to take it -- and the *next* `runTest`
drains the stash and rethrows it as `UncaughtExceptionsBeforeTest`, under its own name,
for a call it never made. Reproduced before touching anything: `jvmTest --rerun-tasks`
failed `NavigationRoutingTest > an account already on the device does not stay on the
loading screen` with four suppressed `NotImplementedError`s, each naming
`NostrRepository.kt:376` from `ChatMessageListViewModel.kt:366`. Seventeen jvmTest files
build on the Nostr one alone.
**Inert, not loud -- for every member.** The question per member was whether a silent
body or a deliberate throw is right, and the callers settle it: the only two populations
are `@ConformancePreviews` composables in commonMain and test fixtures, and both want a
repository that has no data rather than one that refuses. A preview that throws is a
broken preview. `NO_OP_DKG_REPOSITORY` and `NO_OP_FROST_SIGNING_REPOSITORY` already work
this way, so this follows that idiom instead of inventing a second one.
**Throwing is never the mechanism for being loud.** Keeping a throw for members that
"should not be called" was the alternative, and it is wrong twice: it re-opens exactly
this bug the next time such a member is reached from an unawaited launch, and it puts the
assertion in the fixture instead of the test. A member a test must not call is that
test's assertion to make -- override it with a recorder and assert it stayed untouched.
**A callback is not invoked.** `onCompletion`, `onSuccess` and `onError` are left alone
by `createNewProfile`, `createNewTextNote`, `leaveChatRoom`, `softDeleteChatRoom` and
`deleteChatRoom`. Calling `onCompletion` would say work happened that did not, which is
how a test comes to pass on the fixture rather than on the code; a test that wants a
branch fired overrides the member and fires the one it is testing.
**A flow emits its empty value once, rather than never.** `flowOf(null)` and
`flowOf(emptyList())` throughout, and `ChatRepository`'s two existing `flow { }` stubs
converted with them. An empty flow leaves a screen in Loading for ever; one emission lets
it render its empty state, which is what a preview is for.
**`createMlsDirectMessage` is the one that cannot say "nothing".** It owes the id of a
room and `String` has no absent value, so it returns `""`, documented as such. The
signature is the real oddity -- both creators beside it can report failure
(`createNip17ChatRoom` returns null, `createMarmotGroup` an `Outcome`) -- but changing it
would touch `DatabaseChatRepository` and its caller, so it is flagged in a comment rather
than widened here. Nothing reaches it in practice: `initiateNewChat` calls it only once a
profile and a key package have both resolved, and against this object neither ever does.
**Nothing was passing vacuously, checked rather than assumed.** The risk in a blanket
sweep is a test that currently fails honestly starting to pass on an empty body. No test
anywhere asserts `NotImplementedError`, and the suite went from 982 tests with 1 failure
to 983 with 0, with nothing newly failing -- including across the `flow { }` change and
the callback decision.
**The guard discovers the singletons rather than listing them.** `NoOpRepositoryTotality`
reads the `repository/` directory, keeps every file declaring a `NO_OP_` val, and reports
each `TODO(` in code with its file, line and member -- comments excluded, since the
contract on `NO_OP_NOSTR_REPOSITORY` names `TODO()` in its own prose. A hand-written list
was the first attempt and was already wrong: it named six files when the tree holds eight,
missing `MantraRepository` and `CachingImportRepository`, both total by luck rather than
cover. A floor of 8 goes with it, in the spirit of `m3-audit.sh`'s floors, because the way
a source-reading test fails silently is by finding nothing and passing. Both halves were
mutation-tested: a reintroduced `TODO()` fails naming `MarmotRepository.kt:49` and its
member, and a floor of 99 reports all eight files found.
**Calling the members for real was the alternative, and tells you less.** Sixty live
calls need a fixture for every parameter type, and a reflective sweep cannot tell a body
that threw from an argument it could not build -- so a failure would not distinguish the
bug from the harness. The invariant is a source-level one, so it is checked there.
**f46a7c62's narrow override is removed.** With the no-op total, the
`object : NostrRepository by ...` in `CeremonyQueueRoutingJvmTest` is dead weight, and its
comment had become false -- it asserted the member is "one of the forty-one `TODO()`
bodies". Replaced with the bare no-op and a past-tense note pointing at the guard.
**Not done here: the reporting mechanism.** The open question was whether the suite should
fail fast on an uncaught coroutine exception, or name the right test. Tracing
kotlinx-coroutines-test 1.11 shows the culprit's stack trace is already preserved, as a
suppressed cause on the `UncaughtExceptionsBeforeTest` -- nothing is lost, it is only
filed under the wrong name, and Gradle's console shows just the top line. A handler that
re-printed it would add noise, not information, so none was added; the missing name is
what this commit supplies. `MarmotRepository.NO_OP_MARMOT_KEY_PACKAGE_BUNDLE` is also
still misnamed -- it is a repository, not a bundle -- and left for its own change.
1171 jvm tests, 0 failed, under `--rerun-tasks`, on the merged tree. m3Audit clean,
`:composeApp:compileDebugKotlinAndroid` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…