feat(subgroups): read a subgroup out of the thing it is for
A dialect, an artifact, a translation and a project already carry the three facts a subgroup needs -- the room they live in, a name, and an id -- so `SubgroupOrigin.of` reads them and hands back a candidate. That is the whole of the convenience, and it is deliberately only that: no ceremony is opened, no refusal is re-decided, and the admins and the threshold are still settled in the picker and nowhere else. `SubgroupOriginEntity` is an interface rather than four overloads so that "which things can a subgroup be made for" is a question the compiler answers. A fifth entity gains the capability by implementing it. The three fields are the ones already on the rows; the only new one is the kind, as a computed property so Room never sees it -- a kind is a fact about the type, and a stored copy could disagree with the event the row came from. The kind is carried because an id alone would have to be looked up in four tables to be understood, and a reader without it cannot tell "a project this device does not hold" from "not a project at all". `Ref` holds both or neither for the same reason. Two things the tests are really for. The name comes back bare, because `MarmotGroupName.of` adds the `#` where the name is used -- into the certificate the parent signs and onto the room -- and a third place that has to agree is a third place that can disagree, most easily with the field the user is typing in. And a blank name stays blank: the picker already refuses to confirm without one, and inventing a name here would route around a refusal whose job is to be hit. Phase 1 of docs/subgroups-for-entities.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,13 @@ import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.PrimaryKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import press.mantra.compose.database.model.traits.OptionalNostrEventEntity
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.database.model.traits.TimestampedEntity
|
||||
import press.mantra.compose.nostr.nip30303.ArtifactEvent
|
||||
import press.mantra.compose.nostr.nip30303.tags.ArtifactVersionMetadataTag
|
||||
@@ -40,16 +42,16 @@ import kotlin.time.Instant
|
||||
)
|
||||
data class MantraArtifact(
|
||||
@PrimaryKey
|
||||
val id: HexKey,
|
||||
override val id: HexKey,
|
||||
val publicKey: String,
|
||||
val name: String,
|
||||
override val name: String,
|
||||
val url: String, // Hash this to get the dTag...
|
||||
val visibility: String, // public, private, permissioned (public, but only members can contribute)
|
||||
val dialectId: String,
|
||||
val license: String,
|
||||
// val artifactApprovalId: String,
|
||||
|
||||
val chatRoomId: String,
|
||||
override val chatRoomId: String,
|
||||
|
||||
/**
|
||||
* Signature will be an empty string if this is a rumor
|
||||
@@ -68,7 +70,16 @@ data class MantraArtifact(
|
||||
override val nostrEventId: HexKey? = null,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt
|
||||
): OptionalNostrEventEntity, TimestampedEntity {
|
||||
): OptionalNostrEventEntity, SubgroupOriginEntity, TimestampedEntity {
|
||||
|
||||
/**
|
||||
* Which kind a reader holding only this row's id is looking at.
|
||||
*
|
||||
* A computed property, so it is not a column and Room never sees it --
|
||||
* the kind is a fact about the type rather than about the row, and a
|
||||
* stored copy could disagree with the event this row came from.
|
||||
*/
|
||||
override val subgroupOriginKind: Kind get() = ArtifactEvent.KIND
|
||||
/**
|
||||
* The event the group signed, rebuilt from this row.
|
||||
*
|
||||
|
||||
@@ -4,11 +4,13 @@ import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.PrimaryKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import press.mantra.compose.database.model.traits.OptionalNostrEventEntity
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.database.model.traits.TimestampedEntity
|
||||
import press.mantra.compose.nostr.nip30303.DialectEvent
|
||||
import press.mantra.compose.nostr.nip30303.tags.CountryTag
|
||||
@@ -32,15 +34,15 @@ import kotlin.time.Instant
|
||||
)
|
||||
data class MantraDialect(
|
||||
@PrimaryKey
|
||||
val id: HexKey,
|
||||
override val id: HexKey,
|
||||
val publicKey: HexKey,
|
||||
val name: String,
|
||||
override val name: String,
|
||||
val country: String,
|
||||
val language: String,
|
||||
|
||||
val signature: String,
|
||||
|
||||
val chatRoomId: String,
|
||||
override val chatRoomId: String,
|
||||
|
||||
val relayHint: String? = null,
|
||||
val marmotGroupEventId: HexKey? = null,
|
||||
@@ -54,7 +56,16 @@ data class MantraDialect(
|
||||
override val nostrEventId: HexKey? = null,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt
|
||||
): OptionalNostrEventEntity, TimestampedEntity {
|
||||
): OptionalNostrEventEntity, SubgroupOriginEntity, TimestampedEntity {
|
||||
|
||||
/**
|
||||
* Which kind a reader holding only this row's id is looking at.
|
||||
*
|
||||
* A computed property, so it is not a column and Room never sees it --
|
||||
* the kind is a fact about the type rather than about the row, and a
|
||||
* stored copy could disagree with the event this row came from.
|
||||
*/
|
||||
override val subgroupOriginKind: Kind get() = DialectEvent.KIND
|
||||
|
||||
@Deprecated(
|
||||
"Chronicle fallback for work signed before the GroupSignedEvent table. Read the " +
|
||||
|
||||
@@ -5,9 +5,11 @@ import androidx.room3.ForeignKey
|
||||
import androidx.room3.Index
|
||||
import androidx.room3.PrimaryKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import press.mantra.compose.database.model.traits.OptionalNostrEventEntity
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.database.model.traits.TimestampedEntity
|
||||
import press.mantra.compose.nostr.nip30303.ProjectEvent
|
||||
import kotlin.time.Clock
|
||||
@@ -39,12 +41,12 @@ import kotlin.time.Instant
|
||||
)
|
||||
data class MantraProject(
|
||||
@PrimaryKey
|
||||
val id: HexKey,
|
||||
override val id: HexKey,
|
||||
|
||||
/** The room's own key on a group-signed row, which is every project. */
|
||||
val publicKey: HexKey,
|
||||
|
||||
val name: String,
|
||||
override val name: String,
|
||||
val description: String,
|
||||
|
||||
/**
|
||||
@@ -73,7 +75,7 @@ data class MantraProject(
|
||||
*/
|
||||
val ownerPublicKeys: String,
|
||||
|
||||
val chatRoomId: String,
|
||||
override val chatRoomId: String,
|
||||
|
||||
/** Signature will be an empty string if this is a rumor */
|
||||
val signature: String,
|
||||
@@ -89,7 +91,16 @@ data class MantraProject(
|
||||
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt
|
||||
): OptionalNostrEventEntity, TimestampedEntity {
|
||||
): OptionalNostrEventEntity, SubgroupOriginEntity, TimestampedEntity {
|
||||
|
||||
/**
|
||||
* Which kind a reader holding only this row's id is looking at.
|
||||
*
|
||||
* A computed property, so it is not a column and Room never sees it --
|
||||
* the kind is a fact about the type rather than about the row, and a
|
||||
* stored copy could disagree with the event this row came from.
|
||||
*/
|
||||
override val subgroupOriginKind: Kind get() = ProjectEvent.KIND
|
||||
|
||||
/** The owners as the set they are. Empty only on a row written around this. */
|
||||
fun ownerPublicKeySet(): Set<HexKey> = ownerPublicKeys
|
||||
|
||||
@@ -4,11 +4,13 @@ import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.PrimaryKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import press.mantra.compose.database.model.traits.OptionalNostrEventEntity
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.database.model.traits.TimestampedEntity
|
||||
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
|
||||
import press.mantra.compose.nostr.nip30303.tags.ArtifactVersionIdTag
|
||||
@@ -46,11 +48,11 @@ import kotlin.time.Instant
|
||||
)
|
||||
data class MantraTranslationArtifactVersion(
|
||||
@PrimaryKey
|
||||
val id: HexKey,
|
||||
override val id: HexKey,
|
||||
val publicKey: HexKey,
|
||||
val artifactVersionId: HexKey,
|
||||
val dialectId: HexKey,
|
||||
val name: String,
|
||||
override val name: String,
|
||||
val visibility: String,
|
||||
val license: String,
|
||||
|
||||
@@ -59,7 +61,7 @@ data class MantraTranslationArtifactVersion(
|
||||
*/
|
||||
val signature: String,
|
||||
|
||||
val chatRoomId: String,
|
||||
override val chatRoomId: String,
|
||||
|
||||
val relayHint: String? = null,
|
||||
val marmotGroupEventId: HexKey? = null,
|
||||
@@ -73,7 +75,16 @@ data class MantraTranslationArtifactVersion(
|
||||
override val nostrEventId: HexKey? = null,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt
|
||||
): OptionalNostrEventEntity, TimestampedEntity {
|
||||
): OptionalNostrEventEntity, SubgroupOriginEntity, TimestampedEntity {
|
||||
|
||||
/**
|
||||
* Which kind a reader holding only this row's id is looking at.
|
||||
*
|
||||
* A computed property, so it is not a column and Room never sees it --
|
||||
* the kind is a fact about the type rather than about the row, and a
|
||||
* stored copy could disagree with the event this row came from.
|
||||
*/
|
||||
override val subgroupOriginKind: Kind get() = TranslationArtifactVersionEvent.KIND
|
||||
|
||||
@Deprecated(
|
||||
"Chronicle fallback for work signed before the GroupSignedEvent table. Read the " +
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package press.mantra.compose.database.model.traits
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
|
||||
/**
|
||||
* A thing a group can decide deserves a room of its own.
|
||||
*
|
||||
* Four rows in four tables -- a dialect, an artifact, a translation, a project --
|
||||
* already carry the same three fields, and a subgroup made for any of them needs
|
||||
* exactly those three: the room it lives in becomes the parent, its name becomes
|
||||
* the proposed name, and its id and kind become what the parent's quorum
|
||||
* certifies the new room is *for*.
|
||||
*
|
||||
* It is an interface rather than four overloads of `SubgroupOrigin.of` so that
|
||||
* "which things can a subgroup be made for" is a question the compiler answers.
|
||||
* A fifth entity gains the capability by implementing this, and the picker, the
|
||||
* certificate and both directions of the UI pick it up without being told.
|
||||
*
|
||||
* ### Why `name` is here and is not an identity
|
||||
*
|
||||
* [name] is for naming a room and nothing else. It is deliberately not treated
|
||||
* as identifying the entity anywhere: a dialect's identity is its
|
||||
* `(language, country)` pair, since nothing in the schema makes a dialect unique
|
||||
* -- the id is a content hash including `createdAt`, there is no index, and the
|
||||
* rows are quorum-signed so they cannot be rewritten. Two rooms that typed
|
||||
* "Southern Sotho" and "Sesotho" hold the same dialect under two names. So the
|
||||
* link a subgroup carries is [id] and [subgroupOriginKind], and the name is only
|
||||
* ever what the room is called.
|
||||
*
|
||||
* ### Why the kind is on the row
|
||||
*
|
||||
* A reader holding an id alone would have to try four tables to find out what it
|
||||
* is looking at, and could not tell "a project this device does not hold" from
|
||||
* "not a project at all". Every one of these rows already knows its own event
|
||||
* kind -- most of them build one in `toXEvent()` -- so stating it costs a line
|
||||
* and saves the lookup. See `SubgroupOrigin`.
|
||||
*/
|
||||
interface SubgroupOriginEntity {
|
||||
/** The entity's own id: a nostr event id, content-hashed. */
|
||||
val id: HexKey
|
||||
|
||||
/** What to call the room. Never read as an identity -- see the class comment. */
|
||||
val name: String
|
||||
|
||||
/** The room this entity lives in, which is the room that would be the parent. */
|
||||
val chatRoomId: String
|
||||
|
||||
/** The nip30303 kind of the event this row is, for a reader holding only the id. */
|
||||
val subgroupOriginKind: Kind
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package press.mantra.compose.managers
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
|
||||
/**
|
||||
* What a subgroup was made for, and everything a picker needs to start one.
|
||||
*
|
||||
* Making a room for a dialect, an artifact, a translation or a project takes
|
||||
* three facts, and all three are already sitting on the row the user was looking
|
||||
* at when they asked: the room it lives in is the parent, its name is the name to
|
||||
* propose, and its id and kind are what the parent's quorum certifies the new
|
||||
* room is for. [of] reads them, and that is the whole of the convenience.
|
||||
*
|
||||
* Pure, and parallel to `TranslationScaffold` and `ChapterScaffolding` -- no
|
||||
* database, no Compose, no repository. Nothing here opens a ceremony: the picker
|
||||
* still does that through `DkgRepository.proposeRitual`, and every refusal is
|
||||
* still `SubgroupManager.refuseSubgroup`'s. What this removes is a user retyping
|
||||
* a name the app already knew and a room ending up related to its entity by
|
||||
* having been named after it.
|
||||
*
|
||||
* See `docs/subgroups-for-entities.md`.
|
||||
*/
|
||||
object SubgroupOrigin {
|
||||
/**
|
||||
* The entity a subgroup is for: a kind and an id, together or not at all.
|
||||
*
|
||||
* There is no constructor that holds one of the two, which is the point of
|
||||
* the type. An id with no kind is unresolvable without trying four tables; a
|
||||
* kind with no id names a category rather than a thing. `SubgroupOriginTag`
|
||||
* refuses to parse half of this for the same reason.
|
||||
*
|
||||
* Holding one is not evidence of anything. The parent's quorum signs that it
|
||||
* *intends* a room to be for this entity; it does not vouch that the entity
|
||||
* exists, and a device that holds the certificate routinely holds none of the
|
||||
* parent's rows -- a subgroup's members need never have been in the parent.
|
||||
*/
|
||||
data class Ref(
|
||||
val kind: Kind,
|
||||
val id: HexKey,
|
||||
)
|
||||
|
||||
/**
|
||||
* Everything the subgroup picker needs, read off [entity].
|
||||
*
|
||||
* [Candidate.name] is bare -- no `#`. The convention is applied by
|
||||
* `MarmotGroupName.of` where the name is actually used, twice and
|
||||
* idempotently: once into the birth certificate the parent's quorum signs and
|
||||
* once onto the room itself. Normalising here as well would be a third place
|
||||
* that has to agree, and the one it would most easily disagree with is the
|
||||
* name field the user is still typing in.
|
||||
*
|
||||
* A blank name comes back blank rather than becoming "a group" or the
|
||||
* entity's id. The picker already refuses to confirm without a name -- "Give
|
||||
* the subgroup a name." -- and inventing one here would route around a
|
||||
* refusal whose whole job is to be hit.
|
||||
*/
|
||||
fun of(entity: SubgroupOriginEntity): Candidate = Candidate(
|
||||
parentChatRoomId = entity.chatRoomId,
|
||||
name = entity.name.trim(),
|
||||
origin = Ref(kind = entity.subgroupOriginKind, id = entity.id),
|
||||
)
|
||||
|
||||
/**
|
||||
* A subgroup that could be made, before anybody has picked who runs it.
|
||||
*
|
||||
* Deliberately not a proposal and not a session. Who the admins are and what
|
||||
* the threshold is are settled in the picker and nowhere else -- ChillDKG
|
||||
* hashes the threshold into the session identity, so there is no later screen
|
||||
* where it could change -- and this type stops short of all of it.
|
||||
*/
|
||||
data class Candidate(
|
||||
val parentChatRoomId: String,
|
||||
val name: String,
|
||||
val origin: Ref,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package press.mantra.compose.managers
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import press.mantra.compose.database.model.MantraArtifact
|
||||
import press.mantra.compose.database.model.MantraDialect
|
||||
import press.mantra.compose.database.model.MantraProject
|
||||
import press.mantra.compose.database.model.MantraTranslationArtifactVersion
|
||||
import press.mantra.compose.database.model.traits.SubgroupOriginEntity
|
||||
import press.mantra.compose.nostr.nip30303.ArtifactEvent
|
||||
import press.mantra.compose.nostr.nip30303.DialectEvent
|
||||
import press.mantra.compose.nostr.nip30303.ProjectEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
|
||||
|
||||
/**
|
||||
* Reading a subgroup out of the thing it is for.
|
||||
*
|
||||
* Four entities, one answer each, and the answer is three fields the row already
|
||||
* carried. What these cases are really guarding is that the three keep meaning
|
||||
* the same thing: the parent is the room the entity lives in and never the
|
||||
* entity's id, the name is bare and un-normalised, and the kind is the entity's
|
||||
* own kind rather than a subgroup kind.
|
||||
*/
|
||||
class SubgroupOriginTest {
|
||||
|
||||
private val room = "r".repeat(64)
|
||||
private val entityId = "e".repeat(64)
|
||||
private val key = "p".repeat(64)
|
||||
|
||||
private fun dialect(name: String = "Sesotho") = MantraDialect(
|
||||
id = entityId,
|
||||
publicKey = key,
|
||||
name = name,
|
||||
country = "Lesotho",
|
||||
language = "st",
|
||||
signature = "",
|
||||
chatRoomId = room,
|
||||
)
|
||||
|
||||
private fun artifact(name: String = "Luke") = MantraArtifact(
|
||||
id = entityId,
|
||||
publicKey = key,
|
||||
name = name,
|
||||
url = "https://example.invalid/luke",
|
||||
visibility = "public",
|
||||
dialectId = "d".repeat(64),
|
||||
license = "CC0",
|
||||
chatRoomId = room,
|
||||
signature = "",
|
||||
)
|
||||
|
||||
private fun translation(name: String = "Setswana Luke") = MantraTranslationArtifactVersion(
|
||||
id = entityId,
|
||||
publicKey = key,
|
||||
artifactVersionId = "v".repeat(64),
|
||||
dialectId = "d".repeat(64),
|
||||
name = name,
|
||||
visibility = "public",
|
||||
license = "CC0",
|
||||
signature = "",
|
||||
chatRoomId = room,
|
||||
)
|
||||
|
||||
private fun project(name: String = "Southern Africa") = MantraProject(
|
||||
id = entityId,
|
||||
publicKey = key,
|
||||
name = name,
|
||||
description = "Four dialects, one gospel.",
|
||||
creatorPublicKey = key,
|
||||
ownerPublicKeys = key,
|
||||
chatRoomId = room,
|
||||
signature = "",
|
||||
)
|
||||
|
||||
private fun assertCandidate(entity: SubgroupOriginEntity, kind: Int, name: String) {
|
||||
val candidate = SubgroupOrigin.of(entity)
|
||||
|
||||
assertEquals(room, candidate.parentChatRoomId)
|
||||
assertEquals(name, candidate.name)
|
||||
assertEquals(SubgroupOrigin.Ref(kind = kind, id = entityId), candidate.origin)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a dialect names its room, its name and kind 30304`() {
|
||||
assertCandidate(dialect(), DialectEvent.KIND, "Sesotho")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an artifact names its room, its name and kind 30300`() {
|
||||
assertCandidate(artifact(), ArtifactEvent.KIND, "Luke")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a translation names its room, its name and kind 30306`() {
|
||||
assertCandidate(translation(), TranslationArtifactVersionEvent.KIND, "Setswana Luke")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a project names its room, its name and kind 30330`() {
|
||||
assertCandidate(project(), ProjectEvent.KIND, "Southern Africa")
|
||||
}
|
||||
|
||||
/**
|
||||
* The four kinds are four, which is the whole reason the ref carries one. An
|
||||
* id alone would have to be looked up in four tables, and this is the case
|
||||
* that fails if somebody gives two entities the same kind by copying a
|
||||
* sibling.
|
||||
*/
|
||||
@Test
|
||||
fun `the four kinds are distinct`() {
|
||||
val kinds = listOf(dialect(), artifact(), translation(), project())
|
||||
.map { SubgroupOrigin.of(it).origin.kind }
|
||||
|
||||
assertEquals(4, kinds.distinct().size, "two entities report the same kind: $kinds")
|
||||
}
|
||||
|
||||
/**
|
||||
* Bare, with no `#`. `MarmotGroupName.of` adds it where the name is used --
|
||||
* into the certificate the parent signs, and onto the room -- and it is
|
||||
* idempotent, so a name normalised here as well would still come out right.
|
||||
* It is kept out anyway because this name goes into a text field the user is
|
||||
* about to type in, and the `#` is drawn beside that field rather than put
|
||||
* inside it.
|
||||
*/
|
||||
@Test
|
||||
fun `the name carries no group prefix`() {
|
||||
assertEquals("Sesotho", SubgroupOrigin.of(dialect()).name)
|
||||
assertEquals("#Sesotho", MarmotGroupName.of(SubgroupOrigin.of(dialect()).name))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `surrounding space is trimmed`() {
|
||||
assertEquals("Sesotho", SubgroupOrigin.of(dialect(" Sesotho ")).name)
|
||||
}
|
||||
|
||||
/**
|
||||
* A blank name stays blank rather than becoming "a group" or the entity's id.
|
||||
* The picker refuses to confirm without one -- "Give the subgroup a name." --
|
||||
* and inventing a name here would route around a refusal whose whole job is
|
||||
* to be hit.
|
||||
*/
|
||||
@Test
|
||||
fun `a blank name is handed back blank, not invented`() {
|
||||
assertEquals("", SubgroupOrigin.of(dialect(" ")).name)
|
||||
assertEquals("", SubgroupOrigin.of(project("")).name)
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent is the room, not the entity. Easy to get backwards -- both are
|
||||
* 64-hex strings on the same row -- and getting it backwards would open a
|
||||
* ceremony in a room that does not exist, which fails late and unhelpfully.
|
||||
*/
|
||||
@Test
|
||||
fun `the parent is the room the entity lives in`() {
|
||||
val candidate = SubgroupOrigin.of(artifact())
|
||||
|
||||
assertEquals(room, candidate.parentChatRoomId)
|
||||
assertEquals(entityId, candidate.origin.id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user