diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 03:32:33 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 03:32:33 +0900 |
| commit | e25bfe0e39771ceac287f93a235aa5954a154236 (patch) | |
| tree | 229bb10c921745cb9d011bdb7a625faf19fb71c1 | |
| parent | 7ed4bbbcf375c4f136a3b90bb6105c278901f654 (diff) | |
| download | LunaticChat-e25bfe0e39771ceac287f93a235aa5954a154236.tar.gz LunaticChat-e25bfe0e39771ceac287f93a235aa5954a154236.tar.bz2 LunaticChat-e25bfe0e39771ceac287f93a235aa5954a154236.zip | |
refactor: drop what no longer carries its weight
- getDirectMessageSpyPlayers had no callers left; handing out a copy of the spy
map is the shape notifySpies was introduced to replace.
- pluginScope was widened to public for "commands that must not block the tick
thread", but commands take the delivery queue and only LunaticChat reads it.
- getPlayerChannels returned a Result that cannot fail, so three callers carried
unreachable error paths and channel.status.error could never be shown.
- The cross-server managers were gated on velocityIntegration.enabled as well as
on a manager that is non-null only when it is enabled, letting the two
conditions disagree.
- LenientBoolean's non-YamlInput fallback was observationally identical to the
cast failing, since both land in ConfigManager's catch-all.
- sendCrossServerMessage caught its own failures underneath the delivery queue,
which already reports them without stopping the sender's later messages. The
second boundary is what forced a CancellationException clause here.
- handleOutgoingCrossServerMessage still recorded the reply target after the
commands took that over, so remote targets were recorded twice - re-inserting
entries clearPlayer had swept, which is the bug the local path was fixed for.
- The reason delivery is queued was written out in both command constructors and
twice more in KDoc; it now lives where the queueing happens.
Spy notification also defers its notice lookup and member set until a spy is
actually online, which is not the normal case.
Co-Authored-By: Claude <noreply@anthropic.com>
17 files changed, 102 insertions, 134 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt index 3f9cb76..85d87cd 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt @@ -41,11 +41,14 @@ class LunaticChat : private lateinit var configuration: LunaticChatConfiguration private lateinit var serviceInitializer: ServiceInitializer - // Read by commands that must not block the tick thread. - lateinit var pluginScope: PluginCoroutineScope - private set + private lateinit var pluginScope: PluginCoroutineScope - /** Serializes each player's outgoing messages so they arrive in the order they were sent. */ + /** + * Serializes each player's outgoing messages so they arrive in the order they were sent. + * + * Commands submit delivery here rather than running it inline: romaji conversion can reach the + * Google IME API, and a command executor runs on the tick thread. + */ lateinit var deliveryQueue: PerPlayerWorkQueue private set private var updateChecker: UpdateChecker? = null diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt index 6ce2672..72a016d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt @@ -176,13 +176,7 @@ class ChannelMembershipManager( // Check if player has reached max membership limit (0 means unlimited) if (config.maxMembershipPerPlayer > 0) { - val playerChannelCount = - getPlayerChannels(playerId) - .getOrElse { - return Result.failure( - ChannelRuntimeException("Failed to get player channels for $playerId", it), - ) - }.size + val playerChannelCount = getPlayerChannels(playerId).size if (playerChannelCount >= config.maxMembershipPerPlayer) { return Result.failure( @@ -333,7 +327,7 @@ class ChannelMembershipManager( * Gets all channels where the player is a member. * * @param playerId The UUID of the player. - * @return Result containing a list of channel IDs where the player is a member. + * @return The channel IDs where the player is a member. */ - fun getPlayerChannels(playerId: UUID): Result<List<String>> = Result.success(channelManager.channelIdsOf(playerId)) + fun getPlayerChannels(playerId: UUID): List<String> = channelManager.channelIdsOf(playerId) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt index 4da0c1a..0155a33 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt @@ -41,10 +41,11 @@ class ChannelMessageHandler( player.playMessageSendNotification() } - // Send to spy players (exclude sender and channel members) - val memberIds = context.members.map { it.playerId }.toSet() + // Send to spy players (exclude sender and channel members). The member set is built lazily + // because notifySpies only consults exclude when a spy is actually online. + val memberIds by lazy { context.members.mapTo(HashSet()) { it.playerId } } SpyPermissionManager.notifySpies( - noticeText = languageManager.getMessage("general.spyMessage"), + noticeText = { languageManager.getMessage("general.spyMessage") }, exclude = { it.uniqueId == playerId || it.uniqueId in memberIds }, ) { formattedMessage } context.members.forEach { member -> diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt index 2033661..7eb077a 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt @@ -156,8 +156,10 @@ class DirectMessageHandler( /** * Handles the sender-side display of an outgoing cross-server direct message. - * Applies romaji conversion, shows the message to the sender, notifies spies, - * and records the remote reply target. + * Applies romaji conversion, shows the message to the sender and notifies spies. + * + * The reply target is recorded by the caller via [recordRemoteRecipient] before the delivery is + * queued, for the same reason as [sendDirectMessage]: /reply reads it on the command thread. * * @return the message body to relay (romaji-converted if applicable), since the * receiving server has no access to the sender's settings. @@ -184,7 +186,6 @@ class DirectMessageHandler( ?.playMessageSendNotification() } - recordRemoteRecipient(sender, targetName, targetServerName) return displayMessage } @@ -230,7 +231,7 @@ class DirectMessageHandler( rawMessage: String, ) { SpyPermissionManager.notifySpies( - noticeText = languageManager.getMessage("general.spyMessage"), + noticeText = { languageManager.getMessage("general.spyMessage") }, exclude = { it.name == senderName || it.name == recipientName }, ) { formatMessage(format, senderName, recipientName, rawMessage, replyTo = senderName) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt index df5b2a9..e999a60 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt @@ -31,9 +31,6 @@ class ReplyCommand( private val dmHandler: DirectMessageHandler, override val languageManager: LanguageManager, private val crossServerDirectMessageManager: CrossServerDirectMessageManager? = null, - // Delivery is queued rather than run inline: romaji conversion can reach the Google IME API, - // and a command executor runs on the tick thread. Queueing per sender keeps their messages in - // the order they typed them. private val deliveryQueue: PerPlayerWorkQueue = plugin.deliveryQueue, ) : LunaticCommand(plugin) { override val description: String diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt index 5e6e2c7..82c1eef 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt @@ -38,9 +38,6 @@ class TellCommand( private val crossServerDirectMessageManager: CrossServerDirectMessageManager? = null, private val remotePlayerRegistry: RemotePlayerRegistry? = null, private val localServerName: String = "", - // Delivery is queued rather than run inline: romaji conversion can reach the Google IME API, - // and a command executor runs on the tick thread. Queueing per sender keeps their messages in - // the order they typed them. private val deliveryQueue: PerPlayerWorkQueue = plugin.deliveryQueue, ) : LunaticCommand(plugin) { override val description: String diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt index 78871b7..ae2ebfe 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt @@ -49,10 +49,7 @@ class ChannelStatusCommand( val activeChannel = activeChannelId?.let { channelManager.getChannel(it).getOrNull() } // Get all player's channels - val playerChannelIds = - membershipManager.getPlayerChannels(sender.uniqueId).getOrElse { - return fail("channel.status.error") - } + val playerChannelIds = membershipManager.getPlayerChannels(sender.uniqueId) // Display header sender.sendMessage( diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt index 2f71b7d..c558e31 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt @@ -38,8 +38,7 @@ class ChannelSwitchCommand( // Tab completion: suggest channels the player is a member of val sender = ctx.source.executor if (sender is org.bukkit.entity.Player) { - val playerChannels = membershipManager.getPlayerChannels(sender.uniqueId).getOrNull() ?: emptyList() - playerChannels.forEach { channelId -> + membershipManager.getPlayerChannels(sender.uniqueId).forEach { channelId -> builder.suggest(channelId) } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt index 2ce24b6..e01942d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt @@ -22,6 +22,10 @@ import java.util.logging.Logger class ConfigManager( private val logger: Logger, ) { + private companion object { + const val UNREADABLE = "config.yml could not be read" + } + private val yaml = Yaml( configuration = @@ -44,7 +48,7 @@ class ConfigManager( // Editors that write a UTF-8 BOM would otherwise leave it on the first key, which // strictMode = false then drops as an unknown setting without a word. yaml.parseToYamlNode(contents.removePrefix("\uFEFF")) - } catch (e: EmptyYamlDocumentException) { + } catch (_: EmptyYamlDocumentException) { // A file that only holds comments is a valid way of saying "use the defaults", so it // is not reported as a failure the operator has to act on. return LunaticChatConfiguration() @@ -67,13 +71,13 @@ class ConfigManager( val setting = e.path.settingKeys() val remaining = document.without(setting) - ?: return allDefaults("config.yml could not be read", e) + ?: return allDefaults(UNREADABLE, e) logger.warning("${setting.joinToString(".")} in config.yml fell back to its default: ${e.message}") document = remaining } catch (e: Exception) { // A serializer can fail without kaml turning it into a YamlException, and there is // no path to prune a single setting by without one. - return allDefaults("config.yml could not be read", e) + return allDefaults(UNREADABLE, e) } } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt index 3c93fb8..72c1395 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt @@ -4,7 +4,6 @@ import com.charleskorn.kaml.YamlException import com.charleskorn.kaml.YamlInput import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder @@ -31,18 +30,15 @@ object LenientBooleanSerializer : KSerializer<Boolean> { override val descriptor = PrimitiveSerialDescriptor("LenientBoolean", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): Boolean { - // Captured before decoding: ConfigManager prunes the offending setting by path, and only a - // YamlException carries one. A plain SerializationException would escape it and cost the - // operator the whole file. - val path = (decoder as? YamlInput)?.node?.path + // Captured before decoding: ConfigManager prunes the offending setting by the path its + // exception carries, and only a YamlException carries one. Throwing without a path would + // cost the operator every other setting in the file. + val path = (decoder as YamlInput).node.path val raw = decoder.decodeString() return when (raw.lowercase()) { in trueWords -> true in falseWords -> false - else -> { - val reason = "expected true/false, yes/no or on/off but found '$raw'" - throw path?.let { YamlException(reason, it) } ?: SerializationException(reason) - } + else -> throw YamlException("expected true/false, yes/no or on/off but found '$raw'", path) } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt index 2e50081..20dd819 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt @@ -20,30 +20,29 @@ object SpyPermissionManager : Listener { private val directMessageSpyPlayers: ConcurrentHashMap<UUID, Player> = ConcurrentHashMap() /** - * Gets all players with spy permission as a map of UUID to Player. - */ - fun getDirectMessageSpyPlayers(): Map<UUID, Player> = directMessageSpyPlayers.toMap() - - /** * Sends a copy of a message to every online spy that [exclude] does not reject. * - * [message] is only invoked when someone will actually read the result, and the "you are - * seeing this because you have spy permission" hover is built once for the whole audience - * rather than per recipient. Spies are rare, so both matter on the message path. + * [noticeText], [exclude] and [message] are only invoked when someone will actually read the + * result, and the "you are seeing this because you have spy permission" hover is built once for + * the whole audience rather than per recipient. Spies are rare, so all of that matters on the + * message path - callers otherwise pay a translation lookup, and channel chat an O(members) set, + * for an audience that is usually empty. * * @param noticeText Text shown on hover, explaining why the reader is seeing the message * @param exclude Rejects players who are party to the message already * @param message Builds the message body */ fun notifySpies( - noticeText: String, + noticeText: () -> String, exclude: (Player) -> Boolean, message: () -> Component, ) { + if (directMessageSpyPlayers.isEmpty()) return + val spies = directMessageSpyPlayers.values.filter { it.isOnline && !exclude(it) } if (spies.isEmpty()) return - val withNotice = message().hoverEvent(HoverEvent.showText(Component.text(noticeText))) + val withNotice = message().hoverEvent(HoverEvent.showText(Component.text(noticeText()))) spies.forEach { it.sendMessage(withNotice) } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt index 8328ad8..574cb30 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt @@ -7,7 +7,6 @@ import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter -import kotlinx.coroutines.CancellationException import org.bukkit.entity.Player import org.bukkit.plugin.Plugin import java.util.UUID @@ -40,6 +39,11 @@ class CrossServerDirectMessageManager( * goes through may wait on the Google IME API. The reply target is recorded by the command * before the work is queued; the sender-side display and the spy notification are handled by * [DirectMessageHandler], and the (possibly romaji-converted) body is what gets relayed. + * + * Failures are deliberately not caught here. The delivery queue is the error boundary for + * queued work and already reports them without stopping the sender's later messages; a second + * boundary underneath it only obscured where failures on this path are handled - and made an + * ordinary cancellation at shutdown look like a delivery failure. */ suspend fun sendCrossServerMessage( sender: Player, @@ -47,40 +51,32 @@ class CrossServerDirectMessageManager( targetServerName: String, message: String, ) { - try { - val messageId = UUID.randomUUID().toString() - processedMessages.markProcessed(messageId) + val messageId = UUID.randomUUID().toString() + processedMessages.markProcessed(messageId) - val relayedMessage = - directMessageHandler.handleOutgoingCrossServerMessage( - sender = sender, - targetName = targetName, - targetServerName = targetServerName, - message = message, - ) + val relayedMessage = + directMessageHandler.handleOutgoingCrossServerMessage( + sender = sender, + targetName = targetName, + targetServerName = targetServerName, + message = message, + ) - val relay = - PluginMessage.DirectMessageRelay( - messageId = messageId, - sourceServerName = configuration.features.velocityIntegration.serverName, - senderId = sender.uniqueId.toString(), - senderName = sender.name, - targetServerName = targetServerName, - targetName = targetName, - message = relayedMessage, - ) + val relay = + PluginMessage.DirectMessageRelay( + messageId = messageId, + sourceServerName = configuration.features.velocityIntegration.serverName, + senderId = sender.uniqueId.toString(), + senderName = sender.name, + targetServerName = targetServerName, + targetName = targetName, + message = relayedMessage, + ) - sender.sendPluginMessage(plugin, PluginMessageChannel.ID, PluginMessageCodec.encode(relay)) - logger.fine { - "Sent direct message to Velocity: messageId=$messageId, " + - "target=$targetName@$targetServerName" - } - } catch (e: CancellationException) { - // Shutdown cancelling the delivery queue is not a delivery failure, and reporting it as - // SEVERE while carrying on past the cancellation would be wrong twice over. - throw e - } catch (e: Exception) { - logger.log(Level.SEVERE, "Failed to send cross-server direct message", e) + sender.sendPluginMessage(plugin, PluginMessageChannel.ID, PluginMessageCodec.encode(relay)) + logger.fine { + "Sent direct message to Velocity: messageId=$messageId, " + + "target=$targetName@$targetServerName" } } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt index 02c4d1a..f0c41c9 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt @@ -382,7 +382,7 @@ class ChannelMembershipManagerTest { channelManager.setPlayerChannel(playerId, null) membership.joinChannel(playerId, "ch2") - val channels = membership.getPlayerChannels(playerId).getOrThrow() + val channels = membership.getPlayerChannels(playerId) assertEquals(2, channels.size) assertTrue(channels.contains("ch1")) assertTrue(channels.contains("ch2")) @@ -495,6 +495,6 @@ class ChannelMembershipManagerTest { channelManager.deleteChannel("drop-ch", ownerId) - assertEquals(listOf("keep-ch"), membership.getPlayerChannels(playerId).getOrThrow()) + assertEquals(listOf("keep-ch"), membership.getPlayerChannels(playerId)) } } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt index a2ba325..1edef92 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt @@ -24,8 +24,6 @@ import kotlin.test.assertTrue * Validates message handling with dependency injection and conversion features. */ class DirectMessageHandlerTest { - private fun <T> sync(block: suspend () -> T): T = runBlocking { block() } - private fun createHandler( configuration: dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration? = null, settingsManager: PlayerSettingsManager? = null, @@ -43,7 +41,7 @@ class DirectMessageHandlerTest { val sender = TestUtils.createMockPlayer() val recipient = TestUtils.createMockPlayer() - val result = sync { handler.sendDirectMessage(sender, recipient, "Test message") } + val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test message") } assertTrue(result) } @@ -65,7 +63,7 @@ class DirectMessageHandlerTest { val sender = TestUtils.createMockPlayer(name = "Alice") val recipient = TestUtils.createMockPlayer(name = "Bob") - val result = sync { handler.sendDirectMessage(sender, recipient, "Test") } + val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test") } assertTrue(result) } @@ -84,7 +82,7 @@ class DirectMessageHandlerTest { val sender = TestUtils.createMockPlayer() val recipient = TestUtils.createMockPlayer() - val result = sync { handler.sendDirectMessage(sender, recipient, "konnichiwa") } + val result = runBlocking { handler.sendDirectMessage(sender, recipient, "konnichiwa") } assertTrue(result) } @@ -110,7 +108,7 @@ class DirectMessageHandlerTest { val recipient = TestUtils.createMockPlayer() // Should not throw exception and should complete quickly (within timeout) - val result = sync { handler.sendDirectMessage(sender, recipient, "konnichiwa") } + val result = runBlocking { handler.sendDirectMessage(sender, recipient, "konnichiwa") } assertTrue(result) } @@ -125,7 +123,7 @@ class DirectMessageHandlerTest { // This validates Issue #1 refactoring - ConfigManager DI val sender = TestUtils.createMockPlayer() val recipient = TestUtils.createMockPlayer() - val result = sync { handler.sendDirectMessage(sender, recipient, "Test") } + val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test") } assertTrue(result) } @@ -139,15 +137,24 @@ class DirectMessageHandlerTest { } @Test - fun `handleOutgoingCrossServerMessage records a remote reply target and returns the body`() { + fun `handleOutgoingCrossServerMessage returns the body to relay`() { + val handler = createHandler() + val sender = TestUtils.createMockPlayer(name = "Alice") + + val relayed = runBlocking { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } + + assertEquals("hi", relayed) + } + + @Test + fun `recordRemoteRecipient makes a remote player the reply target`() { val handler = createHandler() val registry = RemotePlayerRegistry(localServerName = "lobby") registry.replaceAll(listOf(PresenceEntry("Bob", "survival"))) handler.remotePlayerRegistry = registry val sender = TestUtils.createMockPlayer(name = "Alice") - val relayed = sync { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } - assertEquals("hi", relayed) + handler.recordRemoteRecipient(sender, "Bob", "survival") val target = handler.getReplyTarget(sender) assertIs<ReplyTarget.Remote>(target) @@ -177,7 +184,7 @@ class DirectMessageHandlerTest { handler.remotePlayerRegistry = RemotePlayerRegistry(localServerName = "lobby") // empty roster val sender = TestUtils.createMockPlayer(name = "Alice") - sync { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } + handler.recordRemoteRecipient(sender, "Bob", "survival") assertNull(handler.getReplyTarget(sender)) } @@ -209,7 +216,7 @@ class DirectMessageHandlerTest { handler.remotePlayerRegistry = registry val sender = TestUtils.createMockPlayer(name = "Alice") - sync { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } + handler.recordRemoteRecipient(sender, "Bob", "survival") handler.clearPlayer(sender) assertNull(handler.getReplyTarget(sender)) @@ -228,7 +235,7 @@ class DirectMessageHandlerTest { val sender = TestUtils.createMockPlayer() val recipient = TestUtils.createMockPlayer() - val result = sync { handler.sendDirectMessage(sender, recipient, "Test") } + val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test") } assertTrue(result) } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt index 301160b..bbcfafb 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.TestUtils import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -64,7 +63,7 @@ class ChannelStatusCommandTest { every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId every { deps.channelManager.getChannel(channelId) } returns Result.success(channel) every { deps.channelManager.getChannelMembers(channelId) } returns Result.success(members) - every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(listOf(channelId)) + every { deps.membershipManager.getPlayerChannels(testUUID) } returns listOf(channelId) mockkStatic(Bukkit::class) try { @@ -83,7 +82,7 @@ class ChannelStatusCommandTest { val deps = createDependencies() every { deps.channelManager.getPlayerChannel(testUUID) } returns null - every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(emptyList()) + every { deps.membershipManager.getPlayerChannels(testUUID) } returns emptyList() mockkStatic(Bukkit::class) try { @@ -113,7 +112,7 @@ class ChannelStatusCommandTest { Result.success( listOf(TestUtils.createTestChannelMember(playerId = testUUID)), ) - every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(channelIds) + every { deps.membershipManager.getPlayerChannels(testUUID) } returns channelIds mockkStatic(Bukkit::class) try { @@ -126,17 +125,4 @@ class ChannelStatusCommandTest { unmockkStatic(Bukkit::class) } } - - @Test - fun `execute should return Failure on error`() { - val deps = createDependencies() - - every { deps.channelManager.getPlayerChannel(testUUID) } returns null - every { deps.membershipManager.getPlayerChannels(testUUID) } returns - Result.failure(ChannelNotFoundException("error")) - - val result = deps.command.execute(deps.ctx) - - assertIs<CommandResult.Failure>(result) - } } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt index d415e97..fc7afc9 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt @@ -24,15 +24,6 @@ class ConfigManagerTest { }.bufferedReader().use { it.readText() } @Test - fun `the bundled config parses`() { - val config = load(bundledConfig) - - assertFalse(config.debug) - assertEquals("player-settings.yaml", config.userSettingsFilePath) - assertEquals(Language.EN, config.language) - } - - @Test fun `the bundled config agrees with the declared defaults`() { // The data class is meant to be the single source of every default. If config.yml ships a // different value for a key, one of the two is lying to the operator. diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt index 03938fc..a6060ce 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt @@ -4,6 +4,8 @@ import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage import dev.m1sk9.lunaticChat.paper.TestUtils import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -15,8 +17,6 @@ import java.util.logging.Logger import kotlin.test.Test class CrossServerDirectMessageManagerTest { - private fun <T> sync(block: suspend () -> T): T = runBlocking { block() } - private class Fixture( cacheSize: Int = 100, ) { @@ -56,11 +56,11 @@ class CrossServerDirectMessageManagerTest { fun `sendCrossServerMessage relays via plugin channel and delegates display`() { val f = Fixture() val sender = TestUtils.createMockPlayer(name = "Alice") - every { sync { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } } returns "hi" + coEvery { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } returns "hi" - sync { f.manager.sendCrossServerMessage(sender, "Bob", "survival", "hi") } + runBlocking { f.manager.sendCrossServerMessage(sender, "Bob", "survival", "hi") } - verify { sync { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } } + coVerify { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } verify { sender.sendPluginMessage(f.plugin, "lunaticchat:main", any<ByteArray>()) } } @@ -143,9 +143,9 @@ class CrossServerDirectMessageManagerTest { fun `sendCrossServerMessage prunes the dedup cache when over capacity`() { val f = Fixture(cacheSize = 1) val sender = TestUtils.createMockPlayer(name = "Alice") - every { sync { f.dmHandler.handleOutgoingCrossServerMessage(any(), any(), any(), any()) } } returns "hi" + coEvery { f.dmHandler.handleOutgoingCrossServerMessage(any(), any(), any(), any()) } returns "hi" - repeat(3) { sync { f.manager.sendCrossServerMessage(sender, "Bob$it", "survival", "hi") } } + repeat(3) { runBlocking { f.manager.sendCrossServerMessage(sender, "Bob$it", "survival", "hi") } } verify(atLeast = 1) { sender.sendPluginMessage(f.plugin, "lunaticchat:main", any<ByteArray>()) } } |
