fix(repository): a no-op answers all sixty calls, and a test keeps it total
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 commit is contained in:
@@ -19,7 +19,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
interface ChatRepository {
|
||||
suspend fun observeChatRoomListByPublicKey(publicKey: String): Flow<List<LocalChatRoom>>
|
||||
@@ -308,7 +308,13 @@ interface ChatRepository {
|
||||
): MarmotReindexReport
|
||||
|
||||
companion object {
|
||||
val NO_OP_CHAT_REPOSITORY = object: ChatRepository {
|
||||
/**
|
||||
* A chat repository with no database behind it: every room is absent, every
|
||||
* membership change is refused with a reason, and no member throws. See
|
||||
* [NostrRepository.NO_OP_NOSTR_REPOSITORY] for why a `TODO()` here is a silent
|
||||
* misattribution rather than a loud failure, and for the rules a new member follows.
|
||||
*/
|
||||
val NO_OP_CHAT_REPOSITORY: ChatRepository = object : ChatRepository {
|
||||
override suspend fun requestGroupHistoryIfMissing(
|
||||
chatRoomId: String,
|
||||
userPublicKey: HexKey
|
||||
@@ -338,9 +344,8 @@ interface ChatRepository {
|
||||
coordinatorPublicKey: HexKey,
|
||||
): String? = "No database"
|
||||
|
||||
override suspend fun observeChatRoomListByPublicKey(publicKey: String): Flow<List<LocalChatRoom>> {
|
||||
return flow { }
|
||||
}
|
||||
override suspend fun observeChatRoomListByPublicKey(publicKey: String): Flow<List<LocalChatRoom>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom> {
|
||||
return emptyList()
|
||||
@@ -375,22 +380,18 @@ interface ChatRepository {
|
||||
userPublicKey: HexKey
|
||||
): Boolean = false
|
||||
|
||||
override suspend fun getAllParticipantsWithPubKey(publicKey: HexKey): List<Participant> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun getAllParticipantsWithPubKey(publicKey: HexKey): List<Participant> =
|
||||
emptyList()
|
||||
|
||||
override suspend fun findDirectMessageRoom(userPublicKey: HexKey, peerPublicKey: HexKey): LocalChatRoom? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun findDirectMessageRoom(userPublicKey: HexKey, peerPublicKey: HexKey): LocalChatRoom? =
|
||||
null
|
||||
|
||||
override suspend fun getChatMessageListByChatRoomId(chatRoomId: String): List<LocalChatMessage> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun observeChatMessageListByChatRoomId(chatRoomId: String): Flow<List<LocalChatMessage>> {
|
||||
return flow {
|
||||
}
|
||||
}
|
||||
override suspend fun observeChatMessageListByChatRoomId(chatRoomId: String): Flow<List<LocalChatMessage>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun getOrCreateChatRoom(
|
||||
chatRoomId: String,
|
||||
@@ -407,13 +408,11 @@ interface ChatRepository {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getMarmotKeyPackageForPublicKey(publicKey: HexKey): MarmotKeyPackage? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun getMarmotKeyPackageForPublicKey(publicKey: HexKey): MarmotKeyPackage? =
|
||||
null
|
||||
|
||||
override suspend fun observeMarmotKeyPackageForPublicKey(publicKey: HexKey): Flow<MarmotKeyPackage?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeMarmotKeyPackageForPublicKey(publicKey: HexKey): Flow<MarmotKeyPackage?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun createNip17ChatRoom(
|
||||
userPublicKey: HexKey,
|
||||
@@ -424,23 +423,30 @@ interface ChatRepository {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The empty id, meaning no room was created.
|
||||
*
|
||||
* The only member here whose return type cannot say "nothing": it owes the id
|
||||
* of a room, and `String` has no absent value. Both creators beside it can say
|
||||
* they failed -- [createNip17ChatRoom] returns null, [createMarmotGroup] an
|
||||
* [MarmotGroupCreation.Outcome] -- so this one's signature is the odd one out
|
||||
* rather than this body being the odd answer. Nothing reaches it in practice:
|
||||
* `ChatRoomMessagingViewModel.initiateNewChat` calls it only once a profile and
|
||||
* a key package have both resolved, and against this object neither ever does.
|
||||
*/
|
||||
override suspend fun createMlsDirectMessage(
|
||||
name: String?,
|
||||
description: String?,
|
||||
userPublicKey: HexKey,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
): String = ""
|
||||
|
||||
override suspend fun inviteMember(
|
||||
localChatRoom: LocalChatRoom,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun addMembers(
|
||||
localChatRoom: LocalChatRoom,
|
||||
@@ -464,45 +470,36 @@ interface ChatRepository {
|
||||
localChatRoom: LocalChatRoom,
|
||||
messageType: Kind,
|
||||
directMessageRecipientPublicKey: HexKey?
|
||||
) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun observeUnsealedGiftWrapPayloads(publicKey: HexKey): Flow<List<GiftWrapPayload>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeUnsealedGiftWrapPayloads(publicKey: HexKey): Flow<List<GiftWrapPayload>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun sealGiftWrapPayload(
|
||||
giftWrapPayload: GiftWrapPayload,
|
||||
nostrSignerSync: NostrSignerSync
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
// None of the three calls onCompletion: nothing was left or deleted, and each
|
||||
// of these callbacks navigates away from a room that is still there.
|
||||
override suspend fun leaveChatRoom(
|
||||
localChatRoom: LocalChatRoom,
|
||||
onCompletion: () -> Unit
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun softDeleteChatRoom(
|
||||
chatRoom: ChatRoom,
|
||||
onCompletion: () -> Unit
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun deleteChatRoom(
|
||||
chatRoom: ChatRoom,
|
||||
onCompletion: () -> Unit
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun reindexMarmotGroupEvents(
|
||||
chatRoomId: String,
|
||||
|
||||
@@ -5,6 +5,7 @@ import press.mantra.compose.database.model.MarmotKeyPackageBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
interface MarmotRepository {
|
||||
|
||||
@@ -33,40 +34,35 @@ interface MarmotRepository {
|
||||
suspend fun encryptAndSendMarmotInnerEvent(marmotInnerEvent: MarmotInnerEvent, nostrSignerSync: NostrSignerSync)
|
||||
|
||||
companion object {
|
||||
val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository {
|
||||
/**
|
||||
* A repository with no key packages behind it: inert, and no member throws. See
|
||||
* [NostrRepository.NO_OP_NOSTR_REPOSITORY] for why a `TODO()` here is a silent
|
||||
* misattribution rather than a loud failure, and for the rules a new member follows.
|
||||
*/
|
||||
val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE: MarmotRepository = object : MarmotRepository {
|
||||
override suspend fun publishMarmotKeyPackageBundle(
|
||||
publicKey: HexKey,
|
||||
nsecPassword: String
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun rotateMarmotKeyPackageBundle(marmotKeyPackageBundle: MarmotKeyPackageBundle) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun rotateMarmotKeyPackageBundle(marmotKeyPackageBundle: MarmotKeyPackageBundle) = Unit
|
||||
|
||||
override suspend fun observeMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> {
|
||||
return emptyList()
|
||||
}
|
||||
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> =
|
||||
emptyList()
|
||||
|
||||
override suspend fun observeUnprocessedMarmotInnerEvents(publicKey: HexKey): Flow<List<MarmotInnerEvent>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeUnprocessedMarmotInnerEvents(publicKey: HexKey): Flow<List<MarmotInnerEvent>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun observeActiveMarmotKeyPackageBundle(publicKey: HexKey): Flow<MarmotKeyPackageBundle?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeActiveMarmotKeyPackageBundle(publicKey: HexKey): Flow<MarmotKeyPackageBundle?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun encryptAndSendMarmotInnerEvent(
|
||||
marmotInnerEvent: MarmotInnerEvent,
|
||||
nostrSignerSync: NostrSignerSync
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
interface NostrRepository {
|
||||
suspend fun getLocalAccounts(): List<LocalAccount>
|
||||
@@ -209,78 +210,97 @@ interface NostrRepository {
|
||||
suspend fun rescheduleBroadcastNostrEventRequests(broadcastNostrEventRequests: List<BroadcastNostrEventRequest>)
|
||||
|
||||
companion object {
|
||||
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
|
||||
override suspend fun getLocalAccounts(): List<LocalAccount> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
/**
|
||||
* A repository with nothing behind it: every question answers "nothing here",
|
||||
* every write goes nowhere, and **no member throws**.
|
||||
*
|
||||
* Previews and test fixtures are the only callers, and both want inertness. It is
|
||||
* written out here because these members were once `TODO()`, and a `TODO()` in a
|
||||
* no-op is not a loud failure -- it is a silent misattribution. Callers reach most
|
||||
* of them from a `viewModelScope.launch(Dispatchers.IO)` that nobody awaits, so the
|
||||
* `NotImplementedError` escapes to the platform handler, is stashed by
|
||||
* kotlinx-coroutines-test's `ExceptionCollector`, and is re-thrown as
|
||||
* `UncaughtExceptionsBeforeTest` inside *the next* `runTest` -- under that test's
|
||||
* name, with the real culprit buried as a suppressed cause. One `TODO()` here
|
||||
* (`queueNegentropySynchronizeRequest`, reached from
|
||||
* `ChatMessageListViewModel.scheduleSynchronization`) failed `NavigationRoutingTest`
|
||||
* four times a run for a call that test never made.
|
||||
*
|
||||
* So the rules for a member added here, which the other three no-op repositories
|
||||
* follow too:
|
||||
*
|
||||
* - **Answer the call, never stub it out.** `NoOpRepositoryTotalityJvmTest` reads
|
||||
* these four objects and fails if an unimplemented stub reappears in one.
|
||||
* - **A flow emits its empty value once** -- `flowOf(null)`, `flowOf(emptyList())` --
|
||||
* rather than `flow { }`. A screen collecting an empty flow waits in Loading
|
||||
* forever; one collecting `flowOf(null)` renders its empty state, which is what
|
||||
* a preview is for.
|
||||
* - **A callback is not invoked.** Nothing happened, and calling `onCompletion` or
|
||||
* `onSuccess` would say it did -- letting a test pass on the fixture rather than
|
||||
* on the code under test.
|
||||
* - **Throwing is never the way to be loud.** A member that a test must not call is
|
||||
* the test's assertion to make, not the fixture's: override it with a recorder and
|
||||
* assert it stayed untouched. An exception thrown here lands on whichever test
|
||||
* runs next.
|
||||
*
|
||||
* A test that needs a real answer from one member overrides just that one:
|
||||
* `object : NostrRepository by NO_OP_NOSTR_REPOSITORY { … }`.
|
||||
*/
|
||||
val NO_OP_NOSTR_REPOSITORY: NostrRepository = object : NostrRepository {
|
||||
override suspend fun getLocalAccounts(): List<LocalAccount> = emptyList()
|
||||
|
||||
override suspend fun observeLocalAccount(publicKey: HexKey): Flow<LocalAccount?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeLocalAccount(publicKey: HexKey): Flow<LocalAccount?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile? = null
|
||||
|
||||
override suspend fun observeProfileWithPublicKey(publicKey: HexKey): Flow<Profile?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeProfileWithPublicKey(publicKey: HexKey): Flow<Profile?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun observeRelation(
|
||||
publicKey: HexKey,
|
||||
activePublicKey: HexKey
|
||||
): Flow<List<Connection>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
): Flow<List<Connection>> = flowOf(emptyList())
|
||||
|
||||
override suspend fun deleteConnection(connection: Connection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun deleteConnection(connection: Connection) = Unit
|
||||
|
||||
override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<List<UnsignedNostrEvent>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<List<UnsignedNostrEvent>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun observePendingBroadcastNostrEventRequests(): Flow<LocalBroadcastNostrEventRequest> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observePendingBroadcastNostrEventRequests(): Flow<LocalBroadcastNostrEventRequest?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun reopenInbox(activeKeyPair: KeyPair): Int = 0
|
||||
|
||||
override suspend fun observePendingSynchronizeNostrEventRequests(ownerPublicKey: HexKey): Flow<SynchronizeNostrEventRequest?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observePendingSynchronizeNostrEventRequests(ownerPublicKey: HexKey): Flow<SynchronizeNostrEventRequest?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun observePendingNegentropySynchronizeRequests(ownerPublicKey: HexKey): Flow<NegentropySynchronizeRequest?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observePendingNegentropySynchronizeRequests(ownerPublicKey: HexKey): Flow<NegentropySynchronizeRequest?> =
|
||||
flowOf(null)
|
||||
|
||||
// onCompletion is deliberately not called: no profile was created, and a
|
||||
// fixture that reports completion is a fixture a test can pass against.
|
||||
override suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String?,
|
||||
biography: String?,
|
||||
onCompletion: suspend () -> Unit
|
||||
) {
|
||||
TODO("")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun signInToProfile(publicKey: HexKey) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun signInToProfile(publicKey: HexKey) = Unit
|
||||
|
||||
override suspend fun setUpProfileForExistingKey(
|
||||
publicKey: HexKey,
|
||||
placeholderUnsignedNostrEventId: Long,
|
||||
name: String?,
|
||||
biography: String?,
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun forgetLocalAccount(publicKey: HexKey) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun forgetLocalAccount(publicKey: HexKey) = Unit
|
||||
|
||||
// Neither onSuccess nor onError: the note was not written, and it did not fail
|
||||
// to be written either -- there was nowhere to write it. A test that wants
|
||||
// either branch overrides this member and calls the one it is testing.
|
||||
override suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
mentionedPublicKeys: List<HexKey>,
|
||||
@@ -289,74 +309,50 @@ interface NostrRepository {
|
||||
quotedNostrEvent: LocalNostrEvent?,
|
||||
onError: () -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? {
|
||||
return null
|
||||
}
|
||||
override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? =
|
||||
null
|
||||
|
||||
override suspend fun saveUnsignedNostrEvents(unsignedNostrEvents: List<UnsignedNostrEvent>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun saveUnsignedNostrEvents(unsignedNostrEvents: List<UnsignedNostrEvent>) = Unit
|
||||
|
||||
override suspend fun wipeDatabase() {
|
||||
}
|
||||
override suspend fun wipeDatabase() = Unit
|
||||
|
||||
override suspend fun publishNostrEvent(
|
||||
unsignedNostrEvent: UnsignedNostrEvent,
|
||||
nostrEvent: NostrEvent,
|
||||
relayURLs: List<String>,
|
||||
activeKeyPair: KeyPair
|
||||
) {
|
||||
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun broadcastProcessed(
|
||||
broadcastNostrEventRequest: BroadcastNostrEventRequest,
|
||||
status: String
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun requeueStaleBroadcastNostrEventRequests(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun requeueStaleBroadcastNostrEventRequests(): Int = 0
|
||||
|
||||
override suspend fun synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) = Unit
|
||||
|
||||
override suspend fun synchronizeNostrEventRequestCompleted(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun synchronizeNostrEventRequestCompleted(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) = Unit
|
||||
|
||||
override suspend fun negentropySynchronizeRequestProcessed(negentropySynchronizeRequest: NegentropySynchronizeRequest) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun negentropySynchronizeRequestProcessed(negentropySynchronizeRequest: NegentropySynchronizeRequest) = Unit
|
||||
|
||||
override suspend fun saveBroadcastReceipt(broadcastNostrEventReceipt: BroadcastNostrEventReceipt) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun saveBroadcastReceipt(broadcastNostrEventReceipt: BroadcastNostrEventReceipt) = Unit
|
||||
|
||||
override suspend fun saveNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>,
|
||||
activeKeyPair: KeyPair
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun saveNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
negentropySynchronizeRequest: NegentropySynchronizeRequest,
|
||||
synchronizationRelayURLs: List<String>,
|
||||
activeKeyPair: KeyPair
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun saveNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
@@ -364,82 +360,54 @@ interface NostrRepository {
|
||||
synchronizationRelayURLs: List<String>,
|
||||
level: Int,
|
||||
activeKeyPair: KeyPair
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun queueSynchronizeNostrEvent(synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun queueSynchronizeNostrEvent(synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>) = Unit
|
||||
|
||||
override suspend fun queueNegentropySynchronizeRequest(negentropySynchronizeRequests: List<NegentropySynchronizeRequest>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun queueNegentropySynchronizeRequest(negentropySynchronizeRequests: List<NegentropySynchronizeRequest>) = Unit
|
||||
|
||||
override suspend fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String): Flow<Int> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String): Flow<Int> =
|
||||
flowOf(0)
|
||||
|
||||
override suspend fun getNostrEvent(
|
||||
publicKey: HexKey,
|
||||
kind: Kind
|
||||
): NostrEvent? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
): NostrEvent? = null
|
||||
|
||||
override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> = flowOf(emptyList())
|
||||
|
||||
override suspend fun observeNostrFeed(synchronizationFilter: SynchronizationFilter): Flow<List<LocalNostrEvent>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeNostrFeed(synchronizationFilter: SynchronizationFilter): Flow<List<LocalNostrEvent>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun getNegentropicNostrFeedIds(
|
||||
synchronizationFilters: Array<SynchronizationFilter>,
|
||||
applyLimits: Boolean
|
||||
): List<NostrEvent> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
): List<NostrEvent> = emptyList()
|
||||
|
||||
override suspend fun observeLocalNostrEventById(nostrEventId: String): Flow<LocalNostrEvent?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeLocalNostrEventById(nostrEventId: String): Flow<LocalNostrEvent?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun searchableProfiles(excludingPublicKeys: Array<HexKey>): List<Profile> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun searchableProfiles(excludingPublicKeys: Array<HexKey>): List<Profile> =
|
||||
emptyList()
|
||||
|
||||
override suspend fun filterProfiles(searchTerm: String): List<LocalProfile> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun filterProfiles(searchTerm: String): List<LocalProfile> = emptyList()
|
||||
|
||||
override suspend fun observeProfileWithFollowers(publicKey: String): Flow<LocalProfileWithFollowers?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeProfileWithFollowers(publicKey: String): Flow<LocalProfileWithFollowers?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun observeProfileWithFollowing(publicKey: String): Flow<LocalProfileWithFollowing?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun observeProfileWithFollowing(publicKey: String): Flow<LocalProfileWithFollowing?> =
|
||||
flowOf(null)
|
||||
|
||||
override suspend fun getRecentSearches(): List<RecentSearch> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun getRecentSearches(): List<RecentSearch> = emptyList()
|
||||
|
||||
override suspend fun removeRecentSearch(recentSearch: RecentSearch) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun removeRecentSearch(recentSearch: RecentSearch) = Unit
|
||||
|
||||
override suspend fun saveSearchQuery(
|
||||
query: String,
|
||||
synchronizationFilterArray: press.mantra.compose.database.model.typealiases.SynchronizationFilterArray?
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
) = Unit
|
||||
|
||||
override suspend fun rescheduleBroadcastNostrEventRequests(broadcastNostrEventRequests: List<BroadcastNostrEventRequest>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override suspend fun rescheduleBroadcastNostrEventRequests(broadcastNostrEventRequests: List<BroadcastNostrEventRequest>) = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,15 @@ interface SearchRepository {
|
||||
)
|
||||
|
||||
companion object {
|
||||
val NO_OP_SEARCH_REPOSITORY = object: SearchRepository {
|
||||
override suspend fun submitSearch(synchronizeNostrEventRequests: List<press.mantra.compose.database.model.SynchronizeNostrEventRequest>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
/**
|
||||
* A search that reaches no relay: inert, and the one member does not throw. See
|
||||
* [NostrRepository.NO_OP_NOSTR_REPOSITORY] for why a `TODO()` here is a silent
|
||||
* misattribution rather than a loud failure.
|
||||
*/
|
||||
val NO_OP_SEARCH_REPOSITORY: SearchRepository = object : SearchRepository {
|
||||
override suspend fun submitSearch(
|
||||
synchronizeNostrEventRequests: List<press.mantra.compose.database.model.SynchronizeNostrEventRequest>
|
||||
) = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package press.mantra.compose.repository
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Every member of every `NO_OP_*` repository answers its call. None is left stubbed out.
|
||||
*
|
||||
* Every one in `commonMain/.../repository/`, found by reading the directory rather than by
|
||||
* naming files -- see [declaration].
|
||||
*
|
||||
* The rule is worth a test of its own because breaking it does not fail where it is broken.
|
||||
* A `TODO()` in one of these objects throws `NotImplementedError`, and the callers are view
|
||||
* models that reach their repository from `viewModelScope.launch(Dispatchers.IO)` -- a
|
||||
* coroutine nobody awaits. The error escapes to the platform handler, where
|
||||
* kotlinx-coroutines-test's `ExceptionCollector` stashes it (`unprocessedExceptions`, because
|
||||
* a `runBlocking` test has no `TestScope` registered to take it), and the *next* `runTest` to
|
||||
* start drains the stash and rethrows it as `UncaughtExceptionsBeforeTest` -- under that
|
||||
* test's name, for a call it never made. The real stack trace survives only as a suppressed
|
||||
* cause several frames down.
|
||||
*
|
||||
* That cost a debugging session once: `queueNegentropySynchronizeRequest` was a `TODO()`,
|
||||
* `CeremonyQueueRoutingJvmTest` reached it through `ChatMessageListViewModel.initiate()`, and
|
||||
* `NavigationRoutingTest > an account already on the device does not stay on the loading
|
||||
* screen` failed four times a run while the test that caused it passed. Seventeen test files
|
||||
* build on `NO_OP_NOSTR_REPOSITORY` alone, so one stub is never one test's problem.
|
||||
*
|
||||
* So this is checked at the only altitude where it can be checked cheaply and totally: the
|
||||
* source. Calling all sixty-odd members for real would mean a fixture for every parameter
|
||||
* type, and a test that fails because a fixture was wrong tells you less than this one does.
|
||||
* A reflective sweep has the same problem -- it cannot tell a body that threw from an argument
|
||||
* it could not build.
|
||||
*
|
||||
* It fails under its own name, which is the whole point.
|
||||
*/
|
||||
class NoOpRepositoryTotalityJvmTest {
|
||||
|
||||
/**
|
||||
* Every file that declares one, found rather than listed.
|
||||
*
|
||||
* A list would be wrong the first time a ninth repository is added -- and was: the
|
||||
* first version of this test named six files when the tree held eight, having missed
|
||||
* `MantraRepository` and `CachingImportRepository` (both total, which is luck rather
|
||||
* than cover). Discovery is the point of the test, so it discovers.
|
||||
*/
|
||||
private val declaration = Regex("""\bval\s+NO_OP_[A-Z0-9_]+""")
|
||||
|
||||
/**
|
||||
* A floor, in the spirit of `docs/scripts/m3-audit.sh`: the way this test breaks
|
||||
* silently is by finding nothing to check -- a moved package, a renamed convention --
|
||||
* and then passing. Eight is the count on 2026-10-02. Raise it when one is added.
|
||||
*/
|
||||
private val knownSingletons = 8
|
||||
|
||||
@Test
|
||||
fun `no no-op repository leaves a member unimplemented`() {
|
||||
val directory = repositoryDirectory()
|
||||
|
||||
val sources = (directory.listFiles() ?: emptyArray())
|
||||
.filter { it.isFile && it.name.endsWith(".kt") }
|
||||
.map { it.name to it.readText() }
|
||||
.filter { (_, source) -> declaration.containsMatchIn(source) }
|
||||
.sortedBy { (name, _) -> name }
|
||||
|
||||
assertTrue(
|
||||
sources.size >= knownSingletons,
|
||||
"Found ${sources.size} no-op repositories in ${directory.path}, expected at " +
|
||||
"least $knownSingletons. Either one was removed -- lower the floor in the " +
|
||||
"same change -- or this test has stopped finding them and is passing on " +
|
||||
"nothing. Found: ${sources.map { it.first }}"
|
||||
)
|
||||
|
||||
val offences = sources.flatMap { (name, source) -> stubbedMembers(name, source) }
|
||||
|
||||
assertEquals(
|
||||
emptyList(),
|
||||
offences,
|
||||
"A no-op repository member is stubbed out. Give it the answer a repository with " +
|
||||
"nothing behind it would give -- null, an empty collection, flowOf(...) of " +
|
||||
"one of those, Unit, 0, false -- and never TODO(). See the contract on " +
|
||||
"NostrRepository.NO_OP_NOSTR_REPOSITORY. Stubs found:\n" +
|
||||
offences.joinToString("\n") { " $it" }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* `file:line member` for every `TODO(` in code, comments excluded -- the contract on
|
||||
* `NO_OP_NOSTR_REPOSITORY` names `TODO()` in its prose, and a doc comment explaining the
|
||||
* rule must not trip the check that enforces it.
|
||||
*/
|
||||
private fun stubbedMembers(name: String, source: String): List<String> {
|
||||
val found = mutableListOf<String>()
|
||||
var inBlockComment = false
|
||||
var member = "(before the first declaration)"
|
||||
|
||||
source.lines().forEachIndexed { index, raw ->
|
||||
var line = raw
|
||||
if (inBlockComment) {
|
||||
val end = line.indexOf("*/")
|
||||
if (end < 0) return@forEachIndexed
|
||||
line = line.substring(end + 2)
|
||||
inBlockComment = false
|
||||
}
|
||||
val blockStart = line.indexOf("/*")
|
||||
if (blockStart >= 0) {
|
||||
inBlockComment = !line.substring(blockStart).contains("*/")
|
||||
line = line.substring(0, blockStart)
|
||||
}
|
||||
val lineComment = line.indexOf("//")
|
||||
if (lineComment >= 0) line = line.substring(0, lineComment)
|
||||
|
||||
if (line.contains("fun ")) member = line.trim()
|
||||
if (line.contains("TODO(")) found += "$name:${index + 1} $member"
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* Found by walking up from the working directory rather than taken from it, because the
|
||||
* one is the `composeApp` project directory under `:composeApp:jvmTest` and the other is
|
||||
* the repository root when the same test is run from the root project. A test about
|
||||
* things that fail silently does not get to pass because it could not find its input.
|
||||
*/
|
||||
private fun repositoryDirectory(): File {
|
||||
val relative = "composeApp/src/commonMain/kotlin/press/mantra/compose/repository"
|
||||
var directory: File? = File("").absoluteFile
|
||||
while (directory != null) {
|
||||
val candidate = File(directory, relative)
|
||||
if (candidate.isDirectory) return candidate
|
||||
directory = directory.parentFile
|
||||
}
|
||||
throw AssertionError(
|
||||
"Could not find $relative above ${File("").absolutePath} -- this test read nothing."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.delay
|
||||
import press.mantra.compose.database.model.ChatRoom
|
||||
import press.mantra.compose.database.model.DkgSession
|
||||
import press.mantra.compose.database.model.NegentropySynchronizeRequest
|
||||
import press.mantra.compose.database.model.Participant
|
||||
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
|
||||
import press.mantra.compose.database.model.intermdiate.LocalParticipant
|
||||
@@ -152,23 +151,16 @@ class CeremonyQueueRoutingJvmTest {
|
||||
val viewModel = ChatMessageListViewModel(
|
||||
initialChatMessageListUIState = ChatMessageListUIState.Loading,
|
||||
localChatRoom = room(),
|
||||
// Not the bare no-op. `initiate` calls `scheduleSynchronization` on
|
||||
// its second line, which launches on `Dispatchers.IO` and -- because
|
||||
// The bare no-op is safe again. `initiate` calls `scheduleSynchronization`
|
||||
// on its second line, which launches on `Dispatchers.IO` and -- because
|
||||
// `room()` gives every participant a null profile, so there is always
|
||||
// somebody it cannot name -- reaches
|
||||
// `queueNegentropySynchronizeRequest`. That member is one of the
|
||||
// forty-one `TODO("Not yet implemented")` bodies on
|
||||
// NO_OP_NOSTR_REPOSITORY, so it throws in a coroutine nobody awaits.
|
||||
//
|
||||
// Nothing catches it, so it reaches the default handler, and the next
|
||||
// `runTest` in the same JVM reports it as `UncaughtExceptionsBeforeTest`
|
||||
// -- against its own name. That is how this test failed
|
||||
// `NavigationRoutingTest` four times a run while passing itself.
|
||||
nostrRepository = object : NostrRepository by NostrRepository.NO_OP_NOSTR_REPOSITORY {
|
||||
override suspend fun queueNegentropySynchronizeRequest(
|
||||
negentropySynchronizeRequests: List<NegentropySynchronizeRequest>
|
||||
) = Unit
|
||||
},
|
||||
// somebody it cannot name -- reaches `queueNegentropySynchronizeRequest`.
|
||||
// That member used to be one of forty-one `TODO()` bodies on
|
||||
// NO_OP_NOSTR_REPOSITORY and threw into whichever `runTest` ran next,
|
||||
// which is how this test failed `NavigationRoutingTest` four times a run
|
||||
// while passing itself. The override that stood here is gone because the
|
||||
// no-op is total now; `NoOpRepositoryTotalityJvmTest` keeps it that way.
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
frostSigningRepository = FrostSigningRepository.NO_OP_FROST_SIGNING_REPOSITORY,
|
||||
dkgRepository = object : DkgRepository by DkgRepository.NO_OP_DKG_REPOSITORY {
|
||||
|
||||
Reference in New Issue
Block a user