331 lines
11 KiB
Swift
331 lines
11 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import SQLite3
|
|
import Testing
|
|
|
|
struct WindsurfStatusProbeTests {
|
|
// MARK: - Helper
|
|
|
|
private static func decode(_ json: String) throws -> WindsurfCachedPlanInfo {
|
|
try JSONDecoder().decode(WindsurfCachedPlanInfo.self, from: Data(json.utf8))
|
|
}
|
|
|
|
// MARK: - JSON Decoding
|
|
|
|
@Test
|
|
func `decodes full plan info`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"startTimestamp": 1771610750000,
|
|
"endTimestamp": 1774029950000,
|
|
"usage": {
|
|
"messages": 50000,
|
|
"usedMessages": 35650,
|
|
"remainingMessages": 14350,
|
|
"flowActions": 150000,
|
|
"usedFlowActions": 0,
|
|
"remainingFlowActions": 150000
|
|
},
|
|
"quotaUsage": {
|
|
"dailyRemainingPercent": 9,
|
|
"weeklyRemainingPercent": 54,
|
|
"dailyResetAtUnix": 1774080000,
|
|
"weeklyResetAtUnix": 1774166400
|
|
}
|
|
}
|
|
""")
|
|
|
|
#expect(info.planName == "Pro")
|
|
#expect(info.startTimestamp == 1_771_610_750_000)
|
|
#expect(info.endTimestamp == 1_774_029_950_000)
|
|
#expect(info.usage?.messages == 50000)
|
|
#expect(info.usage?.usedMessages == 35650)
|
|
#expect(info.usage?.remainingMessages == 14350)
|
|
#expect(info.usage?.flowActions == 150_000)
|
|
#expect(info.usage?.usedFlowActions == 0)
|
|
#expect(info.usage?.remainingFlowActions == 150_000)
|
|
#expect(info.quotaUsage?.dailyRemainingPercent == 9)
|
|
#expect(info.quotaUsage?.weeklyRemainingPercent == 54)
|
|
#expect(info.quotaUsage?.dailyResetAtUnix == 1_774_080_000)
|
|
#expect(info.quotaUsage?.weeklyResetAtUnix == 1_774_166_400)
|
|
}
|
|
|
|
@Test
|
|
func `decodes minimal plan info`() throws {
|
|
let info = try Self.decode("""
|
|
{"planName": "Free"}
|
|
""")
|
|
|
|
#expect(info.planName == "Free")
|
|
#expect(info.usage == nil)
|
|
#expect(info.quotaUsage == nil)
|
|
#expect(info.endTimestamp == nil)
|
|
}
|
|
|
|
@Test
|
|
func `decodes empty object`() throws {
|
|
let info = try Self.decode("{}")
|
|
|
|
#expect(info.planName == nil)
|
|
#expect(info.usage == nil)
|
|
#expect(info.quotaUsage == nil)
|
|
}
|
|
|
|
// MARK: - toUsageSnapshot Conversion
|
|
|
|
@Test
|
|
func `converts full plan to usage snapshot`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"startTimestamp": 1771610750000,
|
|
"endTimestamp": 1774029950000,
|
|
"usage": {
|
|
"messages": 50000, "usedMessages": 35650, "remainingMessages": 14350,
|
|
"flowActions": 150000, "usedFlowActions": 0, "remainingFlowActions": 150000
|
|
},
|
|
"quotaUsage": {
|
|
"dailyRemainingPercent": 9, "weeklyRemainingPercent": 54,
|
|
"dailyResetAtUnix": 1774080000, "weeklyResetAtUnix": 1774166400
|
|
}
|
|
}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
// Primary = daily: usedPercent = 100 - 9 = 91
|
|
#expect(snapshot.primary?.usedPercent == 91)
|
|
#expect(snapshot.primary?.resetsAt != nil)
|
|
|
|
// Secondary = weekly: usedPercent = 100 - 54 = 46
|
|
#expect(snapshot.secondary?.usedPercent == 46)
|
|
#expect(snapshot.secondary?.resetsAt != nil)
|
|
|
|
// Identity
|
|
#expect(snapshot.identity?.providerID == .windsurf)
|
|
#expect(snapshot.identity?.loginMethod == "Pro")
|
|
#expect(snapshot.identity?.accountOrganization != nil)
|
|
}
|
|
|
|
@Test
|
|
func `converts minimal plan to usage snapshot`() throws {
|
|
let info = try Self.decode("""
|
|
{"planName": "Free"}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
// Without quotaUsage, primary and secondary should be nil
|
|
#expect(snapshot.primary == nil)
|
|
#expect(snapshot.secondary == nil)
|
|
#expect(snapshot.identity?.loginMethod == "Free")
|
|
#expect(snapshot.identity?.accountOrganization == nil)
|
|
}
|
|
|
|
@Test
|
|
func `converts usage counts when quota usage is absent`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"usage": {
|
|
"messages": 50000,
|
|
"usedMessages": 1200,
|
|
"remainingMessages": 48800,
|
|
"flowActions": 150000,
|
|
"usedFlowActions": 0,
|
|
"remainingFlowActions": 150000,
|
|
"flexCredits": 123700,
|
|
"usedFlexCredits": 0,
|
|
"remainingFlexCredits": 123700
|
|
}
|
|
}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
#expect(snapshot.primary?.usedPercent == 2.4)
|
|
#expect(snapshot.primary?.resetDescription == "1200 / 50000 messages")
|
|
#expect(snapshot.secondary?.usedPercent == 0)
|
|
#expect(snapshot.secondary?.resetDescription == "0 / 150000 flow actions")
|
|
}
|
|
|
|
@Test
|
|
func `usage counts infer used amount from remaining`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"usage": {
|
|
"messages": 100,
|
|
"remainingMessages": 25
|
|
}
|
|
}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
#expect(snapshot.primary?.usedPercent == 75)
|
|
#expect(snapshot.primary?.resetDescription == "75 / 100 messages")
|
|
#expect(snapshot.secondary == nil)
|
|
}
|
|
|
|
@Test
|
|
func `daily at zero remaining shows 100 percent used`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"quotaUsage": {"dailyRemainingPercent": 0, "weeklyRemainingPercent": 100}
|
|
}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
#expect(snapshot.primary?.usedPercent == 100)
|
|
#expect(snapshot.secondary?.usedPercent == 0)
|
|
}
|
|
|
|
@Test
|
|
func `weekly at full remaining shows 0 percent used`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"quotaUsage": {"dailyRemainingPercent": 100, "weeklyRemainingPercent": 100}
|
|
}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
#expect(snapshot.primary?.usedPercent == 0)
|
|
#expect(snapshot.secondary?.usedPercent == 0)
|
|
}
|
|
|
|
@Test
|
|
func `reset dates are correctly converted from unix timestamps`() throws {
|
|
let info = try Self.decode("""
|
|
{
|
|
"planName": "Pro",
|
|
"quotaUsage": {
|
|
"dailyRemainingPercent": 50, "weeklyRemainingPercent": 50,
|
|
"dailyResetAtUnix": 1774080000, "weeklyResetAtUnix": 1774166400
|
|
}
|
|
}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
#expect(snapshot.primary?.resetsAt == Date(timeIntervalSince1970: 1_774_080_000))
|
|
#expect(snapshot.secondary?.resetsAt == Date(timeIntervalSince1970: 1_774_166_400))
|
|
}
|
|
|
|
@Test
|
|
func `end timestamp converts to expiry description`() throws {
|
|
let futureMs = Int64(Date().addingTimeInterval(86400 * 30).timeIntervalSince1970 * 1000)
|
|
let info = try Self.decode("""
|
|
{"planName": "Pro", "endTimestamp": \(futureMs)}
|
|
""")
|
|
|
|
let snapshot = info.toUsageSnapshot()
|
|
|
|
#expect(snapshot.identity?.accountOrganization?.hasPrefix("Expires ") == true)
|
|
}
|
|
|
|
// MARK: - Probe Database Decoding
|
|
|
|
@Test
|
|
func `probe decodes UTF-8 JSON blob`() throws {
|
|
let dbURL = try Self.makeTemporaryDatabase(
|
|
jsonData: Data(#"{"planName":"UTF-8 Pro"}"#.utf8))
|
|
defer { try? FileManager.default.removeItem(at: dbURL.deletingLastPathComponent()) }
|
|
|
|
let info = try WindsurfStatusProbe(dbPath: dbURL.path).fetch()
|
|
|
|
#expect(info.planName == "UTF-8 Pro")
|
|
}
|
|
|
|
@Test
|
|
func `probe decodes UTF-16LE JSON blob`() throws {
|
|
let jsonData = try #require(#"{"planName":"UTF-16 Pro"}"#.data(using: .utf16LittleEndian))
|
|
let dbURL = try Self.makeTemporaryDatabase(jsonData: jsonData)
|
|
defer { try? FileManager.default.removeItem(at: dbURL.deletingLastPathComponent()) }
|
|
|
|
let info = try WindsurfStatusProbe(dbPath: dbURL.path).fetch()
|
|
|
|
#expect(info.planName == "UTF-16 Pro")
|
|
}
|
|
|
|
// MARK: - Probe Error Cases
|
|
|
|
@Test
|
|
func `probe throws dbNotFound for missing file`() {
|
|
let probe = WindsurfStatusProbe(dbPath: "/nonexistent/path/state.vscdb")
|
|
|
|
#expect(throws: WindsurfStatusProbeError.self) {
|
|
_ = try probe.fetch()
|
|
}
|
|
}
|
|
|
|
private static func makeTemporaryDatabase(jsonData: Data) throws -> URL {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("windsurf-status-probe-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
let dbURL = directory.appendingPathComponent("state.vscdb")
|
|
|
|
var db: OpaquePointer?
|
|
guard sqlite3_open(dbURL.path, &db) == SQLITE_OK else {
|
|
throw TestSQLiteError.openFailed(String(cString: sqlite3_errmsg(db)))
|
|
}
|
|
defer { sqlite3_close(db) }
|
|
|
|
try self.execute(
|
|
"""
|
|
CREATE TABLE ItemTable(
|
|
key TEXT PRIMARY KEY,
|
|
value BLOB
|
|
);
|
|
""",
|
|
db: db)
|
|
|
|
var stmt: OpaquePointer?
|
|
guard sqlite3_prepare_v2(
|
|
db,
|
|
"INSERT INTO ItemTable(key, value) VALUES('windsurf.settings.cachedPlanInfo', ?);",
|
|
-1,
|
|
&stmt,
|
|
nil) == SQLITE_OK
|
|
else {
|
|
throw TestSQLiteError.prepareFailed(String(cString: sqlite3_errmsg(db)))
|
|
}
|
|
defer { sqlite3_finalize(stmt) }
|
|
|
|
let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
let bindResult = jsonData.withUnsafeBytes { buffer in
|
|
sqlite3_bind_blob(stmt, 1, buffer.baseAddress, Int32(jsonData.count), transient)
|
|
}
|
|
guard bindResult == SQLITE_OK else {
|
|
throw TestSQLiteError.bindFailed(String(cString: sqlite3_errmsg(db)))
|
|
}
|
|
guard sqlite3_step(stmt) == SQLITE_DONE else {
|
|
throw TestSQLiteError.stepFailed(String(cString: sqlite3_errmsg(db)))
|
|
}
|
|
|
|
return dbURL
|
|
}
|
|
|
|
private static func execute(_ sql: String, db: OpaquePointer?) throws {
|
|
var errorMessage: UnsafeMutablePointer<CChar>?
|
|
guard sqlite3_exec(db, sql, nil, nil, &errorMessage) == SQLITE_OK else {
|
|
defer { sqlite3_free(errorMessage) }
|
|
let message = errorMessage.map { String(cString: $0) } ?? "unknown error"
|
|
throw TestSQLiteError.execFailed(message)
|
|
}
|
|
}
|
|
|
|
private enum TestSQLiteError: Error {
|
|
case openFailed(String)
|
|
case execFailed(String)
|
|
case prepareFailed(String)
|
|
case bindFailed(String)
|
|
case stepFailed(String)
|
|
}
|
|
}
|