test(ai): expand AI flow and clipboard semantic coverage

- 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.
This commit is contained in:
Rocky
2026-08-29 11:51:25 +08:00
parent 3c10d73d7f
commit b275b6b0d9
15 changed files with 1348 additions and 125 deletions
+108 -3
View File
@@ -114,6 +114,24 @@ final class IntelligentPolishTests: XCTestCase {
XCTAssertFalse(captured.lastPrompt.contains("趣味风格共享格式化"))
}
func testCallerSuppliedOptionsReachLLMClientUnchanged() async throws {
let captured = CapturingLLMClient()
let service = PolishingService(store: store, client: captured)
let options = LLMGenerationOptions(
temperature: 0.42,
topP: 0.73,
maxTokens: 777
)
_ = try await service.polish(
"分析这些风格学习样本",
systemPrompt: "Return structured evidence.",
options: options
)
XCTAssertEqual(captured.lastOptions, options)
}
func testPersonalDictionaryUpsertManual() {
var dict = PersonalDictionary.empty
let entry = dict.upsertManual(term: "Kubernetes")
@@ -315,14 +333,71 @@ final class IntelligentPolishTests: XCTestCase {
)
}
func testPolishServiceCapsTimeoutAt120() async throws {
func testPolishServiceDefaultsToKeyboardTimeoutCap() async throws {
store.setEngineMode("local")
let captured = CapturingLLMClient()
let service = PolishingService(store: store, client: captured, timeout: 15)
let service = PolishingService(store: store, client: captured)
let veryLong = String(repeating: "测试", count: 2000)
_ = try await service.polish(veryLong, context: PolishContext())
let passedTimeout = try XCTUnwrap(captured.lastTimeout)
XCTAssertLessThanOrEqual(passedTimeout, 120)
XCTAssertEqual(passedTimeout, FlowSessionKeys.maxPolishTimeout)
}
func testPolishServiceAllowsExplicit45SecondTimeoutCap() async throws {
let captured = CapturingLLMClient()
let service = PolishingService(
store: store,
client: captured,
timeout: 45,
maximumTimeout: 45
)
_ = try await service.polish(
"分析这些风格学习样本",
systemPrompt: "Return structured evidence."
)
XCTAssertEqual(captured.lastTimeout, 45)
}
func testPolishServicePropagatesCallerCancellation() async {
let gate = LLMRequestGate()
let service = PolishingService(
store: store,
client: SuspendingLLMClient(gate: gate)
)
let task = Task {
try await service.polish(
"分析这些风格学习样本",
systemPrompt: "Return structured evidence."
)
}
await gate.waitUntilStarted()
task.cancel()
do {
_ = try await task.value
XCTFail("Expected caller cancellation")
} catch {
XCTAssertTrue(error is CancellationError, "Unexpected error: \(error)")
}
}
func testPolishServicePreservesProviderCancellationError() async {
let service = PolishingService(store: store, client: ThrowingLLMClient())
do {
_ = try await service.polish(
"分析这些风格学习样本",
systemPrompt: "Return structured evidence."
)
XCTFail("Expected provider cancellation")
} catch let error as LLMError {
XCTAssertEqual(error, .cancelled)
} catch {
XCTFail("Expected LLMError.cancelled, got \(error)")
}
}
func testPolishServiceUsesChineseForChineseProviders() async throws {
@@ -779,6 +854,36 @@ private final class ThrowingLLMClient: LLMClient, @unchecked Sendable {
}
}
private actor LLMRequestGate {
private var started = false
private var waiters: [CheckedContinuation<Void, Never>] = []
func markStarted() {
started = true
let pending = waiters
waiters.removeAll()
pending.forEach { $0.resume() }
}
func waitUntilStarted() async {
if started { return }
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
}
private struct SuspendingLLMClient: LLMClient {
let gate: LLMRequestGate
let requestTimeout: TimeInterval = 15
func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String {
await gate.markStarted()
try await Task.sleep(nanoseconds: 10_000_000_000)
return text
}
}
private final class FixedResponseLLMClient: LLMClient, @unchecked Sendable {
let requestTimeout: TimeInterval = 15
private let response: String