Files
bitchat/bitchatTests/ProtocolContractTests.swift
Hot Pixel GroupandGitHub e191e9c6f2 Fix slash-command suggestions that insert commands the processor rejects (#1354)
The autocomplete panel is the only in-app surface for discovering slash
commands, but several suggestions do not match what CommandProcessor
accepts, so tapping them inserts a command that returns "unknown
command":

- CommandInfo suggests /dm, /favorite, /unfavorite, but the processor
  only handles /m, /msg, /fav, /unfav. Aliases are aligned to the
  accepted spellings (msg, fav, unfav).
- Favorites are suggested only in geohash contexts (isGeoPublic ||
  isGeoDM) — exactly where the processor rejects them ("favorites are
  only for mesh peers"). The gating is inverted so they appear in mesh,
  where they work.

Also, small related fixes to the discovery surface:
- /help is now handled (the ChatViewModel command docstring already
  claimed it existed); it prints a local system line listing the valid
  commands, and the unknown-command error points at it.
- The suggestion panel keeps the matched command's usage row (e.g.
  "/msg <nickname>") visible while arguments are typed, instead of
  vanishing at the first space; in that mode the row is informational
  and no longer overwrites the draft on tap.

New string is added source-language (en) only. The CommandInfo contract
test is updated to the corrected metadata.
2026-07-05 14:15:57 +02:00

122 lines
5.6 KiB
Swift

import Testing
import Foundation
import Combine
import CoreBluetooth
import BitFoundation
@testable import bitchat
private final class DefaultDelegateProbe: BitchatDelegate {
func didReceiveMessage(_ message: BitchatMessage) {}
func didConnectToPeer(_ peerID: PeerID) {}
func didDisconnectFromPeer(_ peerID: PeerID) {}
func didUpdatePeerList(_ peers: [PeerID]) {}
func didUpdateBluetoothState(_ state: CBManagerState) {}
}
private final class DefaultTransportProbe: Transport {
weak var delegate: BitchatDelegate?
weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate?
let subject = CurrentValueSubject<[TransportPeerSnapshot], Never>([])
let myPeerID = PeerID(str: "0011223344556677")
var myNickname = "Tester"
private(set) var sentMessages: [(content: String, mentions: [String])] = []
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
subject.eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { subject.value }
func setNickname(_ nickname: String) { myNickname = nickname }
func startServices() {}
func stopServices() {}
func emergencyDisconnectAll() {}
func isPeerConnected(_ peerID: PeerID) -> Bool { false }
func isPeerReachable(_ peerID: PeerID) -> Bool { false }
func peerNickname(peerID: PeerID) -> String? { nil }
func getPeerNicknames() -> [PeerID: String] { [:] }
func getFingerprint(for peerID: PeerID) -> String? { nil }
func getNoiseSessionState(for peerID: PeerID) -> LazyHandshakeState { .none }
func triggerHandshake(with peerID: PeerID) {}
func sendMessage(_ content: String, mentions: [String]) { sentMessages.append((content, mentions)) }
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {}
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {}
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {}
func sendBroadcastAnnounce() {}
func sendDeliveryAck(for messageID: String, to peerID: PeerID) {}
}
struct ProtocolContractTests {
@Test
func commandInfo_exposesAliasesPlaceholdersAndGeoVariants() {
// Aliases must match what CommandProcessor actually accepts —
// the suggestion panel is the only command-discovery surface.
#expect(CommandInfo.message.id == "msg")
#expect(CommandInfo.message.alias == "/msg")
#expect(CommandInfo.message.placeholder != nil)
#expect(CommandInfo.clear.placeholder == nil)
#expect(CommandInfo.favorite.description.isEmpty == false)
#expect(CommandInfo.all(isGeoPublic: false, isGeoDM: false).contains(.help))
// Favorites are rejected by the processor in geohash contexts, so
// they are suggested only in mesh.
#expect(CommandInfo.all(isGeoPublic: false, isGeoDM: false).contains(.favorite))
#expect(CommandInfo.all(isGeoPublic: true, isGeoDM: false).contains(.favorite) == false)
#expect(CommandInfo.all(isGeoPublic: false, isGeoDM: true).contains(.unfavorite) == false)
}
@Test
func protocolEnums_andDelegateDefaults_haveStableContracts() {
let delegate = DefaultDelegateProbe()
let peerID = PeerID(str: "8899aabbccddeeff")
#expect(MessageType.requestSync.description == "requestSync")
#expect(NoisePayloadType.verifyResponse.description == "verifyResponse")
#expect(DeliveryStatus.sending.displayText == "Sending...")
#expect(DeliveryStatus.sent.displayText == "Sent")
#expect(DeliveryStatus.delivered(to: "Alice", at: Date()).displayText == "Delivered to Alice")
#expect(DeliveryStatus.read(by: "Bob", at: Date()).displayText == "Read by Bob")
#expect(DeliveryStatus.failed(reason: "oops").displayText == "Failed: oops")
#expect(DeliveryStatus.partiallyDelivered(reached: 1, total: 3).displayText == "Delivered to 1/3")
#expect(delegate.isFavorite(fingerprint: "fp") == false)
delegate.didUpdateMessageDeliveryStatus("msg-1", status: .sent)
delegate.didReceiveNoisePayload(from: peerID, type: .privateMessage, payload: Data(), timestamp: Date())
delegate.didReceivePublicMessage(from: peerID, nickname: "Alice", content: "hi", timestamp: Date(), messageID: "msg-1")
}
@Test
func transportDefaults_forwardOrNoOp() {
let probe = DefaultTransportProbe()
let peerID = PeerID(str: "0123456789abcdef")
let filePacket = BitchatFilePacket(
fileName: "voice.m4a",
fileSize: 4,
mimeType: "audio/mp4",
content: Data([1, 2, 3, 4])
)
probe.sendMessage("hello", mentions: ["@alice"], messageID: "msg-1", timestamp: Date())
probe.sendVerifyChallenge(to: peerID, noiseKeyHex: "abcd", nonceA: Data([0x01]))
probe.sendVerifyResponse(to: peerID, noiseKeyHex: "abcd", nonceA: Data([0x02]))
probe.sendFileBroadcast(filePacket, transferId: "tx-1")
probe.sendFilePrivate(filePacket, to: peerID, transferId: "tx-2")
probe.cancelTransfer("tx-3")
probe.declinePendingFile(id: "pending")
#expect(probe.sentMessages.count == 1)
#expect(probe.sentMessages.first?.content == "hello")
#expect(probe.acceptPendingFile(id: "pending") == nil)
}
@Test
func previewMessage_exposesStableSampleShape() {
let preview = BitchatMessage.preview
#expect(preview.sender == "John Doe")
#expect(preview.content == "Hello")
#expect(preview.deliveryStatus == .sent)
#expect(preview.isPrivate == false)
}
}