//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)
|
}
|
}
|