196 lines
7.2 KiB
Swift
196 lines
7.2 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
struct MiMoUsageScriptTests {
|
|
@Test
|
|
func `script keeps final cumulative streaming usage`() throws {
|
|
let rows = [
|
|
self.assistantRow(outputTokens: 10),
|
|
self.assistantRow(outputTokens: 40),
|
|
self.assistantRow(outputTokens: 90),
|
|
]
|
|
let allTime = try self.runScript(files: ["session.jsonl": rows])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 120, cacheCreate: 10, cacheRead: 5, output: 90, messages: 1))
|
|
}
|
|
|
|
@Test
|
|
func `script keeps final cumulative streaming usage without session id`() throws {
|
|
let rows = [
|
|
self.assistantRow(outputTokens: 10, sessionID: nil),
|
|
self.assistantRow(outputTokens: 40, sessionID: nil),
|
|
self.assistantRow(outputTokens: 90, sessionID: nil),
|
|
]
|
|
let allTime = try self.runScript(files: ["session.jsonl": rows])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 120, cacheCreate: 10, cacheRead: 5, output: 90, messages: 1))
|
|
}
|
|
|
|
@Test
|
|
func `script keeps final cumulative usage without request id`() throws {
|
|
let rows = [
|
|
self.assistantRow(outputTokens: 10, requestID: nil),
|
|
self.assistantRow(outputTokens: 40, requestID: nil),
|
|
self.assistantRow(outputTokens: 90, requestID: nil),
|
|
]
|
|
let allTime = try self.runScript(files: ["session.jsonl": rows])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 120, cacheCreate: 10, cacheRead: 5, output: 90, messages: 1))
|
|
}
|
|
|
|
@Test
|
|
func `script counts rows without session identity conservatively`() throws {
|
|
let rows = [
|
|
self.assistantRow(outputTokens: 10, sessionID: nil, requestID: nil),
|
|
self.assistantRow(outputTokens: 40, sessionID: nil, requestID: nil),
|
|
self.assistantRow(outputTokens: 90, sessionID: nil, requestID: nil),
|
|
]
|
|
let allTime = try self.runScript(files: ["session.jsonl": rows])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 360, cacheCreate: 30, cacheRead: 15, output: 140, messages: 3))
|
|
}
|
|
|
|
@Test
|
|
func `script keeps distinct requests sharing a message id`() throws {
|
|
let rows = [
|
|
self.assistantRow(outputTokens: 40, requestID: "req_one"),
|
|
self.assistantRow(outputTokens: 90, requestID: "req_two"),
|
|
]
|
|
let allTime = try self.runScript(files: ["session.jsonl": rows])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 240, cacheCreate: 20, cacheRead: 10, output: 130, messages: 2))
|
|
}
|
|
|
|
@Test
|
|
func `script deduplicates copied rows from the same session`() throws {
|
|
let rows = [self.assistantRow(outputTokens: 90)]
|
|
let allTime = try self.runScript(files: [
|
|
"session.jsonl": rows,
|
|
"session-copy.jsonl": rows,
|
|
])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 120, cacheCreate: 10, cacheRead: 5, output: 90, messages: 1))
|
|
}
|
|
|
|
@Test
|
|
func `script deduplicates copied requests from different sessions`() throws {
|
|
let allTime = try self.runScript(files: [
|
|
"session-a.jsonl": [self.assistantRow(outputTokens: 90, sessionID: "session_a")],
|
|
"session-b.jsonl": [self.assistantRow(outputTokens: 90, sessionID: "session_b")],
|
|
])
|
|
|
|
self.assertUsage(
|
|
allTime,
|
|
expected: .init(input: 120, cacheCreate: 10, cacheRead: 5, output: 90, messages: 1))
|
|
}
|
|
|
|
private func runScript(files: [String: [[String: Any]]]) throws -> [String: Any] {
|
|
let root = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("mimo-usage-script-\(UUID().uuidString)")
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let mimoHome = root.appendingPathComponent("mimo")
|
|
let projects = mimoHome
|
|
.appendingPathComponent(".claude")
|
|
.appendingPathComponent("projects")
|
|
.appendingPathComponent("project-a")
|
|
try FileManager.default.createDirectory(at: projects, withIntermediateDirectories: true)
|
|
|
|
for (name, rows) in files {
|
|
let session = projects.appendingPathComponent(name)
|
|
let jsonl = try rows
|
|
.map { try JSONSerialization.data(withJSONObject: $0) }
|
|
.map { try #require(String(bytes: $0, encoding: .utf8)) }
|
|
.joined(separator: "\n")
|
|
try jsonl.write(to: session, atomically: true, encoding: .utf8)
|
|
}
|
|
|
|
let cache = root.appendingPathComponent("usage.json")
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
|
process.arguments = ["python3", self.scriptURL.path, "--update"]
|
|
process.environment = ProcessInfo.processInfo.environment.merging([
|
|
"MIMO_CLAUDE_HOME": mimoHome.path,
|
|
"MIMO_LOCAL_USAGE_PATH": cache.path,
|
|
]) { _, new in new }
|
|
let stderr = Pipe()
|
|
process.standardError = stderr
|
|
|
|
try process.run()
|
|
process.waitUntilExit()
|
|
|
|
let errorText = try #require(String(
|
|
bytes: stderr.fileHandleForReading.readDataToEndOfFile(),
|
|
encoding: .utf8))
|
|
#expect(process.terminationStatus == 0, Comment(rawValue: errorText))
|
|
|
|
let payload = try #require(
|
|
JSONSerialization.jsonObject(with: Data(contentsOf: cache)) as? [String: Any])
|
|
let windows = try #require(payload["windows"] as? [String: Any])
|
|
return try #require(windows["all_time"] as? [String: Any])
|
|
}
|
|
|
|
private func assertUsage(_ allTime: [String: Any], expected: UsageExpectation) {
|
|
#expect(allTime["input"] as? Int == expected.input)
|
|
#expect(allTime["cache_create"] as? Int == expected.cacheCreate)
|
|
#expect(allTime["cache_read"] as? Int == expected.cacheRead)
|
|
#expect(allTime["output"] as? Int == expected.output)
|
|
#expect(allTime["messages"] as? Int == expected.messages)
|
|
}
|
|
|
|
private var scriptURL: URL {
|
|
URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("Scripts/mimo-usage.py")
|
|
}
|
|
|
|
private func assistantRow(
|
|
outputTokens: Int,
|
|
sessionID: String? = "session_stream",
|
|
requestID: String? = "req_stream") -> [String: Any]
|
|
{
|
|
var row: [String: Any] = [
|
|
"type": "assistant",
|
|
"timestamp": ISO8601DateFormatter().string(from: Date()),
|
|
"message": [
|
|
"id": "msg_stream",
|
|
"usage": [
|
|
"input_tokens": 120,
|
|
"cache_creation_input_tokens": 10,
|
|
"cache_read_input_tokens": 5,
|
|
"output_tokens": outputTokens,
|
|
],
|
|
],
|
|
]
|
|
if let sessionID {
|
|
row["sessionId"] = sessionID
|
|
}
|
|
if let requestID {
|
|
row["requestId"] = requestID
|
|
}
|
|
return row
|
|
}
|
|
|
|
private struct UsageExpectation {
|
|
let input: Int
|
|
let cacheCreate: Int
|
|
let cacheRead: Int
|
|
let output: Int
|
|
let messages: Int
|
|
}
|
|
}
|