package main import ( "context" "errors" "strings" "sync/atomic" "testing" "time" "voicesnap/internal/config" "voicesnap/internal/engine" "voicesnap/internal/history" "voicesnap/internal/overlay" "voicesnap/internal/textoutput" "voicesnap/internal/userdict" ) func TestLiveCaptionDisplayTextFitsOverlayBuffer(t *testing.T) { input := strings.Repeat("这是一句实时字幕", 12) got := liveCaptionDisplayText(input) if got == "" { t.Fatal("caption should not be empty") } if len([]byte(got)) > liveCaptionMaxBytes { t.Fatalf("caption bytes = %d, want <= %d", len([]byte(got)), liveCaptionMaxBytes) } if !strings.HasPrefix(got, "...") { t.Fatalf("caption = %q, want leading truncation marker", got) } } func TestReleaseTailCaptureDelayUsesEngineCapability(t *testing.T) { a := &App{} a.replaceEngine(tailCaptureTestEngine{delay: 300 * time.Millisecond}) if got := a.releaseTailCaptureDelay(); got != 300*time.Millisecond { t.Fatalf("releaseTailCaptureDelay() = %v, want 300ms", got) } } func TestReleaseTailCaptureDelayCanBeDisabledByEngineCapability(t *testing.T) { a := &App{} a.replaceEngine(tailCaptureTestEngine{delay: 0}) if got := a.releaseTailCaptureDelay(); got != 0 { t.Fatalf("releaseTailCaptureDelay() = %v, want 0", got) } } func TestReleaseTailCaptureDelayDefaultsToNoProtectionWindow(t *testing.T) { a := &App{} a.replaceEngine(plainTestEngine{}) if got := a.releaseTailCaptureDelay(); got != 0 { t.Fatalf("releaseTailCaptureDelay() = %v, want 0", got) } } func TestReleaseTailCaptureDelayDisabledWithoutEngine(t *testing.T) { a := &App{} a.replaceEngine(nil) if got := a.releaseTailCaptureDelay(); got != 0 { t.Fatalf("releaseTailCaptureDelay() = %v, want 0 without engine", got) } } func TestSlowRecognitionReason(t *testing.T) { tests := []struct { name string recognizeMS int64 totalMS int64 want string }{ {name: "fast", recognizeMS: 2000, totalMS: 2500, want: ""}, {name: "recognize", recognizeMS: 2001, totalMS: 2400, want: "recognize"}, {name: "total", recognizeMS: 1000, totalMS: 2501, want: "total"}, {name: "both", recognizeMS: 2001, totalMS: 2501, want: "recognize,total"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := slowRecognitionReason(tt.recognizeMS, tt.totalMS); got != tt.want { t.Fatalf("slowRecognitionReason(%d, %d) = %q, want %q", tt.recognizeMS, tt.totalMS, got, tt.want) } }) } } func TestHoldPreCaptureUsesEngineCapability(t *testing.T) { a := &App{} a.replaceEngine(holdPreCaptureTestEngine{enabled: true}) if !a.holdPreCaptureEnabled.Load() { t.Fatal("hold pre-capture should be enabled by engine capability") } } func TestHoldPreCaptureCanBeDisabledByEngineCapability(t *testing.T) { a := &App{} a.replaceEngine(holdPreCaptureTestEngine{enabled: false}) if a.holdPreCaptureEnabled.Load() { t.Fatal("hold pre-capture should respect engine capability false") } } func TestHoldPreCaptureDefaultsToTrueForReadyEngine(t *testing.T) { a := &App{} a.replaceEngine(plainTestEngine{}) if !a.holdPreCaptureEnabled.Load() { t.Fatal("hold pre-capture should default to true for ready engines") } } func TestHoldPreCaptureDisabledWithoutEngine(t *testing.T) { a := &App{} a.replaceEngine(nil) if a.holdPreCaptureEnabled.Load() { t.Fatal("hold pre-capture should be disabled without an engine") } } func TestHasEngineReflectsCurrentEngine(t *testing.T) { a := &App{} if a.hasEngine() { t.Fatal("new app should not report an engine") } a.replaceEngine(plainTestEngine{}) if !a.hasEngine() { t.Fatal("app should report an engine after replaceEngine") } a.replaceEngine(nil) if a.hasEngine() { t.Fatal("app should not report an engine after clearing") } } func TestInitEngineSerializesAndDiscardsStaleEngine(t *testing.T) { first := &closeTrackingTestEngine{name: "first"} second := &closeTrackingTestEngine{name: "second"} firstStarted := make(chan struct{}) allowFirst := make(chan struct{}) calls := atomic.Int32{} a := &App{ engineFactory: func(initID uint64) (engine.Engine, engineMetadata, bool, error) { call := calls.Add(1) switch call { case 1: close(firstStarted) <-allowFirst return first, engineMetadata{InitID: initID, ResolvedModelID: "first", HardwareInfo: "first"}, true, nil case 2: return second, engineMetadata{InitID: initID, ResolvedModelID: "second", HardwareInfo: "second"}, true, nil default: t.Fatalf("unexpected engine factory call %d", call) return nil, engineMetadata{}, false, nil } }, } doneFirst := make(chan struct{}) go func() { a.initEngine() close(doneFirst) }() select { case <-firstStarted: case <-time.After(time.Second): t.Fatal("first engine init did not start") } doneSecond := make(chan struct{}) go func() { a.initEngine() close(doneSecond) }() waitForCondition(t, time.Second, func() bool { return a.engineInitSeq.Load() == 2 }) close(allowFirst) select { case <-doneFirst: case <-time.After(time.Second): t.Fatal("first engine init did not finish") } select { case <-doneSecond: case <-time.After(time.Second): t.Fatal("second engine init did not finish") } a.engineMu.Lock() gotEngine := a.eng gotMeta := a.engineMeta a.engineMu.Unlock() if gotEngine != second { t.Fatalf("final engine = %#v, want second", gotEngine) } if !first.closed.Load() { t.Fatal("stale first engine should be closed") } if second.closed.Load() { t.Fatal("current second engine should remain open") } if gotMeta.ResolvedModelID != "second" || gotMeta.InitID != 2 { t.Fatalf("engine metadata = %+v, want second init metadata", gotMeta) } } func waitForCondition(t *testing.T, timeout time.Duration, fn func() bool) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if fn() { return } time.Sleep(10 * time.Millisecond) } t.Fatal("condition was not met before timeout") } func TestShouldStartHoldPreCaptureLocked(t *testing.T) { a := &App{} a.holdPreCaptureEnabled.Store(true) if !a.shouldStartHoldPreCaptureLocked() { t.Fatal("expected hold pre-capture to start when enabled and idle") } a.isRecording = true if a.shouldStartHoldPreCaptureLocked() { t.Fatal("should not pre-capture while already recording") } a.isRecording = false a.isStoppingRecording = true if a.shouldStartHoldPreCaptureLocked() { t.Fatal("should not pre-capture while stopping") } a.isStoppingRecording = false a.isFreetalking = true if a.shouldStartHoldPreCaptureLocked() { t.Fatal("should not pre-capture while tap recording is active") } a.isFreetalking = false a.lastStopTime = time.Now() if a.shouldStartHoldPreCaptureLocked() { t.Fatal("should not pre-capture during post-stop debounce") } } func TestHoldPreCaptureAutoConfirmsWithoutAdditionalKeyDown(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() a.hk.(*fakeHotkeyListener).down.Store(true) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.hotkeyActive }) if got := a.recorder.(*fakeAppRecorder).starts.Load(); got != 1 { t.Fatalf("recorder starts = %d, want 1", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusRecording) { t.Fatalf("indicator status = %q, want %q", got, overlay.StatusRecording) } } func TestHoldPreCaptureTimerConfirmsWhileRecorderStartBlocked(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() a.hk.(*fakeHotkeyListener).down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) recorder.startBlock = blockStart a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return recorder.starts.Load() == 1 }) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.hotkeyActive }) if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusRecording) { t.Fatalf("indicator status = %q, want %q while recorder start is blocked", got, overlay.StatusRecording) } close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isHoldRecorderStarting }) } func TestHoldPreCaptureReleaseAfterConfirmRunsRecognitionWhileRecorderStartBlocked(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) recorder.startBlock = blockStart recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} recorder.hasVoice.Store(true) eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return paster.pasted.Load() == "PrivateVoice AX POC" }) if got := recorder.stopAndGetSamples.Load(); got != 1 { t.Fatalf("StopAndGetSamples while recorder start is blocked = %d, want 1", got) } if got := eng.recognizes.Load(); got != 1 { t.Fatalf("recognizes = %d, want 1", got) } a.mu.Lock() if a.isHoldStopPending { t.Fatal("normal release while recorder is starting should not mark cancel-style pending stop") } a.mu.Unlock() close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) if got := recorder.stops.Load(); got != 0 { t.Fatalf("app-level fake recorder stops = %d, want 0; native cleanup is covered by audio recorder tests", got) } } func TestHoldPreCaptureReleaseAfterConfirmRunsRecognitionBeforeRecorderStartEntered(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) preStartBlock := make(chan struct{}) recorder.preStartBlock = preStartBlock recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} recorder.hasVoice.Store(true) eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) if got := recorder.starts.Load(); got != 0 { t.Fatalf("recorder Start entered before release = %d, want 0", got) } listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return paster.pasted.Load() == "PrivateVoice AX POC" }) if got := recorder.stopAndGetSamples.Load(); got != 1 { t.Fatalf("StopAndGetSamples before recorder Start entered = %d, want 1", got) } if got := eng.recognizes.Load(); got != 1 { t.Fatalf("recognizes = %d, want 1", got) } close(preStartBlock) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) if got := recorder.stops.Load(); got != 0 { t.Fatalf("app-level fake recorder stops = %d, want 0 for normal release pipeline", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusDone) { t.Fatalf("indicator status = %q, want %q", got, overlay.StatusDone) } } func TestHoldPreCaptureReleaseWaitsForStartupSamplesBeforeRecognition(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) readSamplesReady := make(chan struct{}) recorder.startBlock = blockStart recorder.readSamplesBlock = readSamplesReady recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} recorder.hasVoice.Store(true) eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() time.Sleep(holdStartupSamplePollInterval * 2) if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples before startup samples = %d, want 0", got) } if got := eng.recognizes.Load(); got != 0 { t.Fatalf("recognizes before startup samples = %d, want 0", got) } close(readSamplesReady) waitForCondition(t, time.Second, func() bool { return paster.pasted.Load() == "PrivateVoice AX POC" }) if got := recorder.stopAndGetSamples.Load(); got != 1 { t.Fatalf("StopAndGetSamples after startup samples = %d, want 1", got) } if got := eng.recognizes.Load(); got != 1 { t.Fatalf("recognizes = %d, want 1", got) } close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) } func TestHoldPreCaptureReleaseWithoutStartupSamplesReportsAudioNotReady(t *testing.T) { oldTimeout := holdStartupSampleWaitTimeout oldPoll := holdStartupSamplePollInterval holdStartupSampleWaitTimeout = 40 * time.Millisecond holdStartupSamplePollInterval = 5 * time.Millisecond defer func() { holdStartupSampleWaitTimeout = oldTimeout holdStartupSamplePollInterval = oldPoll }() a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) recorder.startBlock = blockStart eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return recorder.stops.Load() == 1 && a.indicator.(*fakeOverlay).lastStatus.Load() == string(overlay.StatusError) }) if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples after audio-not-ready timeout = %d, want 0", got) } if got := eng.recognizes.Load(); got != 0 { t.Fatalf("recognizes after audio-not-ready timeout = %d, want 0", got) } if got := paster.pasted.Load(); got != nil { t.Fatalf("pasted text after audio-not-ready timeout = %v, want nil", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusError) { t.Fatalf("indicator status = %q, want %q", got, overlay.StatusError) } close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) } func TestHoldPreCaptureReleaseWhileRecorderPermissionPendingDoesNotRecognize(t *testing.T) { oldTimeout := holdStartupSampleWaitTimeout oldPoll := holdStartupSamplePollInterval holdStartupSampleWaitTimeout = 40 * time.Millisecond holdStartupSamplePollInterval = 5 * time.Millisecond defer func() { holdStartupSampleWaitTimeout = oldTimeout holdStartupSamplePollInterval = oldPoll }() a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) preStartBlock := make(chan struct{}) recorder.preStartBlock = preStartBlock eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) if got := recorder.starts.Load(); got != 0 { t.Fatalf("recorder Start entered before permission pending release = %d, want 0", got) } listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return recorder.stops.Load() == 1 && a.indicator.(*fakeOverlay).lastStatus.Load() == string(overlay.StatusError) }) if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples after permission-pending audio-not-ready = %d, want 0", got) } if got := eng.recognizes.Load(); got != 0 { t.Fatalf("recognizes after permission-pending audio-not-ready = %d, want 0", got) } if got := paster.pasted.Load(); got != nil { t.Fatalf("pasted text after permission-pending audio-not-ready = %v, want nil", got) } close(preStartBlock) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) if got := recorder.starts.Load(); got != 1 { t.Fatalf("recorder Start after permission resumes = %d, want 1", got) } if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder stops after late permission success = %d, want 1", got) } } func TestHoldPreCaptureEscapeAfterConfirmDefersStopUntilRecorderStart(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) recorder.startBlock = blockStart captionCancelled := false a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) a.mu.Lock() a.liveCaptionCancel = func() { captionCancelled = true } a.mu.Unlock() listener.escapeDown.Store(true) done := make(chan struct{}) go func() { a.pollHotkey() close(done) }() select { case <-done: case <-time.After(100 * time.Millisecond): t.Fatal("Escape cancel should not wait for blocked recorder start") } if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder stops before start returned = %d, want 1", got) } if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples before recorder start returned = %d, want 0", got) } a.mu.Lock() if !a.isHoldStopPending { t.Fatal("Escape while recorder is starting should mark pending stop") } if a.liveCaptionCancel != nil { t.Fatal("Escape while recorder is starting should clear live caption") } a.mu.Unlock() if !captionCancelled { t.Fatal("Escape while recorder is starting should cancel live caption") } close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples after deferred Escape stop = %d, want 0", got) } if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder stops = %d, want 1", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusCancelled) { t.Fatalf("indicator status = %q, want %q", got, overlay.StatusCancelled) } } func TestPollHotkeyIgnoresUnsetHotkey(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) a.cfg.HotkeyVK = config.UnsetHotkeyVK a.pollHotkey() if got := listener.keyChecks.Load(); got != 0 { t.Fatalf("IsKeyDown calls for unset hotkey = %d, want 0", got) } if a.hotkeyActive { t.Fatal("unset hotkey must not become active") } if a.isRecording || a.isFreetalking { t.Fatal("unset hotkey must not start recording") } } func TestHoldPreCaptureEscapeWhileRecorderPermissionPendingStopsLateStart(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) preStartBlock := make(chan struct{}) recorder.preStartBlock = preStartBlock eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) if got := recorder.starts.Load(); got != 0 { t.Fatalf("recorder Start entered before Escape during permission pending = %d, want 0", got) } listener.escapeDown.Store(true) done := make(chan struct{}) go func() { a.pollHotkey() close(done) }() select { case <-done: case <-time.After(100 * time.Millisecond): t.Fatal("Escape during permission pending should not wait for recorder Start") } if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder Stop during permission-pending Escape = %d, want 1", got) } if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples during permission-pending Escape = %d, want 0", got) } if got := eng.recognizes.Load(); got != 0 { t.Fatalf("recognizes during permission-pending Escape = %d, want 0", got) } close(preStartBlock) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder stops after late permission success = %d, want 1", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusCancelled) { t.Fatalf("indicator status = %q, want %q", got, overlay.StatusCancelled) } } func TestHoldPreCaptureCombinationWhileRecorderPermissionPendingStopsLateStart(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) preStartBlock := make(chan struct{}) recorder.preStartBlock = preStartBlock eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() listener.otherSince.Store(true) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isCombination && !a.isRecording && !a.isHoldRecordingPending && a.isHoldStopPending }) if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder Stop during permission-pending combination cancel = %d, want 1", got) } if got := recorder.stopAndGetSamples.Load(); got != 0 { t.Fatalf("StopAndGetSamples during permission-pending combination cancel = %d, want 0", got) } if got := eng.recognizes.Load(); got != 0 { t.Fatalf("recognizes during permission-pending combination cancel = %d, want 0", got) } close(preStartBlock) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending }) if got := recorder.stops.Load(); got != 1 { t.Fatalf("recorder stops after late combination permission success = %d, want 1", got) } } func TestHoldPreCaptureReleaseAfterRecorderStartRunsRecognitionAndOutput(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} recorder.hasVoice.Store(true) eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && !a.isHoldRecorderStarting }) listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return paster.pasted.Load() == "PrivateVoice AX POC" }) if got := recorder.stopAndGetSamples.Load(); got != 1 { t.Fatalf("StopAndGetSamples = %d, want 1", got) } if got := eng.recognizes.Load(); got != 1 { t.Fatalf("recognizes = %d, want 1", got) } if got := recorder.stops.Load(); got != 0 { t.Fatalf("recorder Stop = %d, want 0 for normal recognition path", got) } } func TestHoldPreCaptureReleasePipelineSuppressesLateRecorderStartError(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() listener := a.hk.(*fakeHotkeyListener) listener.down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) recorder.startBlock = blockStart recorder.startErr = errors.New("start failed") recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} recorder.hasVoice.Store(true) eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} a.replaceEngine(eng) paster := &fakeAppPaster{} a.outputRouter = textoutput.NewRouter(paster) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting }) listener.down.Store(false) a.mu.Lock() a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { return paster.pasted.Load() == "PrivateVoice AX POC" }) close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isHoldRecorderStarting }) if got := eng.recognizes.Load(); got != 1 { t.Fatalf("recognizes = %d, want 1", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusDone) { t.Fatalf("late recorder start error overwrote indicator status = %q, want %q", got, overlay.StatusDone) } } func TestHoldPreCaptureRecorderStartLateErrorCleansConfirmedState(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() a.hk.(*fakeHotkeyListener).down.Store(true) recorder := a.recorder.(*fakeAppRecorder) blockStart := make(chan struct{}) recorder.startBlock = blockStart recorder.startErr = errors.New("start failed") captionCancelled := false a.liveCaptionCancel = func() { captionCancelled = true } a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting && a.liveCaptionCancel != nil }) close(blockStart) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecorderStarting && a.liveCaptionCancel == nil }) if !captionCancelled { t.Fatal("late recorder start error should cancel live caption") } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusError) { t.Fatalf("indicator status = %q, want %q", got, overlay.StatusError) } } func TestHoldPreCaptureReleaseBeforeActivationCancelsTimer(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() a.hk.(*fakeHotkeyListener).down.Store(true) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.hk.(*fakeHotkeyListener).down.Store(false) a.pollHoldHotkeyLocked(false, true) a.mu.Unlock() time.Sleep(holdActivationDelay + 50*time.Millisecond) a.mu.Lock() defer a.mu.Unlock() if a.isRecording || a.isHoldRecordingPending { t.Fatalf("recording=%t pending=%t, want both false after early release", a.isRecording, a.isHoldRecordingPending) } if got := a.recorder.(*fakeAppRecorder).stops.Load(); got != 1 { t.Fatalf("recorder stops = %d, want 1", got) } } func TestHoldPreCaptureTimerCancelsWhenKeyReleasedBeforePoll(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() a.hk.(*fakeHotkeyListener).down.Store(true) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() a.hk.(*fakeHotkeyListener).down.Store(false) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return !a.isRecording && !a.isHoldRecordingPending }) a.mu.Lock() defer a.mu.Unlock() if a.hotkeyActive != true { t.Fatal("hotkeyActive should remain true until release poll handles the key-up") } if got := a.recorder.(*fakeAppRecorder).stops.Load(); got != 1 { t.Fatalf("recorder stops = %d, want 1", got) } if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got == string(overlay.StatusRecording) { t.Fatalf("indicator status = %q, should not confirm recording after physical release", got) } } func TestHoldPreCaptureCombinationBeforeActivationCancels(t *testing.T) { a, cleanup := newHoldPreCaptureTestApp() defer cleanup() a.hk.(*fakeHotkeyListener).down.Store(true) a.mu.Lock() a.pollHoldHotkeyLocked(true, true) a.mu.Unlock() a.hk.(*fakeHotkeyListener).otherSince.Store(true) waitForCondition(t, time.Second, func() bool { a.mu.Lock() defer a.mu.Unlock() return a.isCombination && !a.isRecording && !a.isHoldRecordingPending }) if got := a.recorder.(*fakeAppRecorder).stops.Load(); got != 1 { t.Fatalf("recorder stops = %d, want 1", got) } } func TestStartRecordingIgnoredWhileStoppingRecording(t *testing.T) { a := &App{isStoppingRecording: true} a.startRecordingLocked() if a.isRecording { t.Fatal("startRecordingLocked should not start while stop tail capture is pending") } } func TestStartTapRecordingIgnoredWhileStoppingRecording(t *testing.T) { a := &App{isStoppingRecording: true} a.startTapRecordingLocked() if a.isRecording || a.isFreetalking { t.Fatal("startTapRecordingLocked should not start while stop tail capture is pending") } } type plainTestEngine struct{} func (plainTestEngine) Recognize([]float32) (string, error) { return "", nil } func (plainTestEngine) HardwareInfo() string { return "test" } func (plainTestEngine) Close() {} type closeTrackingTestEngine struct { plainTestEngine name string closed atomic.Bool } func (e *closeTrackingTestEngine) HardwareInfo() string { return e.name } func (e *closeTrackingTestEngine) Close() { e.closed.Store(true) } type tailCaptureTestEngine struct { plainTestEngine delay time.Duration } func (e tailCaptureTestEngine) ReleaseTailCaptureDelay() time.Duration { return e.delay } type holdPreCaptureTestEngine struct { plainTestEngine enabled bool } func (e holdPreCaptureTestEngine) HoldPreCaptureEnabled() bool { return e.enabled } func newHoldPreCaptureTestApp() (*App, context.CancelFunc) { ctx, cancel := context.WithCancel(context.Background()) a := &App{ ctx: ctx, cancel: cancel, cfg: &config.Config{HotkeyVK: 0x5C, HotkeyMode: config.HotkeyModeHold}, recorder: &fakeAppRecorder{}, hk: &fakeHotkeyListener{}, indicator: &fakeOverlay{}, history: history.New(), userdict: userdict.New(), } a.replaceEngine(holdPreCaptureTestEngine{enabled: true}) a.holdPreCaptureEnabled.Store(true) return a, cancel } type fakeAppRecorder struct { starts atomic.Int32 stops atomic.Int32 stopAndGetSamples atomic.Int32 hasVoice atomic.Bool samples []float32 preStartBlock <-chan struct{} startBlock <-chan struct{} readSamplesBlock <-chan struct{} startErr error } func (r *fakeAppRecorder) OnVolume(func(float64)) {} func (r *fakeAppRecorder) OnDeviceChange(func(string)) {} func (r *fakeAppRecorder) Start() error { if r.preStartBlock != nil { <-r.preStartBlock } r.starts.Add(1) if r.startBlock != nil { <-r.startBlock } return r.startErr } func (r *fakeAppRecorder) Stop() { r.stops.Add(1) } func (r *fakeAppRecorder) StopAndGetSamples() []float32 { r.stopAndGetSamples.Add(1) return append([]float32(nil), r.samples...) } func (r *fakeAppRecorder) HasVoiceActivity() bool { return r.hasVoice.Load() } func (r *fakeAppRecorder) ReadSamplesSince(offset int) ([]float32, int) { if r.readSamplesBlock != nil { select { case <-r.readSamplesBlock: default: return nil, 0 } } total := len(r.samples) if offset < 0 { offset = 0 } if offset > total { offset = total } return append([]float32(nil), r.samples[offset:]...), total } func (r *fakeAppRecorder) Close() {} type recognizingHoldEngine struct { text string recognizes atomic.Int32 } func (e *recognizingHoldEngine) Recognize([]float32) (string, error) { e.recognizes.Add(1) return e.text, nil } func (e *recognizingHoldEngine) HardwareInfo() string { return "test" } func (e *recognizingHoldEngine) Close() {} func (e *recognizingHoldEngine) HoldPreCaptureEnabled() bool { return true } type fakeAppPaster struct { pasted atomic.Value typed atomic.Value } func (p *fakeAppPaster) Paste(text string, keepClipboard bool) error { p.pasted.Store(text) return nil } func (p *fakeAppPaster) TypeText(text string) error { p.typed.Store(text) return nil } type fakeHotkeyListener struct { down atomic.Bool escapeDown atomic.Bool otherSince atomic.Bool keyChecks atomic.Int32 } func (h *fakeHotkeyListener) IsKeyDown(key int) bool { h.keyChecks.Add(1) if key == 0x1B { return h.escapeDown.Load() } return h.down.Load() } func (h *fakeHotkeyListener) IsAnyOtherKeyPressed(int) bool { return false } func (h *fakeHotkeyListener) IsAnyOtherKeyPressedSince(int, time.Time) bool { return h.otherSince.Load() } func (h *fakeHotkeyListener) Close() {} type fakeOverlay struct { lastStatus atomic.Value } func (o *fakeOverlay) Show() {} func (o *fakeOverlay) Hide() {} func (o *fakeOverlay) SetStatus(status overlay.Status, text string) { o.lastStatus.Store(string(status)) } func (o *fakeOverlay) SetVolume(float64) {} func (o *fakeOverlay) SetPosition(int, int) {} func (o *fakeOverlay) GetPosition() (int, int) { return 0, 0 } func (o *fakeOverlay) Size() (int, int) { return 170, 48 } func (o *fakeOverlay) OnDragged(func(int, int)) {} func (o *fakeOverlay) Close() {} func (o *fakeOverlay) AutoPosition() {}