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 TestVoiceBackupsKeepAudioButNotTemporaryTranscripts(t *testing.T) { root := t.TempDir() store := &VoiceStore{dir: filepath.Join(root, voiceDirName)} name, err := store.Save([]byte("made-up backed-up audio")) require.NoError(t, err) temp := filepath.Join(store.dir, ".transcript-in-progress") require.NoError(t, os.Mkdir(temp, 0o700)) require.NoError(t, os.WriteFile(filepath.Join(temp, "text.txt"), []byte("private temporary text"), 0o600)) target := t.TempDir() require.NoError(t, backupCopy(t.Context(), root, target, filepath.Join(t.TempDir(), "backup.tar.gz"))) require.FileExists(t, filepath.Join(target, voiceDirName, name)) require.NoDirExists(t, filepath.Join(target, voiceDirName, ".transcript-in-progress")) archive := filepath.Join(t.TempDir(), "voice.tar.gz") _, err = backupArchive(t.Context(), target, archive) require.NoError(t, err) restored := t.TempDir() require.NoError(t, backupExtract(t.Context(), archive, restored)) audio, err := os.ReadFile(filepath.Join(restored, voiceDirName, name)) require.NoError(t, err) require.Equal(t, []byte("made-up backed-up audio"), audio) info, err := os.Stat(filepath.Join(restored, voiceDirName, name)) require.NoError(t, err) require.Equal(t, os.FileMode(0o600), info.Mode().Perm()) } func TestVoiceConfigReadsExplicitLocalPaths(t *testing.T) { dir := t.TempDir() configWriteTest(t, dir, `{"voice":{"transcriber":"local-whisper","model":"model.bin"}}`) config, err := loadConfig(dir) require.NoError(t, err) require.Equal(t, VoiceConfig{Transcriber: "local-whisper", Model: "model.bin"}, config.Voice) } 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) }) } }