feat: cursor navigation, key sounds, dictionary tooling, key security
Batch of in-progress app work from the working tree. - feat(keyboard): CursorNavigation + CursorDragPad for caret movement; KeyboardSoundFeedback for system key click sounds - feat(dictionary): DictionaryAliasGenerator + PersonalDictionaryEntrySheet; TranscriptPostProcessor quality gate; retire DictionaryLearner - feat(ui): TabBarVisibility handling; drop PageHeaderRow / PageHeaderConfirmButton; refresh views and localizable strings - fix(security): move the hardcoded DeepSeek key out of PreconfiguredKeys.swift into a gitignored PreconfiguredKeys.local.swift (seeded from .example by generate-xcodeproj.sh) - docs(agents): add Conventional Commits versioning + bilingual changelog rules - chore(gitignore): ignore PreconfiguredKeys.local.swift, .cache/, pycache Custom language model / lexicon work stays on feature/custom-language-model-asr. Changelog bullets added under [Unreleased]; no version bump.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// OSGKeyboard · Tests
|
||||
//
|
||||
// v0.3.0: locks the behavior of the rewritten PolishingService and
|
||||
// its two supporting services (AppContextDetector, DictionaryLearner).
|
||||
// its supporting service (AppContextDetector).
|
||||
// The tests are deliberately hermetic — no LLMClient, no ASR, no
|
||||
// App Group — so they run in <100 ms total.
|
||||
|
||||
@@ -18,11 +18,6 @@ final class IntelligentPolishTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Each test gets a fresh, throwaway UserDefaults suite so
|
||||
// engine mode / API key / dictionary / context state does
|
||||
// not leak between tests. The AppGroupStore falls back to
|
||||
// `.standard` when no App Group entitlement is present, so
|
||||
// we point it at a private suite to keep this test hermetic.
|
||||
suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)"
|
||||
defaults = UserDefaults(suiteName: suiteName)!
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
@@ -34,31 +29,95 @@ final class IntelligentPolishTests: XCTestCase {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - PolishingService prompt construction
|
||||
// MARK: - Polish intensity migration
|
||||
|
||||
func testPolishServiceOffIntensitySkipsLLM() async throws {
|
||||
// When intensity is `.off`, the service must return the
|
||||
// raw input unchanged *and* not touch the LLM. We assert
|
||||
// both by passing a deliberately broken LLM client and
|
||||
// expecting the call to return cleanly.
|
||||
store.setEngineMode("cloud")
|
||||
let service = PolishingService(
|
||||
store: store,
|
||||
client: ThrowingLLMClient() // would throw if invoked
|
||||
)
|
||||
let result = try await service.polish("hello world", context: PolishContext(intensity: .off))
|
||||
XCTAssertEqual(result, "hello world")
|
||||
func testPolishIntensityMigratesLegacyOffToMedium() {
|
||||
defaults.set(PolishIntensity.legacyOffRawValue, forKey: "config.polishIntensity")
|
||||
XCTAssertEqual(store.polishIntensity, .medium)
|
||||
XCTAssertEqual(defaults.string(forKey: "config.polishIntensity"), PolishIntensity.medium.rawValue)
|
||||
}
|
||||
|
||||
func testPolishServiceLocalEngineWithoutCloudPolishReturnsRaw() async throws {
|
||||
store.setEngineMode("local")
|
||||
// localModeCloudPolishEnabled defaults to false.
|
||||
func testPolishIntensityResolveLegacyOff() {
|
||||
XCTAssertEqual(PolishIntensity.resolve(storedRawValue: "off"), .medium)
|
||||
}
|
||||
|
||||
// MARK: - PolishingService prompt construction
|
||||
|
||||
func testPolishServiceUltraShortTextSkipsLLM() async throws {
|
||||
store.setEngineMode("cloud")
|
||||
let service = PolishingService(
|
||||
store: store,
|
||||
client: ThrowingLLMClient()
|
||||
)
|
||||
let result = try await service.polish("hello world", context: PolishContext(intensity: .medium))
|
||||
XCTAssertEqual(result, "hello world")
|
||||
let result = try await service.polish("好", context: PolishContext(intensity: .heavy))
|
||||
XCTAssertEqual(result, "好")
|
||||
}
|
||||
|
||||
func testPolishServiceShortStructuredTextStillInvokesLLM() async throws {
|
||||
store.setEngineMode("local")
|
||||
let captured = CapturingLLMClient()
|
||||
let service = PolishingService(store: store, client: captured)
|
||||
_ = try await service.polish(
|
||||
"第一点测试第二点上线",
|
||||
context: PolishContext(intensity: .medium)
|
||||
)
|
||||
XCTAssertFalse(captured.lastPrompt.isEmpty)
|
||||
}
|
||||
|
||||
func testPersonalDictionaryUpsertManual() {
|
||||
var dict = PersonalDictionary.empty
|
||||
let entry = dict.upsertManual(term: "Kubernetes")
|
||||
XCTAssertEqual(entry?.term, "Kubernetes")
|
||||
XCTAssertEqual(entry?.source, .manual)
|
||||
XCTAssertEqual(dict.entries.count, 1)
|
||||
|
||||
let updated = dict.upsertManual(term: "kubernetes", existingID: entry?.id)
|
||||
XCTAssertEqual(updated?.term, "kubernetes")
|
||||
XCTAssertEqual(dict.entries.count, 1)
|
||||
}
|
||||
|
||||
func testDictionaryAliasGeneratorParsesJSONArray() {
|
||||
let aliases = DictionaryAliasGenerator.parseAliases(
|
||||
from: #"["k8s","库伯内特斯"]"#,
|
||||
excludingTerm: "Kubernetes"
|
||||
)
|
||||
XCTAssertEqual(aliases, ["k8s", "库伯内特斯"])
|
||||
}
|
||||
|
||||
func testDictionaryAliasGeneratorExcludesCanonicalTerm() {
|
||||
let aliases = DictionaryAliasGenerator.parseAliases(
|
||||
from: #"["Kubernetes","k8s"]"#,
|
||||
excludingTerm: "Kubernetes"
|
||||
)
|
||||
XCTAssertEqual(aliases, ["k8s"])
|
||||
}
|
||||
|
||||
func testEntryInferCategoryForChinese() {
|
||||
XCTAssertEqual(PersonalDictionary.Entry.inferCategory(for: "张三"), .properNoun)
|
||||
XCTAssertEqual(PersonalDictionary.Entry.inferCategory(for: "LLM"), .acronym)
|
||||
}
|
||||
|
||||
func testPersonalDictionaryMigratesLegacyHistorySource() {
|
||||
let legacy = PersonalDictionary(entries: [
|
||||
PersonalDictionary.Entry(term: "Kubernetes", category: .productName, source: .history),
|
||||
])
|
||||
let data = try! JSONEncoder().encode(legacy)
|
||||
defaults.set(data, forKey: "config.personalDictionary.v1")
|
||||
|
||||
let loaded = store.personalDictionary
|
||||
XCTAssertEqual(loaded.entries.first?.source, .manual)
|
||||
XCTAssertEqual(loaded.entries.first?.term, "Kubernetes")
|
||||
}
|
||||
|
||||
func testPolishServiceLocalEngineInvokesLLM() async throws {
|
||||
store.setEngineMode("local")
|
||||
let captured = CapturingLLMClient()
|
||||
let service = PolishingService(store: store, client: captured)
|
||||
_ = try await service.polish(
|
||||
"今天我们部署 k8s 集群",
|
||||
context: PolishContext(appContext: .code, intensity: .medium)
|
||||
)
|
||||
XCTAssertFalse(captured.lastPrompt.isEmpty)
|
||||
}
|
||||
|
||||
func testPolishServiceMissingAPIKeyThrows() async {
|
||||
@@ -78,9 +137,6 @@ final class IntelligentPolishTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testPolishServiceShortTextSkipsLLM() async throws {
|
||||
// Per the prompt's hard rule #4, ≤ 8 CJK chars / ≤ 15
|
||||
// English words must be returned verbatim. We exercise
|
||||
// the upper bound here.
|
||||
store.setEngineMode("cloud")
|
||||
let service = PolishingService(
|
||||
store: store,
|
||||
@@ -107,24 +163,174 @@ final class IntelligentPolishTests: XCTestCase {
|
||||
"Prompt must include dictionary term. Got: \(captured.lastPrompt)")
|
||||
XCTAssertTrue(captured.lastPrompt.contains("Code context"),
|
||||
"Prompt must include app-context guideline. Got: \(captured.lastPrompt)")
|
||||
XCTAssertTrue(captured.lastPrompt.contains("medium") || captured.lastPrompt.contains("中度"),
|
||||
"Prompt must mention the intensity. Got: \(captured.lastPrompt)")
|
||||
XCTAssertTrue(
|
||||
captured.lastPrompt.contains("全局输出契约") || captured.lastPrompt.contains("Global output contract"),
|
||||
"Prompt must include global output contract. Got: \(captured.lastPrompt.prefix(200))"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
captured.lastPrompt.localizedCaseInsensitiveContains("emoji"),
|
||||
"Prompt must include strict emoji control guidance. Got: \(captured.lastPrompt)"
|
||||
)
|
||||
XCTAssertFalse(
|
||||
captured.lastPrompt.localizedCaseInsensitiveContains("emoji-friendly"),
|
||||
"Chat context must not encourage emojis. Got: \(captured.lastPrompt)"
|
||||
)
|
||||
}
|
||||
|
||||
func testPolishServicePromptIncludesStructureRulesAtLightIntensity() async throws {
|
||||
store.setEngineMode("local")
|
||||
let captured = CapturingLLMClient()
|
||||
let service = PolishingService(store: store, client: captured)
|
||||
_ = try await service.polish(
|
||||
"今天有三个任务第一点修复登录第二点优化键盘",
|
||||
context: PolishContext(intensity: .light)
|
||||
)
|
||||
XCTAssertTrue(
|
||||
captured.lastPrompt.contains("第一点") || captured.lastPrompt.contains("numbered"),
|
||||
"Light intensity must still include structure rules. Got: \(captured.lastPrompt.prefix(300))"
|
||||
)
|
||||
}
|
||||
|
||||
func testPolishServiceScalesTimeoutWithTextLength() async throws {
|
||||
store.setEngineMode("local")
|
||||
let captured = CapturingLLMClient()
|
||||
let service = PolishingService(store: store, client: captured, timeout: 15)
|
||||
let longText = String(repeating: "这是一段比较长的语音识别测试文本,", count: 20)
|
||||
_ = try await service.polish(longText, context: PolishContext(intensity: .medium))
|
||||
let passedTimeout = try XCTUnwrap(captured.lastTimeout)
|
||||
XCTAssertGreaterThan(
|
||||
passedTimeout, 15,
|
||||
"Long transcripts must scale the per-request HTTP timeout above the baseline"
|
||||
)
|
||||
}
|
||||
|
||||
func testPolishServiceCapsTimeoutAt120() async throws {
|
||||
store.setEngineMode("local")
|
||||
let captured = CapturingLLMClient()
|
||||
let service = PolishingService(store: store, client: captured, timeout: 15)
|
||||
let veryLong = String(repeating: "测试", count: 2000)
|
||||
_ = try await service.polish(veryLong, context: PolishContext(intensity: .medium))
|
||||
let passedTimeout = try XCTUnwrap(captured.lastTimeout)
|
||||
XCTAssertLessThanOrEqual(passedTimeout, 120)
|
||||
}
|
||||
|
||||
func testPolishServiceUsesChineseForChineseProviders() async throws {
|
||||
defaults.set("deepseek", forKey: "config.providerId")
|
||||
store.setEngineMode("cloud")
|
||||
store.setEngineMode("local")
|
||||
let captured = CapturingLLMClient()
|
||||
let service = PolishingService(store: store, client: captured)
|
||||
_ = try await service.polish("hello", context: PolishContext(intensity: .medium))
|
||||
// The polisher routes Chinese providers through the Chinese
|
||||
// prompt, which is identifiable by its "三件事" header.
|
||||
XCTAssertTrue(
|
||||
captured.lastPrompt.contains("三件事"),
|
||||
"DeepSeek should get the Chinese prompt. Got prefix: \(captured.lastPrompt.prefix(80))"
|
||||
captured.lastPrompt.contains("全局输出契约"),
|
||||
"Local engine should get the Chinese prompt via DeepSeek. Got prefix: \(captured.lastPrompt.prefix(80))"
|
||||
)
|
||||
}
|
||||
|
||||
func testPolishServiceStripsAddedEmojiFromLLMOutput() async throws {
|
||||
store.setEngineMode("local")
|
||||
let emojiClient = FixedResponseLLMClient(response: "今天的工作已经全部完成了👍")
|
||||
let service = PolishingService(store: store, client: emojiClient)
|
||||
let result = try await service.polish(
|
||||
"今天的工作已经全部完成了",
|
||||
context: PolishContext(intensity: .medium)
|
||||
)
|
||||
XCTAssertFalse(result.contains("👍"))
|
||||
XCTAssertTrue(result.contains("完成"))
|
||||
}
|
||||
|
||||
func testPolishServiceFallsBackWhenOutputEmpty() async throws {
|
||||
store.setEngineMode("local")
|
||||
let emptyClient = FixedResponseLLMClient(response: " ")
|
||||
let service = PolishingService(store: store, client: emptyClient)
|
||||
let result = try await service.polish(
|
||||
"今天的部署已经全部完成",
|
||||
context: PolishContext(intensity: .medium)
|
||||
)
|
||||
XCTAssertEqual(result, "今天的部署已经全部完成")
|
||||
}
|
||||
|
||||
// MARK: - TranscriptPostProcessor
|
||||
|
||||
func testShouldSkipLLMForUltraShortWithoutStructure() {
|
||||
XCTAssertTrue(TranscriptPostProcessor.shouldSkipLLM(for: "好"))
|
||||
XCTAssertTrue(TranscriptPostProcessor.shouldSkipLLM(for: "OK"))
|
||||
XCTAssertTrue(TranscriptPostProcessor.shouldSkipLLM(for: "明天见"))
|
||||
}
|
||||
|
||||
func testShouldNotSkipLLMWhenStructurePresent() {
|
||||
XCTAssertFalse(TranscriptPostProcessor.shouldSkipLLM(for: "第一点做完第二点再做"))
|
||||
}
|
||||
|
||||
func testStripAddedEmojisRemovesNewEmoji() {
|
||||
let result = TranscriptPostProcessor.stripAddedEmojis(
|
||||
original: "好的",
|
||||
output: "好的👍"
|
||||
)
|
||||
XCTAssertEqual(result, "好的")
|
||||
}
|
||||
|
||||
func testNormalizeNumberedLists() {
|
||||
let input = "第一点 修复\n第二点 上线"
|
||||
let output = TranscriptPostProcessor.normalizeNumberedLists(input)
|
||||
XCTAssertTrue(output.contains("1. 修复"))
|
||||
XCTAssertTrue(output.contains("2. 上线"))
|
||||
}
|
||||
|
||||
func testQualityGateNeverRevertsToRawOnNumberChange() {
|
||||
// Listifying / fixing ASR number-mishearings legitimately
|
||||
// changes the number set — this must NOT revert to the raw text.
|
||||
let decision = TranscriptPostProcessor.qualityGate(
|
||||
original: "第一点测试第2:00上线",
|
||||
candidate: "1. 测试\n2. 上线"
|
||||
)
|
||||
if case .accept(let text) = decision {
|
||||
XCTAssertTrue(text.contains("1. 测试"))
|
||||
XCTAssertTrue(text.contains("2. 上线"))
|
||||
} else {
|
||||
XCTFail("Expected accept — number changes must not trigger raw fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func testQualityGateStillFallsBackOnEmptyOutput() {
|
||||
let decision = TranscriptPostProcessor.qualityGate(
|
||||
original: "部署完成",
|
||||
candidate: " "
|
||||
)
|
||||
if case .fallback(let text) = decision {
|
||||
XCTAssertEqual(text, "部署完成")
|
||||
} else {
|
||||
XCTFail("Expected fallback on empty output")
|
||||
}
|
||||
}
|
||||
|
||||
func testRepairMidSentenceLineBreakJoinsBrokenSentence() {
|
||||
let input = "你是不是真的解决了这个格式化和标点符号包括\n这些问题"
|
||||
let output = TranscriptPostProcessor.repairMidSentenceLineBreaks(input)
|
||||
XCTAssertEqual(output, "你是不是真的解决了这个格式化和标点符号包括这些问题")
|
||||
}
|
||||
|
||||
func testRepairMidSentenceLineBreakKeepsSentenceBoundary() {
|
||||
let input = "今天完成了部署。\n明天开始测试。"
|
||||
let output = TranscriptPostProcessor.repairMidSentenceLineBreaks(input)
|
||||
XCTAssertEqual(output, input)
|
||||
}
|
||||
|
||||
func testRepairMidSentenceLineBreakKeepsListItems() {
|
||||
let input = "1. 修复登录\n2. 优化键盘"
|
||||
let output = TranscriptPostProcessor.repairMidSentenceLineBreaks(input)
|
||||
XCTAssertEqual(output, input)
|
||||
}
|
||||
|
||||
func testRepairMidSentenceLineBreakJoinsEnglishWithSpace() {
|
||||
let input = "this is a broken\nsentence"
|
||||
let output = TranscriptPostProcessor.repairMidSentenceLineBreaks(input)
|
||||
XCTAssertEqual(output, "this is a broken sentence")
|
||||
}
|
||||
|
||||
func testHasStructureSignalDetectsChineseEnumeration() {
|
||||
XCTAssertTrue(TranscriptPostProcessor.hasStructureSignal(in: "首先测试其次上线"))
|
||||
XCTAssertTrue(TranscriptPostProcessor.hasStructureSignal(in: "第一点修复"))
|
||||
}
|
||||
|
||||
// MARK: - AppContextDetector
|
||||
|
||||
func testAppContextDetectorRecognizesCodeByIndentation() {
|
||||
@@ -165,29 +371,32 @@ final class IntelligentPolishTests: XCTestCase {
|
||||
|
||||
func testAppContextDetectorFallbackChain() {
|
||||
let detector = AppContextDetector()
|
||||
// No preceding text and no cache → environmental fallback.
|
||||
let env = detector.detect(
|
||||
precedingText: nil,
|
||||
storedCache: nil,
|
||||
now: Date(timeIntervalSince1970: 1_700_000_000) // a workday moment
|
||||
now: Date(timeIntervalSince1970: 1_700_000_000)
|
||||
)
|
||||
XCTAssertNotEqual(env, .unknown)
|
||||
}
|
||||
|
||||
func testAppContextDetectorCacheWinsOverFallback() {
|
||||
let detector = AppContextDetector()
|
||||
// 5-minute-old cache with `.code` must be returned even
|
||||
// when there is no preceding text.
|
||||
let cache = (context: AppContext.code, observedAt: Date().addingTimeInterval(-300))
|
||||
let result = detector.detect(precedingText: "", storedCache: cache)
|
||||
XCTAssertEqual(result, .code)
|
||||
}
|
||||
|
||||
func testChatAppContextGuidelineDoesNotEncourageEmoji() {
|
||||
let guideline = AppContext.chat.polishGuideline
|
||||
XCTAssertFalse(guideline.localizedCaseInsensitiveContains("emoji-friendly"))
|
||||
XCTAssertTrue(guideline.localizedCaseInsensitiveContains("Do not add emojis"))
|
||||
}
|
||||
|
||||
// MARK: - PersonalDictionary.promptFragment
|
||||
|
||||
func testDictionaryPromptFragmentIsEmptyForEmptyDictionary() {
|
||||
func testDictionaryPromptFragmentIncludesBuiltInOSGKeyboard() {
|
||||
let prompt = PersonalDictionary.empty.promptFragment()
|
||||
XCTAssertEqual(prompt, "")
|
||||
XCTAssertTrue(prompt.contains("OSGKeyboard"))
|
||||
}
|
||||
|
||||
func testDictionaryPromptFragmentGroupsByCategory() {
|
||||
@@ -197,86 +406,48 @@ final class IntelligentPolishTests: XCTestCase {
|
||||
PersonalDictionary.Entry(term: "Rocky", category: .properNoun, source: .manual),
|
||||
])
|
||||
let prompt = dict.promptFragment()
|
||||
XCTAssertTrue(prompt.contains("OSGKeyboard"))
|
||||
XCTAssertTrue(prompt.contains("Kubernetes"))
|
||||
XCTAssertTrue(prompt.contains("iOS"))
|
||||
XCTAssertTrue(prompt.contains("Rocky"))
|
||||
}
|
||||
|
||||
// MARK: - DictionaryLearner
|
||||
|
||||
func testLearnerPromotesRepeatedCapitalizedToken() {
|
||||
let history: [SpeechHistoryEntry] = [
|
||||
.init(text: "Deploy Kubernetes today", engineMode: "cloud"),
|
||||
.init(text: "Restart Kubernetes pod", engineMode: "cloud"),
|
||||
]
|
||||
let learner = DictionaryLearner(minOccurrences: 2)
|
||||
let added = learner.learn(from: history)
|
||||
XCTAssertTrue(added.contains { $0.term == "Kubernetes" },
|
||||
"Kubernetes should be promoted. Got: \(added.map(\.term))")
|
||||
}
|
||||
|
||||
func testLearnerIgnoresStopwords() {
|
||||
let history: [SpeechHistoryEntry] = [
|
||||
.init(text: "this is the test", engineMode: "cloud"),
|
||||
.init(text: "this is the second test", engineMode: "cloud"),
|
||||
.init(text: "this is the third test", engineMode: "cloud"),
|
||||
]
|
||||
let learner = DictionaryLearner(minOccurrences: 2)
|
||||
let added = learner.learn(from: history)
|
||||
let terms = Set(added.map(\.term))
|
||||
XCTAssertFalse(terms.contains("this"))
|
||||
XCTAssertFalse(terms.contains("the"))
|
||||
XCTAssertFalse(terms.contains("is"))
|
||||
}
|
||||
|
||||
func testLearnerRespectsMinimumOccurrence() {
|
||||
let history: [SpeechHistoryEntry] = [
|
||||
.init(text: "First time mentioning Whisper", engineMode: "cloud"),
|
||||
]
|
||||
let learner = DictionaryLearner(minOccurrences: 2)
|
||||
let added = learner.learn(from: history)
|
||||
XCTAssertFalse(added.contains { $0.term == "Whisper" })
|
||||
}
|
||||
|
||||
func testLearnerIdempotent() {
|
||||
let history: [SpeechHistoryEntry] = [
|
||||
.init(text: "OpenAI rocks", engineMode: "cloud"),
|
||||
.init(text: "OpenAI again", engineMode: "cloud"),
|
||||
]
|
||||
let learner = DictionaryLearner(minOccurrences: 2)
|
||||
let first = learner.learn(from: history)
|
||||
let second = learner.learn(from: history)
|
||||
XCTAssertTrue(first.contains { $0.term == "OpenAI" })
|
||||
// Second call must not double-add; the existing entry's
|
||||
// usage count is bumped instead.
|
||||
let openaiEntries = second.filter { $0.term == "OpenAI" }
|
||||
XCTAssertEqual(openaiEntries.count, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test doubles
|
||||
|
||||
/// Records every call so the test can inspect the prompt the
|
||||
/// polisher would have sent. We do not assert on `response`; the
|
||||
/// LLMClient contract is exercised by `LLMClientTests`.
|
||||
private final class CapturingLLMClient: LLMClient, @unchecked Sendable {
|
||||
private(set) var lastPrompt: String = ""
|
||||
private(set) var lastTimeout: TimeInterval?
|
||||
let requestTimeout: TimeInterval = 15
|
||||
|
||||
func polish(_ text: String, systemPrompt: String) async throws -> String {
|
||||
func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String {
|
||||
lastPrompt = systemPrompt
|
||||
lastTimeout = timeout
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
private final class EchoLLMClient: LLMClient, @unchecked Sendable {
|
||||
let requestTimeout: TimeInterval = 15
|
||||
func polish(_ text: String, systemPrompt: String) async throws -> String { text }
|
||||
func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String { text }
|
||||
}
|
||||
|
||||
private final class ThrowingLLMClient: LLMClient, @unchecked Sendable {
|
||||
let requestTimeout: TimeInterval = 15
|
||||
func polish(_ text: String, systemPrompt: String) async throws -> String {
|
||||
func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String {
|
||||
throw LLMError.cancelled
|
||||
}
|
||||
}
|
||||
|
||||
private final class FixedResponseLLMClient: LLMClient, @unchecked Sendable {
|
||||
let requestTimeout: TimeInterval = 15
|
||||
private let response: String
|
||||
|
||||
init(response: String) {
|
||||
self.response = response
|
||||
}
|
||||
|
||||
func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user