From 24f551a39eae849d891da26cc91f90d354e3a2db Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 12 Jul 2026 02:44:05 +0800
Subject: [PATCH] chore(mas): refresh codebase memory after split
---
C1.source/privatevoice.src/app_live_caption_test.go | 892 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 892 insertions(+), 0 deletions(-)
diff --git a/C1.source/privatevoice.src/app_live_caption_test.go b/C1.source/privatevoice.src/app_live_caption_test.go
index accb99c..1a13c1d 100644
--- a/C1.source/privatevoice.src/app_live_caption_test.go
+++ b/C1.source/privatevoice.src/app_live_caption_test.go
@@ -1,12 +1,19 @@
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) {
@@ -255,6 +262,760 @@
}
}
+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}
@@ -307,3 +1068,134 @@
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() {}
--
Gitblit v1.9.3