package main import ( "strings" "sync/atomic" "testing" "time" "voicesnap/internal/engine" ) 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 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 }