498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
144 lines
4.6 KiB
Swift
144 lines
4.6 KiB
Swift
// AIEventExtractionTests.swift
|
|
// OSGKeyboardTests
|
|
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class AIEventExtractionTests: XCTestCase {
|
|
private var calendar: Calendar {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(identifier: "Asia/Shanghai")!
|
|
return calendar
|
|
}
|
|
|
|
private var now: Date {
|
|
date(2026, 8, 13, 20, 24)
|
|
}
|
|
|
|
func testNONEAndEmptyProduceNoItems() {
|
|
XCTAssertEqual(lines("NONE"), [])
|
|
XCTAssertEqual(lines("没有日期或时间"), [])
|
|
XCTAssertEqual(lines("no events"), [])
|
|
XCTAssertEqual(lines(" \n "), [])
|
|
}
|
|
|
|
func testAllDayDateOnly() {
|
|
XCTAssertEqual(
|
|
lines("2026-08-15||提交周报|"),
|
|
["2026-08-15|ALLDAY|提交周报|"]
|
|
)
|
|
}
|
|
|
|
func testTimedFillsDefaultOneHour() {
|
|
XCTAssertEqual(
|
|
lines("2026-08-15 14:00||项目评审|"),
|
|
["2026-08-15 14:00|2026-08-15 15:00|项目评审|"]
|
|
)
|
|
}
|
|
|
|
func testTimedKeepsExplicitEndAndLocation() {
|
|
XCTAssertEqual(
|
|
lines("2026-08-15 14:00|2026-08-15 16:00|项目评审|3楼会议室"),
|
|
["2026-08-15 14:00|2026-08-15 16:00|项目评审|3楼会议室"]
|
|
)
|
|
}
|
|
|
|
func testTimeOnlyUsesToday() {
|
|
XCTAssertEqual(
|
|
lines("15:00||打电话给客户|"),
|
|
["2026-08-13 15:00|2026-08-13 16:00|打电话给客户|"]
|
|
)
|
|
}
|
|
|
|
func testTimeOnlyEndOnSameDayOvernightRollsForward() {
|
|
XCTAssertEqual(
|
|
lines("2026-08-15 23:00|01:00|跨夜值班|"),
|
|
["2026-08-15 23:00|2026-08-16 01:00|跨夜值班|"]
|
|
)
|
|
}
|
|
|
|
func testTwoFieldStartAndTitle() {
|
|
XCTAssertEqual(
|
|
lines("2026-08-15 14:00|开会"),
|
|
["2026-08-15 14:00|2026-08-15 15:00|开会|"]
|
|
)
|
|
}
|
|
|
|
func testThreeFieldStartTitleLocationWhenMiddleIsNotATime() {
|
|
XCTAssertEqual(
|
|
lines("2026-08-15 14:00|开会|会议室A"),
|
|
["2026-08-15 14:00|2026-08-15 15:00|开会|会议室A"]
|
|
)
|
|
}
|
|
|
|
func testDropsLinesWithoutStartOrTitle() {
|
|
let raw = """
|
|
买牛奶
|
|
||无开始|
|
|
2026-08-15 14:00||
|
|
2026-08-16||有效全天|
|
|
"""
|
|
XCTAssertEqual(lines(raw), ["2026-08-16|ALLDAY|有效全天|"])
|
|
}
|
|
|
|
func testMultipleEventsCapAtTwenty() {
|
|
let raw = (1...25).map { "2026-08-15 10:00||任务\($0)|" }.joined(separator: "\n")
|
|
let items = lines(raw)
|
|
XCTAssertEqual(items.count, 20)
|
|
XCTAssertEqual(items.first, "2026-08-15 10:00|2026-08-15 11:00|任务1|")
|
|
XCTAssertEqual(items.last, "2026-08-15 10:00|2026-08-15 11:00|任务20|")
|
|
}
|
|
|
|
func testWholeClipboardEchoIsRejected() {
|
|
let source = String(repeating: "这是一段很长的会议纪要内容,包含许多句子。", count: 4)
|
|
XCTAssertEqual(
|
|
AIEventExtraction.lines(
|
|
from: "2026-08-15||\(source)|",
|
|
sourceClipboard: source,
|
|
now: now,
|
|
calendar: calendar
|
|
),
|
|
[]
|
|
)
|
|
}
|
|
|
|
func testPromptIncludesClockPipeContractAndNONE() {
|
|
let prompt = AIClipboardSkillCatalog.instruction(
|
|
skillID: AIClipboardSkillCatalog.extractEventsID,
|
|
locale: "zh",
|
|
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
|
now: now
|
|
)
|
|
XCTAssertTrue(prompt.contains("本地时区"))
|
|
XCTAssertTrue(prompt.contains("开始|结束|标题|地点"))
|
|
XCTAssertTrue(prompt.contains("NONE"))
|
|
XCTAssertTrue(prompt.contains("不要把整段原文当成一条日程"))
|
|
}
|
|
|
|
func testEnglishPromptIncludesClockAndNONE() {
|
|
let prompt = AIClipboardSkillCatalog.instruction(
|
|
skillID: AIClipboardSkillCatalog.extractEventsID,
|
|
locale: "en",
|
|
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
|
|
now: now
|
|
)
|
|
XCTAssertTrue(prompt.contains("local timezone"))
|
|
XCTAssertTrue(prompt.contains("NONE"))
|
|
XCTAssertTrue(prompt.contains("all-day"))
|
|
}
|
|
|
|
private func lines(_ raw: String) -> [String] {
|
|
AIEventExtraction.lines(from: raw, now: now, calendar: calendar)
|
|
}
|
|
|
|
private func date(_ y: Int, _ m: Int, _ d: Int, _ h: Int, _ min: Int) -> Date {
|
|
var components = DateComponents()
|
|
components.year = y
|
|
components.month = m
|
|
components.day = d
|
|
components.hour = h
|
|
components.minute = min
|
|
return calendar.date(from: components)!
|
|
}
|
|
}
|