b275b6b0d9
- Cover reply-variant decision paths, scene-detector abstention, and the merged reply center stance catalog. - Lock in clipboard semantic analyzer thresholds, complaint-only suppression, and existing-skill routing. - Add durable session, refresh, and managed-gateway reliability tests for the Apple account path. - Verify personal-style prompt derivation, low-confidence ASR tendencies, and Polish style learning against real corpus evidence. - Exercise the assistant keyboard and Polish styles UI flows in end-to-end UI tests.
267 lines
9.7 KiB
Swift
267 lines
9.7 KiB
Swift
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class AISessionStateTests: XCTestCase {
|
|
func testSuccessfulAnswerReplacesPreviousOnlyAtTerminalResult() throws {
|
|
var state = AISessionState()
|
|
let conversationID = UUID()
|
|
let firstUtteranceID = UUID()
|
|
state.enter(conversationID: conversationID)
|
|
state.beginPreparing(utteranceID: firstUtteranceID)
|
|
state.beginListening(utteranceID: firstUtteranceID)
|
|
state.beginRecognizing(utteranceID: firstUtteranceID)
|
|
state.beginGenerating(question: "问题一", utteranceID: firstUtteranceID)
|
|
state.receiveAnswer("答案一", utteranceID: firstUtteranceID)
|
|
|
|
let secondUtteranceID = UUID()
|
|
state.beginPreparing(utteranceID: secondUtteranceID)
|
|
XCTAssertEqual(state.answer?.text, "答案一")
|
|
|
|
state.beginListening(utteranceID: secondUtteranceID)
|
|
state.beginGenerating(question: "问题二", utteranceID: secondUtteranceID)
|
|
XCTAssertEqual(state.answer?.text, "答案一")
|
|
|
|
state.receiveAnswer("答案二", utteranceID: secondUtteranceID)
|
|
XCTAssertEqual(state.answer?.text, "答案二")
|
|
XCTAssertTrue(state.canInsert)
|
|
}
|
|
|
|
func testCancellationRestoresPreviousAnswerState() {
|
|
var state = AISessionState()
|
|
let firstUtteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: firstUtteranceID)
|
|
state.beginListening(utteranceID: firstUtteranceID)
|
|
state.receiveAnswer("可用答案", utteranceID: firstUtteranceID)
|
|
|
|
let secondUtteranceID = UUID()
|
|
state.beginPreparing(utteranceID: secondUtteranceID)
|
|
state.updateTranscript("未完成问题", utteranceID: secondUtteranceID)
|
|
state.cancelCurrentWork()
|
|
|
|
XCTAssertEqual(state.phase, .ready)
|
|
XCTAssertEqual(state.answer?.text, "可用答案")
|
|
XCTAssertEqual(state.transcript, "")
|
|
}
|
|
|
|
func testAnswerRequiresInsertBeforeSend() {
|
|
var state = AISessionState()
|
|
let utteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: utteranceID)
|
|
state.receiveAnswer("答案", utteranceID: utteranceID)
|
|
|
|
XCTAssertTrue(state.canInsert)
|
|
XCTAssertFalse(state.canSend)
|
|
|
|
state.markAnswerInserted(offersSend: true)
|
|
|
|
XCTAssertEqual(state.phase, .awaitingSend)
|
|
XCTAssertFalse(state.canInsert)
|
|
XCTAssertTrue(state.canSend)
|
|
XCTAssertEqual(state.answer?.isInserted, true)
|
|
|
|
state.markAnswerSent()
|
|
|
|
XCTAssertEqual(state.phase, .sent)
|
|
XCTAssertFalse(state.canSend)
|
|
XCTAssertEqual(state.answer?.isSent, true)
|
|
}
|
|
|
|
func testNonSendFieldFinishesAfterInsertion() {
|
|
var state = AISessionState()
|
|
let utteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: utteranceID)
|
|
state.receiveAnswer("答案", utteranceID: utteranceID)
|
|
|
|
state.markAnswerInserted(offersSend: false)
|
|
|
|
XCTAssertEqual(state.phase, .inserted)
|
|
XCTAssertFalse(state.canPerformAnswerAction)
|
|
XCTAssertEqual(state.answer?.isInserted, true)
|
|
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()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: firstUtteranceID)
|
|
state.receiveAnswer("已插入答案", utteranceID: firstUtteranceID)
|
|
state.markAnswerInserted(offersSend: true)
|
|
|
|
let secondUtteranceID = UUID()
|
|
state.beginPreparing(utteranceID: secondUtteranceID)
|
|
state.cancelCurrentWork()
|
|
|
|
XCTAssertEqual(state.phase, .awaitingSend)
|
|
XCTAssertTrue(state.canSend)
|
|
}
|
|
|
|
func testStaleResultCannotReplaceCurrentAnswer() {
|
|
var state = AISessionState()
|
|
let currentUtteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: currentUtteranceID)
|
|
|
|
state.receiveAnswer("迟到答案", utteranceID: UUID())
|
|
|
|
XCTAssertNil(state.answer)
|
|
XCTAssertEqual(state.phase, .preparing)
|
|
}
|
|
|
|
func testPartialAnswerKeepsPreviousCommittedAnswerUntilFinal() {
|
|
var state = AISessionState()
|
|
let firstUtteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: firstUtteranceID)
|
|
state.receiveAnswer("答案一", utteranceID: firstUtteranceID)
|
|
|
|
let secondUtteranceID = UUID()
|
|
state.beginPreparing(utteranceID: secondUtteranceID)
|
|
state.beginGenerating(question: "问题二", utteranceID: secondUtteranceID)
|
|
state.receivePartialAnswer("草", utteranceID: secondUtteranceID)
|
|
|
|
XCTAssertEqual(state.phase, .generating)
|
|
XCTAssertEqual(state.draftAnswerText, "草")
|
|
XCTAssertEqual(state.answer?.text, "答案一")
|
|
XCTAssertFalse(state.canInsert)
|
|
|
|
state.receivePartialAnswer("草稿答案", utteranceID: secondUtteranceID)
|
|
XCTAssertEqual(state.draftAnswerText, "草稿答案")
|
|
|
|
state.receiveAnswer("答案二", utteranceID: secondUtteranceID)
|
|
XCTAssertNil(state.draftAnswerText)
|
|
XCTAssertEqual(state.answer?.text, "答案二")
|
|
XCTAssertTrue(state.canInsert)
|
|
}
|
|
|
|
func testCancelClearsDraftAndRestoresPreviousAnswer() {
|
|
var state = AISessionState()
|
|
let firstUtteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: firstUtteranceID)
|
|
state.receiveAnswer("可用答案", utteranceID: firstUtteranceID)
|
|
|
|
let secondUtteranceID = UUID()
|
|
state.beginPreparing(utteranceID: secondUtteranceID)
|
|
state.beginGenerating(question: "新问题", utteranceID: secondUtteranceID)
|
|
state.receivePartialAnswer("半截", utteranceID: secondUtteranceID)
|
|
state.cancelCurrentWork()
|
|
|
|
XCTAssertNil(state.draftAnswerText)
|
|
XCTAssertEqual(state.phase, .ready)
|
|
XCTAssertEqual(state.answer?.text, "可用答案")
|
|
}
|
|
|
|
func testReplyVariantsBecomeReadyWithoutCreatingInsertableAnswer() {
|
|
var state = AISessionState()
|
|
let utteranceID = UUID()
|
|
let variants = makeReplyVariants()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: utteranceID)
|
|
|
|
state.receiveReplyVariants(variants, utteranceID: utteranceID)
|
|
|
|
XCTAssertEqual(state.phase, .ready)
|
|
XCTAssertTrue(state.canSelectReplyVariant)
|
|
XCTAssertFalse(state.canInsert)
|
|
XCTAssertNil(state.answer)
|
|
XCTAssertEqual(state.replyVariants.map(\.kind), [.ordinary, .formal, .playful])
|
|
}
|
|
|
|
func testSelectingReplyVariantExposesSelectionForInsertionAndFeedback() throws {
|
|
var state = AISessionState()
|
|
let utteranceID = UUID()
|
|
let variants = makeReplyVariants()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: utteranceID)
|
|
state.receiveReplyVariants(variants, utteranceID: utteranceID)
|
|
|
|
let answer = try XCTUnwrap(
|
|
state.selectReplyVariant(id: variants[2].id)
|
|
)
|
|
|
|
XCTAssertEqual(answer.id, variants[2].id)
|
|
XCTAssertEqual(answer.text, "轻松回复 🎉")
|
|
XCTAssertEqual(state.selectedReplyVariant, variants[2])
|
|
XCTAssertTrue(state.canInsert)
|
|
XCTAssertFalse(state.canSelectReplyVariant)
|
|
|
|
state.markAnswerInserted(offersSend: false)
|
|
XCTAssertEqual(state.phase, .inserted)
|
|
}
|
|
|
|
func testDiscardReplyVariantsClearsAllCandidateState() {
|
|
var state = AISessionState()
|
|
let utteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: utteranceID)
|
|
state.receiveReplyVariants(makeReplyVariants(), utteranceID: utteranceID)
|
|
|
|
state.discardReadyAnswer()
|
|
|
|
XCTAssertEqual(state.phase, .idle)
|
|
XCTAssertTrue(state.replyVariants.isEmpty)
|
|
XCTAssertNil(state.selectedReplyVariant)
|
|
}
|
|
|
|
func testSceneReplyVariantsUseLocalRoleOrder() {
|
|
var state = AISessionState()
|
|
let utteranceID = UUID()
|
|
state.enter()
|
|
state.beginPreparing(utteranceID: utteranceID)
|
|
state.receiveReplyVariants(
|
|
[
|
|
AIReplyVariant(
|
|
kind: .invitationTentative,
|
|
emotion: .neutral,
|
|
text: "我确认一下。"
|
|
),
|
|
AIReplyVariant(
|
|
kind: .invitationDecline,
|
|
emotion: .grateful,
|
|
text: "这次先不去了。"
|
|
),
|
|
AIReplyVariant(
|
|
kind: .invitationAccept,
|
|
emotion: .warm,
|
|
text: "好呀,到时见。"
|
|
)
|
|
],
|
|
utteranceID: utteranceID
|
|
)
|
|
|
|
XCTAssertTrue(state.canSelectReplyVariant)
|
|
XCTAssertEqual(
|
|
state.replyVariants.map(\.kind),
|
|
AIReplyVariantSet.invitation.kinds
|
|
)
|
|
}
|
|
|
|
private func makeReplyVariants() -> [AIReplyVariant] {
|
|
[
|
|
AIReplyVariant(kind: .ordinary, emotion: .warm, text: "普通回复"),
|
|
AIReplyVariant(kind: .formal, emotion: .neutral, text: "正式回复"),
|
|
AIReplyVariant(kind: .playful, emotion: .celebratory, text: "轻松回复 🎉")
|
|
]
|
|
}
|
|
}
|