feat(keyboard): add clipboard voice command mode
Long-press mic runs ASR as an instruction over eligible clipboard text, with host-confirm recording UI, min-record gate, and light ASR prewarm. Bump CURRENT_PROJECT_VERSION to 53.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// ClipboardCommandPromptComposerTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class ClipboardCommandPromptComposerTests: XCTestCase {
|
||||
|
||||
func testUserMessageIncludesMaterialInstructionAndPrevious() {
|
||||
let input = ClipboardCommandPromptComposer.Input(
|
||||
snapshot: "对方说周末见面",
|
||||
instruction: "委婉拒绝",
|
||||
previousOutput: "这周不太方便"
|
||||
)
|
||||
let user = ClipboardCommandPromptComposer.userMessage(input, language: .chinese)
|
||||
XCTAssertTrue(user.contains("【材料】"))
|
||||
XCTAssertTrue(user.contains("对方说周末见面"))
|
||||
XCTAssertTrue(user.contains("【指令】"))
|
||||
XCTAssertTrue(user.contains("委婉拒绝"))
|
||||
XCTAssertTrue(user.contains("【上一版结果】"))
|
||||
XCTAssertTrue(user.contains("这周不太方便"))
|
||||
}
|
||||
|
||||
func testSystemPromptDoesNotEmbedR6DictationBan() {
|
||||
let system = ClipboardCommandPromptComposer.compose(
|
||||
.init(snapshot: "材料", instruction: "总结"),
|
||||
language: .chinese
|
||||
)
|
||||
XCTAssertTrue(system.contains("剪贴板写作助手"))
|
||||
XCTAssertFalse(system.contains("不是向你提出的问题或命令"))
|
||||
}
|
||||
|
||||
func testEligibilityTrackerKeepsSameChangeCountWindow() {
|
||||
let first = ClipboardCommandEligibilityTracker.refresh(
|
||||
changeCount: 3,
|
||||
rawText: "周末有空一起吃个饭吗?我想聊下项目进度。",
|
||||
previous: nil,
|
||||
now: 1_000
|
||||
)
|
||||
XCTAssertNotNil(first)
|
||||
|
||||
let same = ClipboardCommandEligibilityTracker.refresh(
|
||||
changeCount: 3,
|
||||
rawText: "周末有空一起吃个饭吗?我想聊下项目进度。",
|
||||
previous: first,
|
||||
now: 1_010
|
||||
)
|
||||
XCTAssertEqual(same?.startedAt, first?.startedAt)
|
||||
|
||||
let expired = ClipboardCommandEligibilityTracker.refresh(
|
||||
changeCount: 3,
|
||||
rawText: "周末有空一起吃个饭吗?我想聊下项目进度。",
|
||||
previous: first,
|
||||
now: 1_040
|
||||
)
|
||||
XCTAssertNil(expired)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// ClipboardMaterialFilterTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class ClipboardMaterialFilterTests: XCTestCase {
|
||||
|
||||
func testRejectsEmpty() {
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate(" "), .rejected(.empty))
|
||||
}
|
||||
|
||||
func testRejectsPhoneAndNumeric() {
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("13812345678"), .rejected(.phoneOrNumeric))
|
||||
XCTAssertEqual(
|
||||
ClipboardMaterialFilter.evaluate("+86 138-1234-5678"),
|
||||
.rejected(.phoneOrNumeric)
|
||||
)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("123-456"), .rejected(.phoneOrNumeric))
|
||||
}
|
||||
|
||||
func testRejectsEmojiOrSymbolOnly() {
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("😀😀😀"), .rejected(.emojiOrSymbolOnly))
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("!!!"), .rejected(.emojiOrSymbolOnly))
|
||||
}
|
||||
|
||||
func testRejectsVerificationCode() {
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("A8f2K1"), .rejected(.verificationCode))
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("x9Y2"), .rejected(.verificationCode))
|
||||
}
|
||||
|
||||
func testRejectsTooShort() {
|
||||
// 周末吃饭吗 = 5 graphemes
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate("周末吃饭吗"), .rejected(.tooShort))
|
||||
let fourteen = String(repeating: "啊", count: 14)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate(fourteen), .rejected(.tooShort))
|
||||
}
|
||||
|
||||
func testRejectsRepetitiveSpam() {
|
||||
let spam = String(repeating: "啊", count: 15)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.evaluate(spam), .rejected(.repetitiveSpam))
|
||||
}
|
||||
|
||||
func testAcceptsNaturalLanguage() {
|
||||
let text = "周末有空一起吃个饭吗?我想聊下项目进度。"
|
||||
switch ClipboardMaterialFilter.evaluate(text) {
|
||||
case .eligible(let snapshot):
|
||||
XCTAssertEqual(snapshot, text)
|
||||
case .rejected(let reason):
|
||||
XCTFail("expected eligible, got \(reason)")
|
||||
}
|
||||
}
|
||||
|
||||
func testAllowsDigitsInsideNaturalSentence() {
|
||||
let text = "明天 3 点见,我们在咖啡厅门口碰头再走。"
|
||||
if case .rejected = ClipboardMaterialFilter.evaluate(text) {
|
||||
XCTFail("sentence with digits should remain eligible")
|
||||
}
|
||||
}
|
||||
|
||||
func testTruncateSnapshot() {
|
||||
let long = String(repeating: "汉", count: 3_050)
|
||||
let truncated = ClipboardMaterialFilter.truncateSnapshot(long)
|
||||
XCTAssertEqual(truncated.count, ClipboardMaterialFilter.maxSnapshotLength)
|
||||
}
|
||||
|
||||
func testConstantsMatchPlan() {
|
||||
XCTAssertEqual(ClipboardMaterialFilter.minimumLength, 15)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.maxSnapshotLength, 3_000)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.longPressDuration, 0.45, accuracy: 0.001)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.minimumRecordingAfterHostConfirm, 0.70, accuracy: 0.001)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.eligibilityDuration, 30, accuracy: 0.001)
|
||||
XCTAssertEqual(ClipboardMaterialFilter.sessionDuration, 30, accuracy: 0.001)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user