//go:build darwin && appstore package textoutput import ( "testing" "voicesnap/internal/axinput" ) type fakeAXDriver struct { directResult axinput.DirectInsertResult fallbackText string copiedText string copyResult axinput.ClipboardResult prepareResult axinput.ClipboardResult prepareCalled bool restoreResult axinput.ClipboardResult restoreToken string restoreCalled bool frontmostIdentity axinput.FrontmostAppIdentity autoPasteResult axinput.AutoPasteResult autoPasteExpected string autoPasteCalled bool } func (d *fakeAXDriver) AccessibilityTrusted(prompt bool) bool { return true } func (d *fakeAXDriver) DirectInsert(text string) axinput.DirectInsertResult { return d.directResult } func (d *fakeAXDriver) PrepareClipboardFallback(text string) axinput.ClipboardResult { d.prepareCalled = true d.fallbackText = text if d.prepareResult.Message != "" || d.prepareResult.OK || d.prepareResult.Prepared || d.prepareResult.Token != "" { return d.prepareResult } return axinput.ClipboardResult{OK: true, Prepared: true, Token: "token"} } func (d *fakeAXDriver) CopyTextToClipboard(text string) axinput.ClipboardResult { d.copiedText = text if d.copyResult.Message != "" || d.copyResult.OK || d.copyResult.Prepared { return d.copyResult } return axinput.ClipboardResult{OK: true, Prepared: true} } func (d *fakeAXDriver) FrontmostAppIdentity() axinput.FrontmostAppIdentity { if d.frontmostIdentity.Message != "" || d.frontmostIdentity.OK { return d.frontmostIdentity } return axinput.FrontmostAppIdentity{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"} } func (d *fakeAXDriver) AutoPasteClipboardIfFrontmostUnchanged(expectedIdentity string) axinput.AutoPasteResult { d.autoPasteCalled = true d.autoPasteExpected = expectedIdentity if d.autoPasteResult.Message != "" || d.autoPasteResult.OK || d.autoPasteResult.Pasted { return d.autoPasteResult } return axinput.AutoPasteResult{OK: true, Pasted: true, FrontmostUnchanged: true} } func (d *fakeAXDriver) RestoreClipboardFallback(token string) axinput.ClipboardResult { d.restoreCalled = true d.restoreToken = token if d.restoreResult.Message != "" || d.restoreResult.OK || d.restoreResult.Restored { return d.restoreResult } return axinput.ClipboardResult{OK: true, Restored: true} } func TestAppStoreRouterUsesVerifiedAXInsert(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: true, Inserted: true, Method: "ax_value_replacement", Message: "verified", Target: "app=TextEdit", }, } router := &Router{driver: driver} result := router.Output("recognized text", false) if !result.OK || !result.DirectInserted || result.FallbackAvailable { t.Fatalf("result = %+v", result) } if result.Text != "recognized text" || result.Target != "app=TextEdit" { t.Fatalf("result = %+v", result) } } func TestAppStoreRouterKeepsTextForExplicitFallback(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_value_replacement", Message: "focused element value is not settable", Target: "app=TextEdit role=AXTextArea", }, } router := &Router{driver: driver} result := router.Output("recognized text", false) if result.OK || !result.FallbackAvailable || result.Text != "recognized text" { t.Fatalf("result = %+v", result) } fallback := router.PrepareLastClipboardFallback() if !fallback.OK || driver.fallbackText != "recognized text" { t.Fatalf("fallback = %+v text=%q", fallback, driver.fallbackText) } } func TestAppStoreRouterBlocksClipboardFallbackForTerminalTarget(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "app=Terminal role=AXWindow", }, } router := &Router{driver: driver} result := router.Output("recognized text", false) if result.OK || result.FallbackAvailable || result.Text != "recognized text" { t.Fatalf("result = %+v", result) } fallback := router.PrepareLastClipboardFallback() if fallback.OK || driver.fallbackText != "" { t.Fatalf("fallback = %+v text=%q, want blocked without clipboard write", fallback, driver.fallbackText) } } func TestAppStoreRouterAllowsClipboardFallbackForUnknownTarget(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, } router := &Router{driver: driver} result := router.Output("recognized text", false) if result.OK || !result.FallbackAvailable || result.Text != "recognized text" { t.Fatalf("result = %+v", result) } fallback := router.PrepareLastClipboardFallback() if !fallback.OK || driver.fallbackText != "recognized text" { t.Fatalf("fallback = %+v text=%q, want clipboard fallback prepared", fallback, driver.fallbackText) } } func TestAppStoreRouterCopiesToClipboardWhenKeepClipboardEnabledAfterAXFailure(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, } router := &Router{driver: driver} result := router.Output("recognized text", true) if result.OK || !result.FallbackAvailable || result.Text != "recognized text" { t.Fatalf("result = %+v", result) } if driver.copiedText != "recognized text" { t.Fatalf("copiedText = %q, want recognized text", driver.copiedText) } } func TestAppStoreRouterAutoPasteExperimentToggleOffDoesNotPaste(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: true, AutoPasteExperiment: false, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.autoPasteCalled { t.Fatal("auto paste called while experiment is disabled") } } func TestAppStoreRouterAutoPasteExperimentPastesAfterClipboardReadback(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, autoPasteResult: axinput.AutoPasteResult{OK: true, Pasted: true, FrontmostUnchanged: true}, } router := &Router{driver: driver} guard := AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: true, AutoPasteExperiment: true, AutoPasteGuard: guard, }) if !result.OK || result.DirectInserted || result.Method != "clipboard_auto_paste_experiment" { t.Fatalf("result = %+v", result) } if driver.copiedText != "recognized text" { t.Fatalf("copiedText = %q, want recognized text", driver.copiedText) } if !driver.autoPasteCalled || driver.autoPasteExpected != guard.Identity { t.Fatalf("autoPasteCalled=%t expected=%q", driver.autoPasteCalled, driver.autoPasteExpected) } } func TestAppStoreRouterAutoPasteExperimentUsesRestoreTokenWhenRetentionOff(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, autoPasteResult: axinput.AutoPasteResult{OK: true, Pasted: true, FrontmostUnchanged: true}, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: false, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if !result.OK || result.Method != "clipboard_auto_paste_experiment" { t.Fatalf("result = %+v", result) } if driver.copiedText != "" { t.Fatalf("copiedText = %q, want no tokenless clipboard write", driver.copiedText) } if driver.fallbackText != "recognized text" || !driver.prepareCalled { t.Fatalf("fallbackText=%q prepareCalled=%t", driver.fallbackText, driver.prepareCalled) } if !driver.autoPasteCalled { t.Fatal("auto paste was not called") } if !driver.restoreCalled || driver.restoreToken != "token" { t.Fatalf("restoreCalled=%t restoreToken=%q", driver.restoreCalled, driver.restoreToken) } } func TestAppStoreRouterAutoPasteExperimentRestoresWhenPasteGuardChangesWithRetentionOff(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, autoPasteResult: axinput.AutoPasteResult{OK: false, Pasted: false, FrontmostUnchanged: false, Message: "frontmost changed"}, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: false, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.copiedText != "" { t.Fatalf("copiedText = %q, want no tokenless clipboard write", driver.copiedText) } if !driver.prepareCalled || driver.fallbackText != "recognized text" { t.Fatalf("prepareCalled=%t fallbackText=%q", driver.prepareCalled, driver.fallbackText) } if !driver.restoreCalled || driver.restoreToken != "token" { t.Fatalf("restoreCalled=%t restoreToken=%q", driver.restoreCalled, driver.restoreToken) } } func TestAppStoreRouterAutoPasteExperimentCancelsWhenRestoreTokenUnavailable(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, prepareResult: axinput.ClipboardResult{OK: true, Prepared: true, Message: "prepared without token"}, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: false, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.autoPasteCalled { t.Fatal("auto paste called without restore token") } if driver.restoreCalled { t.Fatal("restore called without token") } } func TestAppStoreRouterAutoPasteExperimentReportsRestoreSkipWhenClipboardChanged(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, autoPasteResult: axinput.AutoPasteResult{OK: true, Pasted: true, FrontmostUnchanged: true}, restoreResult: axinput.ClipboardResult{OK: false, Restored: false, Message: "clipboard changed after fallback preparation; restore skipped"}, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: false, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if !driver.autoPasteCalled || !driver.restoreCalled { t.Fatalf("autoPasteCalled=%t restoreCalled=%t", driver.autoPasteCalled, driver.restoreCalled) } } func TestAppStoreRouterAutoPasteExperimentBlocksTerminalIdentity(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: true, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.Terminal pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.autoPasteCalled { t.Fatal("auto paste called for terminal identity") } if driver.copiedText != "" || driver.fallbackText != "" || driver.prepareCalled { t.Fatalf("clipboard touched for terminal identity: copied=%q fallback=%q prepareCalled=%t", driver.copiedText, driver.fallbackText, driver.prepareCalled) } } func TestAppStoreRouterAutoPasteExperimentBlocksTerminalCurrentIdentityBeforeClipboardWrite(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, frontmostIdentity: axinput.FrontmostAppIdentity{OK: true, Identity: "bundle=com.apple.Terminal pid=456"}, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: false, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.copiedText != "" || driver.fallbackText != "" || driver.prepareCalled { t.Fatalf("clipboard touched before terminal current identity block: copied=%q fallback=%q prepareCalled=%t", driver.copiedText, driver.fallbackText, driver.prepareCalled) } if driver.autoPasteCalled { t.Fatal("auto paste called after terminal current identity block") } } func TestAppStoreRouterAutoPasteExperimentRequiresClipboardPrepared(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, copyResult: axinput.ClipboardResult{OK: false, Prepared: false, Message: "clipboard write failed"}, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: true, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: true, Identity: "bundle=com.apple.TextEdit pid=123"}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.autoPasteCalled { t.Fatal("auto paste called when clipboard preparation failed") } } func TestAppStoreRouterAutoPasteExperimentRequiresGuard(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "target=unknown", }, } router := &Router{driver: driver} result := router.OutputWithOptions("recognized text", OutputOptions{ KeepClipboard: true, AutoPasteExperiment: true, AutoPasteGuard: AutoPasteGuard{OK: false}, }) if result.OK || !result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.autoPasteCalled { t.Fatal("auto paste called without a frontmost guard") } } func TestAppStoreRouterDoesNotAutoCopyForKnownTerminalTarget(t *testing.T) { driver := &fakeAXDriver{ directResult: axinput.DirectInsertResult{ OK: false, Method: "ax_focused_element", Message: "focused element unavailable", Target: "app=Terminal role=AXWindow", }, } router := &Router{driver: driver} result := router.Output("recognized text", true) if result.OK || result.FallbackAvailable { t.Fatalf("result = %+v", result) } if driver.copiedText != "" { t.Fatalf("copiedText = %q, want no clipboard write for terminal target", driver.copiedText) } } func TestDirectInsertDiagnosticClassifiesFocusedElementUnavailable(t *testing.T) { category, codes := directInsertDiagnostic(axinput.DirectInsertResult{ Message: "focused element unavailable: system=-25204 focused_app=0 focused_app_element=-25204 frontmost=-25204", }) if category != "focused_element_unavailable" { t.Fatalf("category = %q", category) } wantCodes := "system=-25204 focused_app=0 focused_app_element=-25204 frontmost=-25204" if codes != wantCodes { t.Fatalf("codes = %q, want %q", codes, wantCodes) } } func TestDirectInsertDiagnosticDoesNotIncludeFreeText(t *testing.T) { category, codes := directInsertDiagnostic(axinput.DirectInsertResult{ Message: "focused text state is not readable; value=-25204 range=-25205 title=SensitiveWindow transcript=PrivateText", }) if category != "focused_text_state_unreadable" { t.Fatalf("category = %q", category) } wantCodes := "value=-25204 range=-25205" if codes != wantCodes { t.Fatalf("codes = %q, want %q", codes, wantCodes) } }