Generalize the photos-originals rclone-mount pattern to a video-projects prefix so the video studio (and imajin ETL, per storage-portability-plan §2.3) can read/write multi-GB project sources/renders as local files while only hot data stays resident on plum (bounded VFS LRU cache). Lets a small-disk laptop work with large footage without filling APFS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
168 lines
7.3 KiB
Swift
168 lines
7.3 KiB
Swift
import Foundation
|
|
import GRDB
|
|
import Testing
|
|
@testable import ICallsSync
|
|
|
|
@Suite("ICallsSync CallRecord and stats")
|
|
struct CallRecordTests {
|
|
@Test("SyncCallPayload round-trips basic fields")
|
|
func payloadBasics() {
|
|
let rec = SyncCallPayload(
|
|
uniqueId: "zpk:42",
|
|
address: "+14155551234",
|
|
normalizedAddress: "+14155551234",
|
|
contactName: "Alice",
|
|
direction: "incoming",
|
|
callType: "telephony",
|
|
answered: true,
|
|
durationSeconds: 127.5,
|
|
startedAt: "2026-06-01T12:00:00Z",
|
|
serviceProvider: "com.apple.Telephony"
|
|
)
|
|
#expect(rec.uniqueId == "zpk:42")
|
|
#expect(rec.direction == "incoming")
|
|
#expect(rec.answered == true)
|
|
#expect(rec.durationSeconds == 127.5)
|
|
}
|
|
|
|
@Test("ICallsSyncStats and error defaults")
|
|
func statsAndErrors() {
|
|
var stats = ICallsSyncStats()
|
|
#expect(stats.callCount == 0)
|
|
stats.callCount = 17
|
|
stats.syncedThisSession = 5
|
|
#expect(stats.callCount == 17)
|
|
|
|
let err = ICallsSyncError.databaseNotFound
|
|
#expect(err.message == "Call history database not found")
|
|
#expect(ICallsSyncError.fullDiskAccessRequired.message == "Full Disk Access required (Call History)")
|
|
#expect(ICallsSyncError.none.message == "")
|
|
}
|
|
}
|
|
|
|
@Suite("ICallsSync call-type classification")
|
|
struct CallTypeClassificationTests {
|
|
@Test("FaceTime audio is ZCALLTYPE 16, video otherwise")
|
|
func faceTime() {
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.FaceTime", callTypeRaw: 16) == "facetime_audio")
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.FaceTime", callTypeRaw: 1) == "facetime_video")
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.FaceTime", callTypeRaw: 0) == "facetime_video")
|
|
}
|
|
|
|
@Test("Cellular maps to telephony")
|
|
func telephony() {
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: "com.apple.Telephony", callTypeRaw: 0) == "telephony")
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: nil, callTypeRaw: 1) == "telephony")
|
|
}
|
|
|
|
@Test("Unknown provider with no telephony hint is unknown")
|
|
func unknown() {
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: nil, callTypeRaw: 0) == "unknown")
|
|
#expect(CallHistoryReader.classifyCallType(serviceProvider: "com.example.weird", callTypeRaw: 7) == "unknown")
|
|
}
|
|
}
|
|
|
|
@Suite("ICallsSync row parsing")
|
|
struct CallRecordParsingTests {
|
|
@Test("ZORIGINATED + ZANSWERED map to direction/answered")
|
|
func directionAndAnswered() {
|
|
let outgoing = CallHistoryReader.parse(
|
|
row: Row(["Z_PK": 1, "ZUNIQUE_ID": "u1", "ZADDRESS": "+14155550000",
|
|
"ZORIGINATED": 1, "ZANSWERED": 1, "ZDURATION": 30.0, "ZDATE": 700_000_000.0]),
|
|
contactLookup: { _ in nil })
|
|
#expect(outgoing.direction == "outgoing")
|
|
#expect(outgoing.answered == true)
|
|
#expect(outgoing.uniqueId == "u1")
|
|
|
|
let missedIncoming = CallHistoryReader.parse(
|
|
row: Row(["Z_PK": 2, "ZORIGINATED": 0, "ZANSWERED": 0, "ZDATE": 700_000_100.0]),
|
|
contactLookup: { _ in nil })
|
|
#expect(missedIncoming.direction == "incoming")
|
|
#expect(missedIncoming.answered == false)
|
|
}
|
|
|
|
@Test("Empty ZUNIQUE_ID falls back to synthetic zpk id")
|
|
func syntheticUniqueId() {
|
|
let rec = CallHistoryReader.parse(
|
|
row: Row(["Z_PK": 99, "ZUNIQUE_ID": "", "ZDATE": 700_000_000.0]),
|
|
contactLookup: { _ in nil })
|
|
#expect(rec.uniqueId == "zpk:99")
|
|
}
|
|
|
|
@Test("Contact lookup fills name when ZNAME is absent")
|
|
func contactEnrichment() {
|
|
let rec = CallHistoryReader.parse(
|
|
row: Row(["Z_PK": 3, "ZADDRESS": "+14155551234", "ZDATE": 700_000_000.0]),
|
|
contactLookup: { $0 == "+14155551234" ? "Bob" : nil })
|
|
#expect(rec.contactName == "Bob")
|
|
}
|
|
}
|
|
|
|
@Suite("ICallsSync reader snapshot (WAL visibility)")
|
|
struct CallHistorySnapshotTests {
|
|
/// Build a ZCALLRECORD fixture in WAL mode with autocheckpoint disabled, so
|
|
/// inserted rows live in the `-wal` file. The writer queue is kept alive by
|
|
/// the caller so the WAL is not flushed before the reader snapshots it —
|
|
/// this is exactly the stale-read condition the snapshot fix exists to beat.
|
|
private func makeFixture(at path: String) throws -> DatabaseQueue {
|
|
let q = try DatabaseQueue(path: path)
|
|
try q.writeWithoutTransaction { db in
|
|
_ = try String.fetchOne(db, sql: "PRAGMA journal_mode = WAL")
|
|
try db.execute(sql: "PRAGMA wal_autocheckpoint = 0")
|
|
}
|
|
try q.write { db in
|
|
try db.execute(sql: """
|
|
CREATE TABLE ZCALLRECORD (
|
|
Z_PK INTEGER PRIMARY KEY,
|
|
ZUNIQUE_ID TEXT, ZADDRESS TEXT, ZNAME TEXT,
|
|
ZORIGINATED INTEGER, ZANSWERED INTEGER, ZDURATION REAL,
|
|
ZDATE REAL, ZCALLTYPE INTEGER, ZSERVICE_PROVIDER TEXT
|
|
)
|
|
""")
|
|
try db.execute(sql: """
|
|
INSERT INTO ZCALLRECORD
|
|
(Z_PK, ZUNIQUE_ID, ZADDRESS, ZORIGINATED, ZANSWERED, ZDURATION, ZDATE, ZCALLTYPE, ZSERVICE_PROVIDER)
|
|
VALUES
|
|
(1, 'older', '+14155550001', 0, 1, 12.0, 700000000.0, 0, 'com.apple.Telephony'),
|
|
(2, 'newer', '+14155550002', 1, 1, 34.0, 700000500.0, 16, 'com.apple.FaceTime')
|
|
""")
|
|
}
|
|
return q
|
|
}
|
|
|
|
@Test("Reader sees rows still sitting in the WAL")
|
|
func readsUnflushedWal() throws {
|
|
let dir = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("icalls-fixture-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: dir) }
|
|
let dbPath = dir.appendingPathComponent("CallHistory.storedata").path
|
|
|
|
let writer = try makeFixture(at: dbPath)
|
|
defer { try? writer.close() }
|
|
|
|
let calls = CallHistoryReader.shared.readCalls(dbPath: dbPath, since: nil)
|
|
#expect(calls.count == 2)
|
|
// Oldest-first (ORDER BY ZDATE ASC) so the server ingests in order.
|
|
#expect(calls.first?.uniqueId == "older")
|
|
#expect(calls.last?.callType == "facetime_audio")
|
|
}
|
|
|
|
@Test("since filters out rows at or before the watermark")
|
|
func sinceWatermark() throws {
|
|
let dir = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("icalls-fixture-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: dir) }
|
|
let dbPath = dir.appendingPathComponent("CallHistory.storedata").path
|
|
|
|
let writer = try makeFixture(at: dbPath)
|
|
defer { try? writer.close() }
|
|
|
|
// Between the two rows (700000000 < 700000250 < 700000500).
|
|
let watermark = Date(timeIntervalSinceReferenceDate: 700_000_250.0)
|
|
let calls = CallHistoryReader.shared.readCalls(dbPath: dbPath, since: watermark)
|
|
#expect(calls.count == 1)
|
|
#expect(calls.first?.uniqueId == "newer")
|
|
}
|
|
}
|