]> git.giacomolli.uk - onechat/blob - voice_transcriber_test.go
58df9ea1cfb5bb235a69050515d71ac54875c8cc
[onechat] / voice_transcriber_test.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/require"
13 )
14
15 func TestVoiceStoresPrivateAudioAndUsesPlainTextWithInternalTimestamps(t *testing.T) {
16 store, transcriber := voiceTestInput(t, `
17 output=
18 context=
19 while [ "$#" -gt 0 ]; do
20 case "$1" in
21 --no-timestamps) exit 2 ;;
22 --max-context) shift; context=$1 ;;
23 --output-file) shift; output=$1 ;;
24 esac
25 shift
26 done
27 [ "$context" = 0 ]
28 printf ' A made-up voice transcript.\n' > "$output.txt"
29 printf 'private speech in process output'
30 printf 'private speech in process errors' >&2
31 `)
32 data := []byte("made-up audio")
33 text := transcriber.Text(t.Context(), store, data)
34 require.Contains(t, text, "[voice ")
35 require.True(t, strings.HasSuffix(text, "]\nA made-up voice transcript."))
36 require.Equal(t, text, transcriber.Text(t.Context(), store, data))
37
38 files, err := os.ReadDir(store.dir)
39 require.NoError(t, err)
40 require.Len(t, files, 1)
41
42 saved, err := os.ReadFile(filepath.Join(store.dir, files[0].Name()))
43 require.NoError(t, err)
44 require.Equal(t, data, saved)
45
46 info, err := os.Stat(filepath.Join(store.dir, files[0].Name()))
47 require.NoError(t, err)
48 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
49
50 info, err = os.Stat(store.dir)
51 require.NoError(t, err)
52 require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
53 }
54
55 func TestVoiceFailuresKeepStoredAudioAndSafeMarkers(t *testing.T) {
56 testCases := []struct {
57 name string
58 script string
59 }{
60 {name: "failed process does not expose its errors", script: "echo secret-speech >&2; exit 1"},
61 {name: "missing transcript gets a marker", script: "exit 0"},
62 {name: "empty transcript gets a marker", script: "while [ \"$1\" != --output-file ]; do shift; done; shift; touch \"$1.txt\""},
63 }
64 for _, tc := range testCases {
65 t.Run(tc.name, func(t *testing.T) {
66 store, transcriber := voiceTestInput(t, tc.script)
67 text := transcriber.Text(t.Context(), store, []byte("made-up audio"))
68 require.Contains(t, text, "[voice ")
69 require.Contains(t, text, "(voice not transcribed: local transcription failed)")
70 require.NotContains(t, text, "secret-speech")
71
72 files, err := os.ReadDir(store.dir)
73 require.NoError(t, err)
74 require.Len(t, files, 1)
75 })
76 }
77 }
78
79 func TestVoiceCancellationAndCallerDeadlineStopTheProcess(t *testing.T) {
80 store, transcriber := voiceTestInput(t, "exec sleep 60")
81 ctx, cancel := context.WithTimeout(t.Context(), 20*time.Millisecond)
82 defer cancel()
83
84 text := transcriber.Text(ctx, store, []byte("made-up audio"))
85 require.Contains(t, text, "context deadline exceeded")
86
87 ctx, cancel = context.WithCancel(t.Context())
88 cancel()
89
90 text = transcriber.Text(ctx, store, []byte("made-up audio"))
91 require.Contains(t, text, "context canceled")
92 }
93
94 func TestVoiceUnavailableAndStorageFailuresDoNotStopIntake(t *testing.T) {
95 store, transcriber := voiceTestInput(t, "exit 0")
96 transcriber.config.Model = filepath.Join(t.TempDir(), "missing")
97 text := transcriber.Text(t.Context(), store, []byte("made-up audio"))
98 require.Contains(t, text, "[voice ")
99 require.Contains(t, text, "transcriber or model is not available")
100 require.Equal(t, "(voice not stored)", transcriber.Text(t.Context(), store, nil))
101 require.Equal(t, "(voice not stored)", transcriber.Text(t.Context(), store, bytes.Repeat([]byte{0}, voiceMaxBytes+1)))
102
103 blocked := filepath.Join(t.TempDir(), "blocked")
104 require.NoError(t, os.WriteFile(blocked, nil, 0o600))
105
106 store.dir = filepath.Join(blocked, "child")
107 require.Equal(t, "(voice not stored)", transcriber.Text(t.Context(), store, []byte("audio")))
108 }
109
110 func TestVoiceLookupHonorsExplicitThenNixThenPath(t *testing.T) {
111 _, transcriber := voiceTestInput(t, "exit 0")
112 valid := transcriber.config
113 t.Setenv("PATH", filepath.Dir(valid.Transcriber))
114
115 testCases := []struct {
116 name string
117 config VoiceConfig
118 program string
119 model string
120 available bool
121 }{
122 {
123 name: "explicit paths override invalid built paths",
124 config: valid,
125 program: "missing",
126 model: "missing",
127 available: true,
128 },
129 {
130 name: "built paths supply absent settings",
131 program: valid.Transcriber,
132 model: valid.Model,
133 available: true,
134 },
135 {
136 name: "PATH supplies absent executable",
137 config: VoiceConfig{Model: valid.Model},
138 available: true,
139 },
140 {
141 name: "invalid explicit executable does not fall back",
142 config: VoiceConfig{Transcriber: "missing", Model: valid.Model},
143 program: valid.Transcriber,
144 },
145 {
146 name: "invalid explicit model does not fall back",
147 config: VoiceConfig{Model: "missing"},
148 program: valid.Transcriber,
149 model: valid.Model,
150 },
151 {
152 name: "missing model leaves input unavailable",
153 program: valid.Transcriber,
154 },
155 }
156 for _, tc := range testCases {
157 t.Run(tc.name, func(t *testing.T) {
158 program, model, err := voiceLookup(tc.config, tc.program, tc.model)
159 if !tc.available {
160 require.ErrorIs(t, err, errVoiceUnavailable)
161
162 return
163 }
164
165 require.NoError(t, err)
166 require.Equal(t, valid.Transcriber, program)
167 require.Equal(t, valid.Model, model)
168 })
169 }
170 }