+package main
+
+import (
+ "bytes"
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestVoiceStoresPrivateAudioAndUsesPlainTextWithInternalTimestamps(t *testing.T) {
+ store, transcriber := voiceTestInput(t, `
+output=
+context=
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --no-timestamps) exit 2 ;;
+ --max-context) shift; context=$1 ;;
+ --output-file) shift; output=$1 ;;
+ esac
+ shift
+done
+[ "$context" = 0 ]
+printf ' A made-up voice transcript.\n' > "$output.txt"
+printf 'private speech in process output'
+printf 'private speech in process errors' >&2
+`)
+ data := []byte("made-up audio")
+ text := transcriber.Text(t.Context(), store, data)
+ require.Contains(t, text, "[voice ")
+ require.True(t, strings.HasSuffix(text, "]\nA made-up voice transcript."))
+ require.Equal(t, text, transcriber.Text(t.Context(), store, data))
+
+ files, err := os.ReadDir(store.dir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ saved, err := os.ReadFile(filepath.Join(store.dir, files[0].Name()))
+ require.NoError(t, err)
+ require.Equal(t, data, saved)
+
+ info, err := os.Stat(filepath.Join(store.dir, files[0].Name()))
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+
+ info, err = os.Stat(store.dir)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
+}
+
+func TestVoiceFailuresKeepStoredAudioAndSafeMarkers(t *testing.T) {
+ testCases := []struct {
+ name string
+ script string
+ }{
+ {name: "failed process does not expose its errors", script: "echo secret-speech >&2; exit 1"},
+ {name: "missing transcript gets a marker", script: "exit 0"},
+ {name: "empty transcript gets a marker", script: "while [ \"$1\" != --output-file ]; do shift; done; shift; touch \"$1.txt\""},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ store, transcriber := voiceTestInput(t, tc.script)
+ text := transcriber.Text(t.Context(), store, []byte("made-up audio"))
+ require.Contains(t, text, "[voice ")
+ require.Contains(t, text, "(voice not transcribed: local transcription failed)")
+ require.NotContains(t, text, "secret-speech")
+
+ files, err := os.ReadDir(store.dir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+ })
+ }
+}
+
+func TestVoiceCancellationAndCallerDeadlineStopTheProcess(t *testing.T) {
+ store, transcriber := voiceTestInput(t, "exec sleep 60")
+ ctx, cancel := context.WithTimeout(t.Context(), 20*time.Millisecond)
+ defer cancel()
+
+ text := transcriber.Text(ctx, store, []byte("made-up audio"))
+ require.Contains(t, text, "context deadline exceeded")
+
+ ctx, cancel = context.WithCancel(t.Context())
+ cancel()
+
+ text = transcriber.Text(ctx, store, []byte("made-up audio"))
+ require.Contains(t, text, "context canceled")
+}
+
+func TestVoiceUnavailableAndStorageFailuresDoNotStopIntake(t *testing.T) {
+ store, transcriber := voiceTestInput(t, "exit 0")
+ transcriber.config.Model = filepath.Join(t.TempDir(), "missing")
+ text := transcriber.Text(t.Context(), store, []byte("made-up audio"))
+ require.Contains(t, text, "[voice ")
+ require.Contains(t, text, "transcriber or model is not available")
+ require.Equal(t, "(voice not stored)", transcriber.Text(t.Context(), store, nil))
+ require.Equal(t, "(voice not stored)", transcriber.Text(t.Context(), store, bytes.Repeat([]byte{0}, voiceMaxBytes+1)))
+
+ blocked := filepath.Join(t.TempDir(), "blocked")
+ require.NoError(t, os.WriteFile(blocked, nil, 0o600))
+
+ store.dir = filepath.Join(blocked, "child")
+ require.Equal(t, "(voice not stored)", transcriber.Text(t.Context(), store, []byte("audio")))
+}
+
+func TestVoiceLookupHonorsExplicitThenNixThenPath(t *testing.T) {
+ _, transcriber := voiceTestInput(t, "exit 0")
+ valid := transcriber.config
+ t.Setenv("PATH", filepath.Dir(valid.Transcriber))
+
+ testCases := []struct {
+ name string
+ config VoiceConfig
+ program string
+ model string
+ available bool
+ }{
+ {
+ name: "explicit paths override invalid built paths",
+ config: valid,
+ program: "missing",
+ model: "missing",
+ available: true,
+ },
+ {
+ name: "built paths supply absent settings",
+ program: valid.Transcriber,
+ model: valid.Model,
+ available: true,
+ },
+ {
+ name: "PATH supplies absent executable",
+ config: VoiceConfig{Model: valid.Model},
+ available: true,
+ },
+ {
+ name: "invalid explicit executable does not fall back",
+ config: VoiceConfig{Transcriber: "missing", Model: valid.Model},
+ program: valid.Transcriber,
+ },
+ {
+ name: "invalid explicit model does not fall back",
+ config: VoiceConfig{Model: "missing"},
+ program: valid.Transcriber,
+ model: valid.Model,
+ },
+ {
+ name: "missing model leaves input unavailable",
+ program: valid.Transcriber,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ program, model, err := voiceLookup(tc.config, tc.program, tc.model)
+ if !tc.available {
+ require.ErrorIs(t, err, errVoiceUnavailable)
+
+ return
+ }
+
+ require.NoError(t, err)
+ require.Equal(t, valid.Transcriber, program)
+ require.Equal(t, valid.Model, model)
+ })
+ }
+}