+package main
+
+import (
+ "errors"
+ "net/http"
+ "os"
+ "path/filepath"
+ "testing"
+ "testing/synctest"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "go.mau.fi/whatsmeow/proto/waE2E"
+ "go.mau.fi/whatsmeow/types"
+ "go.mau.fi/whatsmeow/types/events"
+)
+
+func TestWhatsAppVoiceNotesAreDurableBeforeAcknowledgement(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ whatsapp.client = fake
+ _, whatsapp.inbox.voice = voiceTestInput(t, voiceTestScript)
+ audio := []byte("made-up WhatsApp voice")
+ fake.downloadData = audio
+ event := whatsappVoiceTestEvent(whatsappTestAna)
+ require.True(t, whatsapp.handle(event))
+ require.NoError(t, whatsapp.state.CloseAll())
+
+ restored := whatsappTestState(t, whatsapp.state.path)
+ require.Len(t, restored.Closed(), 1)
+
+ text := restored.Closed()[0].Messages[0].Text
+ require.Contains(t, text, "[voice ")
+ require.Contains(t, text, "A made-up voice transcript.")
+
+ files, err := os.ReadDir(whatsapp.inbox.voices.dir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ stored, err := os.ReadFile(filepath.Join(whatsapp.inbox.voices.dir, files[0].Name()))
+ require.NoError(t, err)
+ require.Equal(t, audio, stored)
+ require.True(t, whatsapp.handle(event))
+ require.NoError(t, whatsapp.state.CloseAll())
+ require.Len(t, whatsapp.state.Closed()[0].Messages, 1)
+}
+
+func TestWhatsAppVoiceRoutingSurvivesRestartWithoutDuplicateEntries(t *testing.T) {
+ dir := t.TempDir()
+ server := newModelFake(t)
+ server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
+ }
+ first := newDaemonMessageTest(t, dir, server)
+ whatsappTestSession(t, dir, true)
+
+ whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
+ whatsapp.client = fake
+ _, whatsapp.inbox.voice = voiceTestInput(t, voiceTestScript)
+ fake.downloadData = []byte("made-up routed voice")
+ event := whatsappVoiceTestEvent(whatsappTestAna)
+ event.Info.Chat = whatsappTestFamily
+ event.Info.IsGroup = true
+
+ whatsapp.groups.set(whatsappTestFamily.String(), "Garden plans")
+ require.NoError(t, first.messages.routes.Set("whatsapp:"+whatsappTestFamily.String(), "Garden"))
+ require.True(t, whatsapp.handle(event))
+ require.NoError(t, whatsapp.state.CloseAll())
+
+ batch := whatsapp.state.Closed()[0]
+ text := batch.Text(time.UTC)
+ routed, err := first.messages.RouteWhatsApp(batch, text)
+ require.NoError(t, err)
+ require.True(t, routed)
+ first.Close(t)
+
+ second := newDaemonMessageTest(t, dir, server)
+ restored := whatsappRouteTestInbox(t, dir)
+ require.NoError(t, restored.deliver(t.Context(), second.loops[0], WhatsAppLink{}, true))
+ require.Empty(t, restored.state.Closed())
+ require.Zero(t, second.agents[0].memory.Len())
+ require.Equal(t, 1, second.agents[1].memory.Len())
+
+ message, err := second.agents[1].memory.Message(0)
+ require.NoError(t, err)
+ require.Equal(t, text, message.Text)
+ require.Contains(t, text, "[voice ")
+ require.Contains(t, text, "A made-up voice transcript.")
+ require.Equal(t, whatsapp.inbox.voices.dir, second.agents[1].agent.Voices().dir)
+ second.Start(t, 1)
+ second.events[1].WaitFor(t, "idle", 1)
+ require.Contains(t, string(server.Call(0).raw), "A made-up voice transcript.")
+ require.NotContains(t, logs.String()+second.logs.String(), "A made-up voice transcript")
+ second.Close(t)
+
+ third := newDaemonMessageTest(t, dir, server)
+ require.Equal(t, 1, third.agents[1].memory.Len())
+ require.False(t, third.loops[1].waiting())
+}
+
+func TestWhatsAppVoiceFailuresKeepLaterMessagesMoving(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ whatsapp.client = fake
+ fake.downloadWait = 3 * time.Minute
+
+ require.True(t, whatsapp.handle(whatsappVoiceTestEvent(whatsappTestAna)))
+
+ fake.downloadWait = 0
+ fake.downloadErr = errors.New("secret URL and key")
+ event := whatsappVoiceTestEvent(whatsappTestAna)
+ event.Info.ID = "failed"
+ require.True(t, whatsapp.handle(event))
+ require.True(t, whatsapp.handle(whatsappTestMessage(whatsappTestAna, whatsappTestAna, "next", "Next text.", event.Info.Timestamp)))
+ require.NoError(t, whatsapp.state.CloseAll())
+
+ messages := whatsapp.state.Closed()[0].Messages
+ require.Len(t, messages, 3)
+ require.Equal(t, "(voice not downloaded)", messages[0].Text)
+ require.Equal(t, "(voice not downloaded)", messages[1].Text)
+ require.Equal(t, "Next text.", messages[2].Text)
+ })
+}
+
+func TestWhatsAppSkipsViewOnceAndNonVoiceAudio(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ whatsapp.client = fake
+ event := whatsappVoiceTestEvent(whatsappTestAna)
+ event.Message.AudioMessage.ViewOnce = new(true)
+ require.True(t, whatsapp.handle(event))
+
+ event.Message.AudioMessage.ViewOnce = new(false)
+ event.Message.AudioMessage.PTT = new(false)
+ require.True(t, whatsapp.handle(event))
+ require.Empty(t, fake.Downloads())
+ require.NoError(t, whatsapp.state.CloseAll())
+ require.Empty(t, whatsapp.state.Closed())
+}
+
+func whatsappVoiceTestEvent(sender types.JID) *events.Message {
+ event := whatsappTestMessage(whatsappTestAna, sender, "voice", "", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
+ event.Message = &waE2E.Message{AudioMessage: &waE2E.AudioMessage{
+ PTT: new(true),
+ Mimetype: new("audio/ogg; codecs=opus"),
+ }}
+
+ return event
+}