The feature's logic was tested from the events down to the chronicle and not at
all above the repository. Four gaps, each one something no other layer would
catch.
**AddProjectViewModel.** A project and its contents go out as one batch whose
memberships carry the project's id -- which is a hash over the group's key at
the room's path and does not exist until the proposal is made, so the screen
hands over a function of the signed lead. Nothing downstream notices if that
function returns the wrong thing: a membership naming another project is a
valid event, signs cleanly, and lands as a row pointing elsewhere. Eight cases
assert the batch's shape against a recording repository that signs the lead the
way a finished session does, including that the creator is the member filling
the form in -- there is no field for it and nothing else to check it against.
**AddToProjectViewModel.** The candidate filtering is the only thing stopping a
duplicate membership, because a duplicate is a valid event with its own id: the
keys are satisfied, the signature is good, the row gets written. A wrong filter
would spend a quorum saying what the group has already said and look like the
feature working. Ten cases, including that the two halves filter independently.
**FrostSigningViewModel's resolver.** Unresolved, the signing screen asks a
member to agree to "a4f1be55d0c3". Seven cases through the same `summarize`
call the screen makes: a membership reads as the artifact and label, a stranger
falls back to a hash rather than an invented name, the room owning its own work
says nothing, and a project being signed in the same batch resolves to nothing
-- which is why a membership says the version first.
**getProjectsByTranslationArtifactVersionId** had no test at any layer while its
artifact-side twin had one from the start. That asymmetry is what a copied join
hides.
Two notes for whoever writes the next view-model test. These view models hold
state in `mutableStateOf` and initiate on `Dispatchers.IO`, so there is nothing
to collect and the call returns before the work is done -- hence `awaitValue`,
polling in real time for the same reason `NavigationIdentityRoutingJvmTest`
does. And `NostrRepository.NO_OP_NOSTR_REPOSITORY.getProfileWithPublicKey` is a
`TODO()`, so a resolver that asks it about anybody throws on a background
dispatcher and surfaces as a timeout rather than a failure anybody can read;
these read profiles from the seeded database instead, which also gives the
honest answer for a stranger.
983 jvmTest, 447 testDebugUnitTest, no failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>