ui: move translation credentials into settings

This commit is contained in:
2026-10-17 22:17:21 +02:00
parent b847a483f0
commit 9eb16ddd5a
7 changed files with 186 additions and 86 deletions
@@ -3,7 +3,7 @@ import ConjureKit
import Testing
@testable import Conjure
@Test(arguments: ["launcher", "launcher-actions", "emoji", "emoji-actions", "translate-input", "translate-output", "translate-key", "notes-editor"])
@Test(arguments: ["launcher", "launcher-actions", "emoji", "emoji-actions", "translate-input", "translate-output", "notes-editor"])
@MainActor func escapeDismissesEntireLauncher(_ destination: String) throws {
_ = NSApplication.shared
let suite = "ConjureEscape.\(UUID().uuidString)"
@@ -47,12 +47,6 @@ import Testing
$0.accessibilityLabel() == "Japanese translation"
})
window.makeFirstResponder(output)
} else if destination == "translate-key" {
let provider = try #require(descendants(content).compactMap { $0 as? NSPopUpButton }.first)
provider.selectItem(at: 1)
_ = provider.target?.perform(provider.action, with: provider)
let key = try #require(descendants(content).compactMap { $0 as? NSSecureTextField }.first)
window.makeFirstResponder(key)
} else if destination == "notes-editor" {
let editor = try #require(descendants(content).compactMap { $0 as? NotesTextView }.first)
window.makeFirstResponder(editor)
@@ -56,6 +56,8 @@ private func waitForTranslation(_ condition: () -> Bool) async throws {
@MainActor
private func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
@Suite(.serialized)
struct TranslationExecutionTests {
@Test @MainActor
func translationExecutionIsExplicitAndMyMemoryNeverTouchesCredentials() async throws {
_ = NSApplication.shared
@@ -100,7 +102,7 @@ func translationExecutionRejectsOldCompletionsWithoutClearingNewRequests() async
tool.input = { input }
let views = descendants(tool.view)
let output = try #require(views.compactMap { $0 as? NSTextView }.first)
let provider = try #require(views.compactMap { $0 as? NSPopUpButton }.first)
let provider = tool.providerPicker
let button = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("translate") })
tool.translate()
try await waitForTranslation { requests.pending.count == 1 }
@@ -153,29 +155,39 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
openAI: { try await requests.translate($0, key: $1) })
tool.input = { "Hello" }
let views = descendants(tool.view)
let field = try #require(views.compactMap { $0 as? NSSecureTextField }.first)
let settings = TranslationSettingsController(credentials: keys.store)
let settingsViews = descendants(settings.view)
let field = try #require(settingsViews.compactMap { $0 as? NSSecureTextField }.first)
let output = try #require(views.compactMap { $0 as? NSTextView }.first)
let status = try #require(views.compactMap { $0 as? NSTextField }.first { $0.accessibilityLabel() == "Translation status and errors" })
let button = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("translate") })
let forget = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("forgetKey") })
let save = try #require(settingsViews.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("saveKey") })
let forget = try #require(settingsViews.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("forgetKey") })
let openSettings = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("openSettings") })
tool.translate() // No stored key.
#expect(requests.inputs.isEmpty)
#expect(button.isEnabled)
#expect(status.textColor == .systemRed)
#expect(!openSettings.isHidden)
field.stringValue = "typed-key"
tool.translate() // Typing in Settings must not implicitly save on Translate.
#expect(requests.inputs.isEmpty)
#expect(keys.value == nil)
#expect(field.stringValue == "typed-key")
keys.denied = true
tool.translate() // A failed save must not silently send the typed key.
save.performClick(nil) // A failed save must retain the typed key for retry.
#expect(requests.inputs.isEmpty)
#expect(field.stringValue == "typed-key")
#expect(keys.value == nil)
#expect(status.stringValue.contains("Credential access denied"))
field.stringValue = ""
#expect(settingsViews.compactMap { $0 as? NSTextField }.contains { $0.stringValue.contains("Credential access denied") })
tool.translate() // Failed reads are also recoverable without a request.
#expect(requests.inputs.isEmpty)
#expect(button.isEnabled)
keys.denied = false
field.stringValue = "typed-key"
save.performClick(nil)
#expect(keys.value == "typed-key")
#expect(field.stringValue.isEmpty)
tool.translate()
try await waitForTranslation { requests.pending.count == 1 }
#expect(keys.value == "typed-key")
@@ -184,6 +196,11 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
requests.pending[0].resume(throwing: CredentialFailure())
try await waitForTranslation { button.isEnabled }
#expect(status.textColor == .systemRed)
keys.denied = true
forget.performClick(nil)
#expect(keys.value == "typed-key")
#expect(settingsViews.compactMap { $0 as? NSTextField }.contains { $0.stringValue.contains("Credential access denied") })
keys.denied = false
tool.translate()
try await waitForTranslation { requests.pending.count == 2 }
forget.performClick(nil)
@@ -199,3 +216,4 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
try await waitForTranslation { output.string == "成功" }
#expect(status.textColor != .systemRed)
}
}