feat(keyboard): unify assistant voice and AI workflows
Merge dictation and AI controls into one assistant surface, preserve safe insertion and clipboard actions, and align settings and tests with the new flow.
This commit is contained in:
@@ -43,7 +43,9 @@ final class AIQuestionServiceTests: XCTestCase {
|
||||
)
|
||||
|
||||
XCTAssertEqual(client.lastMessages?.last, .user(rawQuestion))
|
||||
XCTAssertTrue(client.lastMessages?.first?.content.contains("Reply in English.") == true)
|
||||
let systemPrompt = client.lastMessages?.first?.content ?? ""
|
||||
XCTAssertTrue(systemPrompt.contains("Reply in English"))
|
||||
XCTAssertTrue(systemPrompt.contains("explicitly asks for another output language"))
|
||||
}
|
||||
|
||||
func testAnswerDoesNotEnterContextUntilHostCommitsTerminalResult() async throws {
|
||||
|
||||
@@ -83,6 +83,22 @@ final class AISessionStateTests: XCTestCase {
|
||||
XCTAssertEqual(state.answer?.isSent, false)
|
||||
}
|
||||
|
||||
func testDiscardReadyAnswerKeepsConversationActive() {
|
||||
var state = AISessionState()
|
||||
let conversationID = UUID()
|
||||
let utteranceID = UUID()
|
||||
state.enter(conversationID: conversationID)
|
||||
state.beginPreparing(utteranceID: utteranceID)
|
||||
state.receiveAnswer("光标变化后保留的回答", utteranceID: utteranceID)
|
||||
|
||||
state.discardReadyAnswer()
|
||||
|
||||
XCTAssertEqual(state.phase, .idle)
|
||||
XCTAssertEqual(state.conversationID, conversationID)
|
||||
XCTAssertNil(state.answer)
|
||||
XCTAssertFalse(state.canInsert)
|
||||
}
|
||||
|
||||
func testCancellationRestoresPendingSendState() {
|
||||
var state = AISessionState()
|
||||
let firstUtteranceID = UUID()
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
// CursorNavigationTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class CursorNavigationTests: XCTestCase {
|
||||
|
||||
/// Uses the built-in 1/2-unit width table. `lineWidth` is therefore in
|
||||
/// "units" (≈ Latin characters) for these tests.
|
||||
private func config(lineWidth: CGFloat) -> CursorNavigation.VisualLineLayoutConfig {
|
||||
CursorNavigation.VisualLineLayoutConfig(lineWidth: lineWidth)
|
||||
}
|
||||
|
||||
func testColumnOnFirstLine() {
|
||||
XCTAssertEqual(CursorNavigation.column(before: "hello"), 5)
|
||||
XCTAssertEqual(CursorNavigation.column(before: nil), 0)
|
||||
}
|
||||
|
||||
func testColumnAfterNewline() {
|
||||
XCTAssertEqual(CursorNavigation.column(before: "hello\nwor"), 3)
|
||||
}
|
||||
|
||||
func testDefaultDisplayWidthLatinAndCJK() {
|
||||
XCTAssertEqual(CursorNavigation.defaultDisplayWidth("a"), 1)
|
||||
XCTAssertEqual(CursorNavigation.defaultDisplayWidth("中"), 2)
|
||||
XCTAssertEqual(CursorNavigation.defaultDisplayWidth("\n"), 0)
|
||||
}
|
||||
|
||||
func testVisualLineDownAcrossSoftWrap() {
|
||||
// 20 Latin chars, wrap at 16 → line0 [0,16), line1 [16,20).
|
||||
let text = String(repeating: "a", count: 20)
|
||||
let before = String(text.prefix(8))
|
||||
let after = String(text.suffix(12))
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 16)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
// Sticky column 8; line1 only has 4 units → clamp to its end.
|
||||
XCTAssertEqual(result?.offset, 12)
|
||||
}
|
||||
|
||||
func testVisualLineDownToShorterWrappedLineClampsToEnd() {
|
||||
// Caret at col 15 (clearly on line0); line1 has only 4 chars.
|
||||
let text = String(repeating: "a", count: 20)
|
||||
let before = String(text.prefix(15))
|
||||
let after = String(text.suffix(5))
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: 15,
|
||||
config: config(lineWidth: 16)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.offset, 5)
|
||||
XCTAssertEqual(result?.stickyColumn, 15)
|
||||
}
|
||||
|
||||
func testVisualLineUpAcrossSoftWrap() {
|
||||
let text = String(repeating: "a", count: 20)
|
||||
let before = String(text.prefix(18))
|
||||
let after = String(text.suffix(2))
|
||||
|
||||
let result = CursorNavigation.visualLineUpOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: 8,
|
||||
config: config(lineWidth: 16)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.offset, -10)
|
||||
}
|
||||
|
||||
func testVisualLineDownAcrossHardNewline() {
|
||||
let before = "hello\nwor"
|
||||
let after = "ld\nfoo"
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
// "hello\nworld\nfoo" — caret before "ld"; column 3 lands after "foo".
|
||||
XCTAssertEqual(result?.offset, 6)
|
||||
}
|
||||
|
||||
func testVisualLineDownPreservesStickyColumnOnLongerNextLine() {
|
||||
let before = "hello\nwor"
|
||||
let after = "ld\nfoobarbaz"
|
||||
|
||||
let result = CursorNavigation.visualLineDownOffset(
|
||||
before: before,
|
||||
after: after,
|
||||
preferredDisplayColumn: 5,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
XCTAssertNotNil(result)
|
||||
// "ld\n" (3) + column 5 on "foobarbaz" = 8 total from cursor.
|
||||
XCTAssertEqual(result?.offset, 8)
|
||||
XCTAssertEqual(result?.stickyColumn, 5)
|
||||
}
|
||||
|
||||
func testVisualLineUpOnFirstLineReturnsNil() {
|
||||
XCTAssertNil(
|
||||
CursorNavigation.visualLineUpOffset(
|
||||
before: "hello",
|
||||
after: " world",
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testVisualLineDownWithNoFollowingTextReturnsNil() {
|
||||
XCTAssertNil(
|
||||
CursorNavigation.visualLineDownOffset(
|
||||
before: "hello",
|
||||
after: nil,
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
)
|
||||
XCTAssertNil(
|
||||
CursorNavigation.visualLineDownOffset(
|
||||
before: "hello",
|
||||
after: "",
|
||||
preferredDisplayColumn: nil,
|
||||
config: config(lineWidth: 100)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,31 @@ final class EditableInputReferenceTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testLiveInsertionSurvivesDelayedFieldFingerprintRefresh() {
|
||||
let instanceID = UUID()
|
||||
let reference = EditableInputReference(
|
||||
displayText: "hello",
|
||||
insertedText: " hello",
|
||||
postInsertionFingerprint: "fingerprint-before-refresh",
|
||||
extensionInstanceID: instanceID
|
||||
)
|
||||
|
||||
XCTAssertTrue(
|
||||
reference.matchesLiveInsertion(
|
||||
extensionInstanceID: instanceID,
|
||||
lastInsertedText: " hello",
|
||||
contextBeforeInput: "prefix hello"
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
reference.matchesLiveInsertion(
|
||||
extensionInstanceID: UUID(),
|
||||
lastInsertedText: " hello",
|
||||
contextBeforeInput: "prefix hello"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testStoreRoundTripAndExpiryCleanup() throws {
|
||||
let suite = "EditableInputReferenceTests.\(UUID().uuidString)"
|
||||
let defaults = try XCTUnwrap(UserDefaults(suiteName: suite))
|
||||
|
||||
Reference in New Issue
Block a user