360 lines
12 KiB
Swift
360 lines
12 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
|
|
struct JetBrainsStatusProbeTests {
|
|
@Test
|
|
func `parses quota XML with tariff quota`() throws {
|
|
// Real-world format with tariffQuota containing available credits
|
|
let quotaInfo = [
|
|
"{ "type": "Available",",
|
|
" "current": "7478.3",",
|
|
" "maximum": "1000000",",
|
|
" "until": "2026-11-09T21:00:00Z",",
|
|
" "tariffQuota": {",
|
|
" "current": "7478.3",",
|
|
" "maximum": "1000000",",
|
|
" "available": "992521.7"",
|
|
" } }",
|
|
].joined()
|
|
let nextRefill = [
|
|
"{ "type": "Known",",
|
|
" "next": "2026-01-16T14:00:54.939Z",",
|
|
" "tariff": {",
|
|
" "amount": "1000000",",
|
|
" "duration": "PT720H"",
|
|
" } }",
|
|
].joined()
|
|
|
|
let xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<component name="AIAssistantQuotaManager2">
|
|
<option
|
|
name="quotaInfo"
|
|
value="\(quotaInfo)" />
|
|
<option
|
|
name="nextRefill"
|
|
value="\(nextRefill)" />
|
|
</component>
|
|
</application>
|
|
"""
|
|
|
|
let data = Data(xml.utf8)
|
|
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
|
|
|
#expect(snapshot.quotaInfo.type == "Available")
|
|
#expect(snapshot.quotaInfo.used == 7478.3)
|
|
#expect(snapshot.quotaInfo.maximum == 1_000_000)
|
|
#expect(snapshot.quotaInfo.available == 992_521.7)
|
|
#expect(snapshot.quotaInfo.until != nil)
|
|
|
|
#expect(snapshot.refillInfo?.type == "Known")
|
|
#expect(snapshot.refillInfo?.amount == 1_000_000)
|
|
#expect(snapshot.refillInfo?.duration == "PT720H")
|
|
#expect(snapshot.refillInfo?.next != nil)
|
|
}
|
|
|
|
@Test
|
|
func `parses quota XML without tariff quota`() throws {
|
|
// Fallback format without tariffQuota
|
|
let quotaInfo = [
|
|
"{ "type": "paid",",
|
|
" "current": "50000",",
|
|
" "maximum": "100000",",
|
|
" "until": "2025-12-31T23:59:59Z" }",
|
|
].joined()
|
|
let nextRefill = [
|
|
"{ "type": "monthly",",
|
|
" "next": "2025-01-01T00:00:00Z",",
|
|
" "tariff": {",
|
|
" "amount": "100000",",
|
|
" "duration": "monthly"",
|
|
" } }",
|
|
].joined()
|
|
|
|
let xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<component name="AIAssistantQuotaManager2">
|
|
<option
|
|
name="quotaInfo"
|
|
value="\(quotaInfo)" />
|
|
<option
|
|
name="nextRefill"
|
|
value="\(nextRefill)" />
|
|
</component>
|
|
</application>
|
|
"""
|
|
|
|
let data = Data(xml.utf8)
|
|
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
|
|
|
#expect(snapshot.quotaInfo.type == "paid")
|
|
#expect(snapshot.quotaInfo.used == 50000)
|
|
#expect(snapshot.quotaInfo.maximum == 100_000)
|
|
// Without tariffQuota, available is calculated as maximum - used
|
|
#expect(snapshot.quotaInfo.available == 50000)
|
|
#expect(snapshot.quotaInfo.until != nil)
|
|
|
|
#expect(snapshot.refillInfo?.type == "monthly")
|
|
#expect(snapshot.refillInfo?.amount == 100_000)
|
|
#expect(snapshot.refillInfo?.duration == "monthly")
|
|
}
|
|
|
|
@Test
|
|
func `calculates usage percentage from available`() {
|
|
// available = 75_000, maximum = 100_000 -> 75% remaining, 25% used
|
|
let quotaInfo = JetBrainsQuotaInfo(
|
|
type: "paid",
|
|
used: 25000,
|
|
maximum: 100_000,
|
|
available: 75000,
|
|
until: nil)
|
|
|
|
#expect(quotaInfo.usedPercent == 25.0)
|
|
#expect(quotaInfo.remainingPercent == 75.0)
|
|
}
|
|
|
|
@Test
|
|
func `calculates usage percentage at zero`() {
|
|
let quotaInfo = JetBrainsQuotaInfo(
|
|
type: "paid",
|
|
used: 0,
|
|
maximum: 100_000,
|
|
available: 100_000,
|
|
until: nil)
|
|
|
|
#expect(quotaInfo.usedPercent == 0.0)
|
|
#expect(quotaInfo.remainingPercent == 100.0)
|
|
}
|
|
|
|
@Test
|
|
func `calculates usage percentage at max`() {
|
|
let quotaInfo = JetBrainsQuotaInfo(
|
|
type: "paid",
|
|
used: 100_000,
|
|
maximum: 100_000,
|
|
available: 0,
|
|
until: nil)
|
|
|
|
#expect(quotaInfo.usedPercent == 100.0)
|
|
#expect(quotaInfo.remainingPercent == 0.0)
|
|
}
|
|
|
|
@Test
|
|
func `handles zero maximum`() {
|
|
let quotaInfo = JetBrainsQuotaInfo(
|
|
type: "free",
|
|
used: 1000,
|
|
maximum: 0,
|
|
available: nil,
|
|
until: nil)
|
|
|
|
#expect(quotaInfo.usedPercent == 0.0)
|
|
#expect(quotaInfo.remainingPercent == 100.0)
|
|
}
|
|
|
|
@Test
|
|
func `converts to usage snapshot`() throws {
|
|
let quotaInfo = JetBrainsQuotaInfo(
|
|
type: "Available",
|
|
used: 7478.3,
|
|
maximum: 1_000_000,
|
|
available: 992_521.7,
|
|
until: Date().addingTimeInterval(3600))
|
|
|
|
let refillInfo = JetBrainsRefillInfo(
|
|
type: "Known",
|
|
next: Date().addingTimeInterval(86400),
|
|
amount: 1_000_000,
|
|
duration: "PT720H")
|
|
|
|
let ideInfo = JetBrainsIDEInfo(
|
|
name: "IntelliJ IDEA",
|
|
version: "2025.3",
|
|
basePath: "/test/path",
|
|
quotaFilePath: "/test/path/options/AIAssistantQuotaManager2.xml")
|
|
|
|
let snapshot = JetBrainsStatusSnapshot(
|
|
quotaInfo: quotaInfo,
|
|
refillInfo: refillInfo,
|
|
detectedIDE: ideInfo)
|
|
|
|
let usage = try snapshot.toUsageSnapshot()
|
|
|
|
#expect(usage.primary != nil)
|
|
// usedPercent should be approximately 0.75% (7_478.3 / 1_000_000 * 100)
|
|
#expect(try #require(usage.primary?.usedPercent) < 1.0)
|
|
// Reset date should come from refillInfo.next, not quotaInfo.until
|
|
#expect(usage.primary?.resetsAt != nil)
|
|
#expect(usage.secondary == nil)
|
|
#expect(usage.identity?.providerID == .jetbrains)
|
|
#expect(usage.identity?.accountOrganization == "IntelliJ IDEA 2025.3")
|
|
#expect(usage.identity?.loginMethod == "Available")
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot uses refill date for reset`() throws {
|
|
let refillDate = Date().addingTimeInterval(86400 * 6) // 6 days from now
|
|
let untilDate = Date().addingTimeInterval(86400 * 300) // 300 days from now
|
|
|
|
let quotaInfo = JetBrainsQuotaInfo(
|
|
type: "Available",
|
|
used: 1000,
|
|
maximum: 1_000_000,
|
|
available: 999_000,
|
|
until: untilDate)
|
|
|
|
let refillInfo = JetBrainsRefillInfo(
|
|
type: "Known",
|
|
next: refillDate,
|
|
amount: 1_000_000,
|
|
duration: "PT720H")
|
|
|
|
let snapshot = JetBrainsStatusSnapshot(
|
|
quotaInfo: quotaInfo,
|
|
refillInfo: refillInfo,
|
|
detectedIDE: nil)
|
|
|
|
let usage = try snapshot.toUsageSnapshot()
|
|
|
|
// Reset date should be refillDate (6 days), not untilDate (300 days)
|
|
#expect(usage.primary?.resetsAt == refillDate)
|
|
}
|
|
|
|
@Test
|
|
func `parses IDE directory`() {
|
|
let ides = [
|
|
("IntelliJIdea2024.3", "IntelliJ IDEA", "2024.3"),
|
|
("PyCharm2024.2", "PyCharm", "2024.2"),
|
|
("WebStorm2024.1", "WebStorm", "2024.1"),
|
|
("GoLand2024.3", "GoLand", "2024.3"),
|
|
("CLion2024.2", "CLion", "2024.2"),
|
|
("RustRover2024.3", "RustRover", "2024.3"),
|
|
]
|
|
|
|
for (dirname, expectedName, expectedVersion) in ides {
|
|
let info = JetBrainsIDEInfo(
|
|
name: expectedName,
|
|
version: expectedVersion,
|
|
basePath: "/test/\(dirname)",
|
|
quotaFilePath: "/test/\(dirname)/options/AIAssistantQuotaManager2.xml")
|
|
|
|
#expect(info.name == expectedName)
|
|
#expect(info.version == expectedVersion)
|
|
#expect(info.displayName == "\(expectedName) \(expectedVersion)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `expands tilde in custom path`() async throws {
|
|
let fileManager = FileManager.default
|
|
let home = fileManager.homeDirectoryForCurrentUser
|
|
let testRoot = home
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Caches")
|
|
.appendingPathComponent("CodexBarTests")
|
|
.appendingPathComponent("JetBrains-\(UUID().uuidString)")
|
|
let optionsDir = testRoot.appendingPathComponent("options")
|
|
try fileManager.createDirectory(
|
|
at: optionsDir,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil)
|
|
defer { try? fileManager.removeItem(at: testRoot) }
|
|
|
|
let quotaInfo = [
|
|
"{"type":"free"",
|
|
","current":"0"",
|
|
","maximum":"100000"}",
|
|
].joined()
|
|
let xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<component name="AIAssistantQuotaManager2">
|
|
<option
|
|
name="quotaInfo"
|
|
value="\(quotaInfo)" />
|
|
</component>
|
|
</application>
|
|
"""
|
|
let quotaFile = optionsDir.appendingPathComponent("AIAssistantQuotaManager2.xml")
|
|
try xml.write(to: quotaFile, atomically: true, encoding: .utf8)
|
|
|
|
let tildePath: String
|
|
if testRoot.path.hasPrefix(home.path) {
|
|
let suffix = testRoot.path.dropFirst(home.path.count)
|
|
tildePath = "~\(suffix)"
|
|
} else {
|
|
tildePath = testRoot.path
|
|
}
|
|
|
|
let settings = ProviderSettingsSnapshot.make(
|
|
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(ideBasePath: " \(tildePath) "))
|
|
|
|
let probe = JetBrainsStatusProbe(settings: settings)
|
|
let snapshot = try await probe.fetch()
|
|
|
|
#expect(snapshot.quotaInfo.maximum == 100_000)
|
|
}
|
|
|
|
@Test
|
|
func `handles HTML entities`() throws {
|
|
let quotaInfo = [
|
|
"{"type":"free"",
|
|
","current":"0"",
|
|
","maximum":"50000"}",
|
|
].joined()
|
|
let xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<component name="AIAssistantQuotaManager2">
|
|
<option
|
|
name="quotaInfo"
|
|
value="\(quotaInfo)" />
|
|
</component>
|
|
</application>
|
|
"""
|
|
|
|
let data = Data(xml.utf8)
|
|
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
|
|
|
#expect(snapshot.quotaInfo.type == "free")
|
|
#expect(snapshot.quotaInfo.used == 0)
|
|
#expect(snapshot.quotaInfo.maximum == 50000)
|
|
}
|
|
|
|
@Test
|
|
func `throws on missing quota info`() throws {
|
|
let xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<component name="AIAssistantQuotaManager2">
|
|
</component>
|
|
</application>
|
|
"""
|
|
|
|
let data = Data(xml.utf8)
|
|
#expect(throws: JetBrainsStatusProbeError.noQuotaInfo) {
|
|
_ = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `throws on empty quota info`() throws {
|
|
let xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<component name="AIAssistantQuotaManager2">
|
|
<option name="quotaInfo" value="" />
|
|
</component>
|
|
</application>
|
|
"""
|
|
|
|
let data = Data(xml.utf8)
|
|
#expect(throws: JetBrainsStatusProbeError.noQuotaInfo) {
|
|
_ = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
|
}
|
|
}
|
|
}
|