@@ -1358,3 +1358,208 @@ func TestChatModelDispatchVoiceLeaveDoesNotClearRoomBeforeAck(t *testing.T) {
13581358 t .Fatalf ("timed out waiting for leave command on ipc" )
13591359 }
13601360}
1361+
1362+ func TestWaitForVoiceMsg (t * testing.T ) {
1363+ ch := make (chan ipc.Message , 1 )
1364+ ch <- ipc.Message {Event : ipc .EventPong }
1365+
1366+ msg := waitForVoiceMsg (ch )()
1367+ voiceMsg , ok := msg .(voiceIPCMsg )
1368+ if ! ok {
1369+ t .Fatalf ("expected voiceIPCMsg, got %T" , msg )
1370+ }
1371+ if ipc .Message (voiceMsg ).Event != ipc .EventPong {
1372+ t .Fatalf ("expected pong event, got %#v" , ipc .Message (voiceMsg ))
1373+ }
1374+
1375+ closed := make (chan ipc.Message )
1376+ close (closed )
1377+ msg = waitForVoiceMsg (closed )()
1378+ errMsg , ok := msg .(voiceIPCErrorMsg )
1379+ if ! ok {
1380+ t .Fatalf ("expected voiceIPCErrorMsg, got %T" , msg )
1381+ }
1382+ if ! strings .Contains (errMsg .err .Error (), "voice daemon disconnected" ) {
1383+ t .Fatalf ("unexpected disconnect error: %v" , errMsg .err )
1384+ }
1385+ }
1386+
1387+ func TestChatModelConnectVoiceIPC (t * testing.T ) {
1388+ m := newChatForTest (t , & APIClient {serverURL : "http://server" , httpClient : http .DefaultClient })
1389+ m .voiceIPC = nil
1390+ if cmd := m .connectVoiceIPC (); cmd != nil {
1391+ t .Fatalf ("expected nil connect command when voice IPC is not configured" )
1392+ }
1393+
1394+ m .voiceIPC = newVoiceIPC ("" )
1395+ cmd := m .connectVoiceIPC ()
1396+ if cmd == nil {
1397+ t .Fatalf ("expected connect command" )
1398+ }
1399+ msg := cmd ()
1400+ errMsg , ok := msg .(voiceIPCErrorMsg )
1401+ if ! ok {
1402+ t .Fatalf ("expected voiceIPCErrorMsg for invalid address, got %T" , msg )
1403+ }
1404+ if ! strings .Contains (errMsg .err .Error (), "voice ipc address is empty" ) {
1405+ t .Fatalf ("unexpected connect error: %v" , errMsg .err )
1406+ }
1407+
1408+ addr := filepath .Join (t .TempDir (), "voice.sock" )
1409+ listener , err := ipc .Listen (addr )
1410+ if err != nil {
1411+ t .Fatalf ("listen voice ipc: %v" , err )
1412+ }
1413+ defer listener .Close ()
1414+ go func () {
1415+ conn , err := listener .Accept ()
1416+ if err != nil {
1417+ return
1418+ }
1419+ _ = conn .Close ()
1420+ }()
1421+
1422+ m .voiceIPC = newVoiceIPC (addr )
1423+ cmd = m .connectVoiceIPC ()
1424+ msg = cmd ()
1425+ connected , ok := msg .(voiceIPCConnectedMsg )
1426+ if ! ok {
1427+ t .Fatalf ("expected voiceIPCConnectedMsg, got %T" , msg )
1428+ }
1429+ if connected .ch == nil {
1430+ t .Fatalf ("expected non-nil voice IPC channel" )
1431+ }
1432+ }
1433+
1434+ func TestChatModelHandleVoiceCommandPaths (t * testing.T ) {
1435+ m := newChatForTest (t , & APIClient {serverURL : "http://server" , httpClient : http .DefaultClient })
1436+ if cmd := m .handleVoiceCommand ("/voice" , []string {"/voice" }); cmd != nil {
1437+ t .Fatalf ("expected no command for usage help" )
1438+ }
1439+ if ! strings .Contains (lastSystemMessage (m ), "voice commands" ) {
1440+ t .Fatalf ("expected voice help message" )
1441+ }
1442+
1443+ m .voiceIPC = nil
1444+ if cmd := m .handleVoiceCommand ("/voice mute" , []string {"/voice" , "mute" }); cmd != nil {
1445+ t .Fatalf ("expected no command when voice IPC is nil" )
1446+ }
1447+ if ! strings .Contains (lastSystemMessage (m ), "voice daemon not configured" ) {
1448+ t .Fatalf ("expected missing daemon message" )
1449+ }
1450+
1451+ m .voiceIPC = & voiceIPC {addr : "" }
1452+ m .voiceRoom = "room-1"
1453+ _ = m .handleVoiceCommand ("/voice leave" , []string {"/voice" , "leave" })
1454+ if ! strings .Contains (lastSystemMessage (m ), "voice leave failed" ) {
1455+ t .Fatalf ("expected leave failure message" )
1456+ }
1457+ _ = m .handleVoiceCommand ("/voice mute" , []string {"/voice" , "mute" })
1458+ if ! strings .Contains (lastSystemMessage (m ), "voice mute failed" ) {
1459+ t .Fatalf ("expected mute failure message" )
1460+ }
1461+ _ = m .handleVoiceCommand ("/voice unmute" , []string {"/voice" , "unmute" })
1462+ if ! strings .Contains (lastSystemMessage (m ), "voice unmute failed" ) {
1463+ t .Fatalf ("expected unmute failure message" )
1464+ }
1465+
1466+ addr := filepath .Join (t .TempDir (), "voice.sock" )
1467+ listener , err := ipc .Listen (addr )
1468+ if err != nil {
1469+ t .Fatalf ("listen voice ipc: %v" , err )
1470+ }
1471+ defer listener .Close ()
1472+
1473+ recv := make (chan ipc.Message , 1 )
1474+ go func () {
1475+ conn , err := listener .Accept ()
1476+ if err != nil {
1477+ return
1478+ }
1479+ defer conn .Close ()
1480+ var msg ipc.Message
1481+ if err := ipc .NewDecoder (conn ).Decode (& msg ); err == nil {
1482+ recv <- msg
1483+ }
1484+ }()
1485+
1486+ m .voiceIPC = newVoiceIPC (addr )
1487+ m .voiceCh = make (chan ipc.Message , 1 )
1488+ m .channels ["ch-1" ] = channelInfo {ID : "ch-1" , Name : "general" }
1489+ m .activeChannel = "ch-1"
1490+
1491+ if cmd := m .handleVoiceCommand ("/voice join" , []string {"/voice" , "join" }); cmd != nil {
1492+ t .Fatalf ("expected no reconnect cmd when voice channel already connected" )
1493+ }
1494+ if m .voiceRoom != "ch-1" {
1495+ t .Fatalf ("expected voice room set to joined channel, got %q" , m .voiceRoom )
1496+ }
1497+ if ! strings .Contains (lastSystemMessage (m ), "voice join requested" ) {
1498+ t .Fatalf ("expected join notice" )
1499+ }
1500+
1501+ select {
1502+ case sent := <- recv :
1503+ if sent .Cmd != ipc .CommandVoiceJoin || sent .Room != "ch-1" {
1504+ t .Fatalf ("unexpected join IPC message: %#v" , sent )
1505+ }
1506+ case <- time .After (2 * time .Second ):
1507+ t .Fatalf ("timeout waiting for join message" )
1508+ }
1509+ }
1510+
1511+ func TestChatModelVoiceHelperMethods (t * testing.T ) {
1512+ m := newChatForTest (t , & APIClient {serverURL : "http://server" , httpClient : http .DefaultClient })
1513+
1514+ pending := ipc.Message {Cmd : ipc .CommandVoiceJoin , Room : "room-1" }
1515+ m .queueVoiceCommand (pending , "room-1" , "queued" )
1516+ m .clearPendingVoiceCommand ()
1517+ if m .voicePendingCmd != nil || m .voicePendingRoom != "" || m .voicePendingNotice != "" {
1518+ t .Fatalf ("expected pending voice command state to be cleared" )
1519+ }
1520+
1521+ if cmd := m .scheduleVoicePing (); cmd == nil {
1522+ t .Fatalf ("expected non-nil voice ping schedule command" )
1523+ }
1524+
1525+ m .voiceMembers = nil
1526+ m .clearVoiceMembers ()
1527+ if m .voiceMembers == nil {
1528+ t .Fatalf ("expected voiceMembers map initialized" )
1529+ }
1530+ m .voiceMembers ["user-1" ] = true
1531+ m .clearVoiceMembers ()
1532+ if len (m .voiceMembers ) != 0 {
1533+ t .Fatalf ("expected voiceMembers map cleared" )
1534+ }
1535+ }
1536+
1537+ func TestChatModelShareDirectoryKeyCmdGuards (t * testing.T ) {
1538+ m := newChatForTest (t , & APIClient {serverURL : "http://server" , httpClient : http .DefaultClient })
1539+ m .auth .IsTrusted = false
1540+ if cmd := m .shareDirectoryKeyCmd (); cmd != nil {
1541+ t .Fatalf ("expected nil cmd when user is not trusted" )
1542+ }
1543+
1544+ m .auth .IsTrusted = true
1545+ m .directoryKey = nil
1546+ if cmd := m .shareDirectoryKeyCmd (); cmd != nil {
1547+ t .Fatalf ("expected nil cmd without directory key" )
1548+ }
1549+
1550+ m .directoryKey = bytes .Repeat ([]byte {7 }, crypto .KeySize )
1551+ m .api = nil
1552+ m .kp = nil
1553+ cmd := m .shareDirectoryKeyCmd ()
1554+ if cmd == nil {
1555+ t .Fatalf ("expected cmd when trusted and directory key exists" )
1556+ }
1557+ msg := cmd ()
1558+ result , ok := msg .(shareDirectoryMsg )
1559+ if ! ok {
1560+ t .Fatalf ("expected shareDirectoryMsg, got %T" , msg )
1561+ }
1562+ if result .err != nil {
1563+ t .Fatalf ("expected nil error when shareDirectoryKey short-circuits missing deps, got %v" , result .err )
1564+ }
1565+ }
0 commit comments