Import Coucou 0.1.9 (Louis-CFM/coucou@83708fe), Mac app only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum AskQuestionTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func check(_ label: String, _ got: String, _ expected: String) {
|
||||
if got == expected {
|
||||
print(" ✓ \(label)")
|
||||
} else {
|
||||
print(" ✗ \(label)")
|
||||
print(" got: \(got.debugDescription)")
|
||||
print(" expected: \(expected.debugDescription)")
|
||||
failures += 1
|
||||
}
|
||||
}
|
||||
|
||||
static func checkTrue(_ label: String, _ value: Bool) {
|
||||
if value { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
static func main() {
|
||||
let opt1: [String: Any] = ["label": "Postgres", "description": "Full-text search"]
|
||||
let opt2: [String: Any] = ["label": "Meilisearch", "description": ""]
|
||||
let opt3: [String: Any] = ["label": "Algolia", "description": "Managed search"]
|
||||
|
||||
// ── parse ──────────────────────────────────────────────────────────────
|
||||
print("AskQuestion.parse")
|
||||
|
||||
let single: [String: Any] = ["questions": [
|
||||
["question": "Which search engine?", "header": "Search", "options": [opt1, opt2, opt3], "multiSelect": false]
|
||||
]]
|
||||
let parsed = AskQuestion.parse(toolInput: single)
|
||||
checkTrue("single question parses", parsed != nil)
|
||||
checkTrue("question text", parsed?.questions[0].question == "Which search engine?")
|
||||
checkTrue("header", parsed?.questions[0].header == "Search")
|
||||
checkTrue("3 options", parsed?.questions[0].options.count == 3)
|
||||
checkTrue("multiSelect false", parsed?.questions[0].multiSelect == false)
|
||||
checkTrue("option label", parsed?.questions[0].options[0].label == "Postgres")
|
||||
|
||||
// header truncated at 12 chars
|
||||
let longHeader: [String: Any] = ["questions": [
|
||||
["question": "Q?", "header": "This is a very long header", "options": [opt1, opt2], "multiSelect": false]
|
||||
]]
|
||||
let lh = AskQuestion.parse(toolInput: longHeader)
|
||||
checkTrue("header truncated to 12", lh?.questions[0].header.count == 12)
|
||||
|
||||
// multiSelect true
|
||||
let multi: [String: Any] = ["questions": [
|
||||
["question": "Pick features", "header": "Features", "options": [opt1, opt2, opt3], "multiSelect": true]
|
||||
]]
|
||||
let mParsed = AskQuestion.parse(toolInput: multi)
|
||||
checkTrue("multiSelect true", mParsed?.questions[0].multiSelect == true)
|
||||
|
||||
// 4 questions (max)
|
||||
let q: [String: Any] = ["question": "Q?", "header": "", "options": [opt1, opt2], "multiSelect": false]
|
||||
let four: [String: Any] = ["questions": [q, q, q, q]]
|
||||
checkTrue("4 questions parses", AskQuestion.parse(toolInput: four) != nil)
|
||||
|
||||
// 5 questions → nil (over max)
|
||||
let five: [String: Any] = ["questions": [q, q, q, q, q]]
|
||||
checkTrue("5 questions → nil", AskQuestion.parse(toolInput: five) == nil)
|
||||
|
||||
// 1 option → nil (under min)
|
||||
let oneOpt: [String: Any] = ["questions": [
|
||||
["question": "Q?", "header": "", "options": [opt1], "multiSelect": false]
|
||||
]]
|
||||
checkTrue("1 option → nil", AskQuestion.parse(toolInput: oneOpt) == nil)
|
||||
|
||||
// missing question text → nil
|
||||
let noQ: [String: Any] = ["questions": [
|
||||
["header": "", "options": [opt1, opt2], "multiSelect": false]
|
||||
]]
|
||||
checkTrue("missing question → nil", AskQuestion.parse(toolInput: noQ) == nil)
|
||||
|
||||
// empty questions array → nil
|
||||
checkTrue("empty questions → nil", AskQuestion.parse(toolInput: ["questions": []]) == nil)
|
||||
|
||||
// missing questions key → nil
|
||||
checkTrue("missing questions → nil", AskQuestion.parse(toolInput: [:]) == nil)
|
||||
|
||||
// option with empty label → nil
|
||||
let emptyLabel: [String: Any] = ["questions": [
|
||||
["question": "Q?", "header": "", "options": [["label": "", "description": ""], opt2], "multiSelect": false]
|
||||
]]
|
||||
checkTrue("empty option label → nil", AskQuestion.parse(toolInput: emptyLabel) == nil)
|
||||
|
||||
// ── buildAnswers ───────────────────────────────────────────────────────
|
||||
print("AskQuestion.buildAnswers")
|
||||
|
||||
let items = AskQuestion.parse(toolInput: multi)!.questions
|
||||
+ AskQuestion.parse(toolInput: single)!.questions
|
||||
|
||||
// items[0] = "Pick features" (multiSelect: true), items[1] = "Which search engine?" (multiSelect: false)
|
||||
let sel1 = [["Postgres"], ["Meilisearch"]]
|
||||
let ans1 = AskQuestion.buildAnswers(questions: items, selections: sel1)
|
||||
checkTrue("multi-select single pick → [String]", (ans1["Pick features"] as? [String]) == ["Postgres"])
|
||||
checkTrue("single-select answer → String", (ans1["Which search engine?"] as? String) == "Meilisearch")
|
||||
|
||||
// Multi-select with multiple picks → [String] array
|
||||
let sel2 = [["Postgres", "Algolia"], []]
|
||||
let ans2 = AskQuestion.buildAnswers(questions: items, selections: sel2)
|
||||
checkTrue("multi-select multiple picks → [String]", (ans2["Pick features"] as? [String]) == ["Postgres", "Algolia"])
|
||||
checkTrue("unanswered question omitted", ans2["Which search engine?"] == nil)
|
||||
|
||||
// Empty selections → empty dict
|
||||
let ans3 = AskQuestion.buildAnswers(questions: items, selections: [[], []])
|
||||
checkTrue("all empty → empty dict", ans3.isEmpty)
|
||||
|
||||
// ── finish ─────────────────────────────────────────────────────────────
|
||||
if failures == 0 {
|
||||
print("\nAll tests passed.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("\n\(failures) test(s) failed.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Test harness
|
||||
|
||||
@main
|
||||
enum ChatParsingTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func check(_ label: String, _ got: String, _ expected: String) {
|
||||
if got == expected {
|
||||
print(" ✓ \(label)")
|
||||
} else {
|
||||
print(" ✗ \(label)")
|
||||
print(" got: \(got.debugDescription)")
|
||||
print(" expected: \(expected.debugDescription)")
|
||||
failures += 1
|
||||
}
|
||||
}
|
||||
|
||||
static func checkTrue(_ label: String, _ value: Bool) {
|
||||
if value { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
// MARK: - Entry point
|
||||
|
||||
static func main() async {
|
||||
|
||||
// ── Unit tests (no network) ──────────────────────────────────────────
|
||||
|
||||
print("LocalChat.normaliseURL")
|
||||
check("strips trailing slash", LocalChat.normaliseURL("http://localhost:11434/"), "http://localhost:11434")
|
||||
check("strips /api suffix", LocalChat.normaliseURL("http://localhost:11434/api"), "http://localhost:11434")
|
||||
check("strips /v1 suffix", LocalChat.normaliseURL("http://localhost:1234/v1"), "http://localhost:1234")
|
||||
check("no-op clean URL", LocalChat.normaliseURL("http://localhost:11434"), "http://localhost:11434")
|
||||
check("trims whitespace", LocalChat.normaliseURL(" http://localhost:11434 "), "http://localhost:11434")
|
||||
|
||||
print("LocalChat.parseSSEDelta")
|
||||
let sseData = #"data: {"id":"1","choices":[{"delta":{"content":"hello"}}]}"#
|
||||
check("parses delta", LocalChat.parseSSEDelta(sseData) ?? "", "hello")
|
||||
checkTrue("ignores [DONE]", LocalChat.parseSSEDelta("data: [DONE]") == nil)
|
||||
checkTrue("ignores non-data", LocalChat.parseSSEDelta(": heartbeat") == nil)
|
||||
checkTrue("ignores null content",
|
||||
LocalChat.parseSSEDelta(#"data: {"choices":[{"delta":{"content":null}}]}"#) == nil)
|
||||
checkTrue("ignores missing content",
|
||||
LocalChat.parseSSEDelta(#"data: {"choices":[{"delta":{}}]}"#) == nil)
|
||||
|
||||
print("LocalChat.filterThinkingBlocks")
|
||||
check("removes closed block",
|
||||
LocalChat.filterThinkingBlocks("<think>internal</think>answer"), "answer")
|
||||
check("no-op without block",
|
||||
LocalChat.filterThinkingBlocks("hello"), "hello")
|
||||
check("multiline block",
|
||||
LocalChat.filterThinkingBlocks("<think>\nstep1\nstep2\n</think>result"), "result")
|
||||
|
||||
print("LocalChat.progressiveFilter")
|
||||
check("open block → hide",
|
||||
LocalChat.progressiveFilter("<think>\nstep one"), "")
|
||||
check("open after text → keep prefix",
|
||||
LocalChat.progressiveFilter("visible<think>hidden"), "visible")
|
||||
check("closed block removed",
|
||||
LocalChat.progressiveFilter("<think>done</think>answer"), "answer")
|
||||
|
||||
print("ChatMarkdown.parse")
|
||||
let blocks = ChatMarkdown.parse(
|
||||
"## Hello\n\nThis is a paragraph.\n\n- item 1\n- item 2\n\n```swift\nlet x = 1\n```")
|
||||
checkTrue("heading count", blocks.filter { if case .heading = $0 { return true }; return false }.count == 1)
|
||||
checkTrue("paragraph count", blocks.filter { if case .paragraph = $0 { return true }; return false }.count == 1)
|
||||
checkTrue("list item count", blocks.filter { if case .listItem = $0 { return true }; return false }.count == 2)
|
||||
checkTrue("code block count", blocks.filter { if case .codeBlock = $0 { return true }; return false }.count == 1)
|
||||
if case .heading(let level, let text) =
|
||||
blocks.first(where: { if case .heading = $0 { return true }; return false })! {
|
||||
checkTrue("heading level 2", level == 2)
|
||||
checkTrue("heading text", text == "Hello")
|
||||
} else { print(" ✗ heading not found"); failures += 1 }
|
||||
|
||||
print("ChatMarkdown.parse — extended")
|
||||
// Numbered list preserves number
|
||||
let numBlocks = ChatMarkdown.parse("1. first\n2. second")
|
||||
let numItems = numBlocks.filter { if case .listItem = $0 { return true }; return false }
|
||||
checkTrue("ordered list count", numItems.count == 2)
|
||||
if case .listItem(let prefix, _, _) = numItems.first! {
|
||||
checkTrue("ordered prefix is '1.'", prefix == "1.")
|
||||
}
|
||||
// Heading requires space after #
|
||||
checkTrue("heading with space", ChatMarkdown.parse("## Hi").contains { if case .heading = $0 { return true }; return false })
|
||||
checkTrue("#nospace is paragraph", ChatMarkdown.parse("#nospace").contains { if case .paragraph = $0 { return true }; return false })
|
||||
// Nested list indent
|
||||
let nested = ChatMarkdown.parse("- top\n - nested")
|
||||
let items = nested.filter { if case .listItem = $0 { return true }; return false }
|
||||
checkTrue("nested list count", items.count == 2)
|
||||
if case .listItem(_, _, let indent) = items[1] { checkTrue("nested indent = 1", indent == 1) }
|
||||
// Blockquote
|
||||
let qBlocks = ChatMarkdown.parse("> quoted text")
|
||||
checkTrue("blockquote parsed", qBlocks.contains { if case .quote = $0 { return true }; return false })
|
||||
if case .quote(let text) = qBlocks.first! { checkTrue("quote text", text == "quoted text") }
|
||||
// Paragraph stops before ordered list
|
||||
let mixBlocks = ChatMarkdown.parse("intro\n1. item")
|
||||
checkTrue("paragraph + ordered list", mixBlocks.filter { if case .paragraph = $0 { return true }; return false }.count == 1
|
||||
&& mixBlocks.filter { if case .listItem = $0 { return true }; return false }.count == 1)
|
||||
// progressiveFilter hides open think block
|
||||
check("open think → empty", LocalChat.progressiveFilter("<think>\nhalf"), "")
|
||||
check("open after text", LocalChat.progressiveFilter("answer<think>hidden"), "answer")
|
||||
check("closed think removed", LocalChat.progressiveFilter("<think>done</think>result"), "result")
|
||||
|
||||
// ── End-to-end tests (fake server) ───────────────────────────────────
|
||||
|
||||
let baseURL: String = {
|
||||
guard CommandLine.arguments.count > 1 else { return "" }
|
||||
return "http://127.0.0.1:\(CommandLine.arguments[1])"
|
||||
}()
|
||||
|
||||
guard !baseURL.isEmpty else {
|
||||
print("\n(Skipping end-to-end tests — no server port provided.)")
|
||||
finish()
|
||||
}
|
||||
|
||||
print("LocalChat.fetchModels (fake server)")
|
||||
|
||||
let models = await LocalChat.fetchModels(baseURL: baseURL)
|
||||
checkTrue("models list non-empty", !models.isEmpty)
|
||||
checkTrue("llama3.2 present", models.contains { $0.id == "llama3.2" })
|
||||
checkTrue("nomic-embed-text filtered", !models.contains { $0.id == "nomic-embed-text" })
|
||||
|
||||
print("LocalChat.streamChat — happy path (fake server)")
|
||||
|
||||
var tokens: [String] = []
|
||||
do {
|
||||
let response = try await LocalChat.streamChat(
|
||||
baseURL: baseURL,
|
||||
model: "llama3.2",
|
||||
messages: [["role": "user", "content": "hello"]],
|
||||
onToken: { visible in tokens.append(visible) }
|
||||
)
|
||||
checkTrue("sent multiple tokens", tokens.count > 1)
|
||||
checkTrue("intermediate tokens non-empty", tokens.contains { !$0.isEmpty })
|
||||
checkTrue("think block removed from response", !response.contains("<think>"))
|
||||
checkTrue("markdown heading in response", response.contains("## Answer"))
|
||||
checkTrue("list item in response", response.contains("- **item 1**"))
|
||||
checkTrue("code block in response", response.contains("```python"))
|
||||
} catch {
|
||||
print(" ✗ unexpected error: \(error)")
|
||||
failures += 1
|
||||
}
|
||||
|
||||
print("LocalChat.streamChat — unknown model (fake server)")
|
||||
|
||||
do {
|
||||
_ = try await LocalChat.streamChat(
|
||||
baseURL: baseURL,
|
||||
model: "unknown-model",
|
||||
messages: [["role": "user", "content": "hello"]],
|
||||
onToken: { _ in }
|
||||
)
|
||||
print(" ✗ should have thrown for unknown model")
|
||||
failures += 1
|
||||
} catch let e as LocalChatError {
|
||||
if case .modelNotFound(let m) = e {
|
||||
checkTrue("model name in error", m == "unknown-model")
|
||||
} else {
|
||||
print(" ✗ wrong error case: \(e)")
|
||||
failures += 1
|
||||
}
|
||||
} catch {
|
||||
print(" ✗ unexpected error type: \(error)")
|
||||
failures += 1
|
||||
}
|
||||
|
||||
print("LocalChat.streamChat — unreachable server")
|
||||
|
||||
do {
|
||||
_ = try await LocalChat.streamChat(
|
||||
baseURL: "http://127.0.0.1:1", // nothing on port 1
|
||||
model: "llama3.2",
|
||||
messages: [["role": "user", "content": "hello"]],
|
||||
onToken: { _ in }
|
||||
)
|
||||
print(" ✗ should have thrown for unreachable server")
|
||||
failures += 1
|
||||
} catch let e as LocalChatError {
|
||||
if case .serverUnreachable = e {
|
||||
print(" ✓ serverUnreachable error")
|
||||
} else {
|
||||
print(" ✗ wrong error case: \(e)")
|
||||
failures += 1
|
||||
}
|
||||
} catch {
|
||||
print(" ✗ unexpected error type: \(error)")
|
||||
failures += 1
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
// MARK: - Finish
|
||||
|
||||
private static func finish() -> Never {
|
||||
if failures == 0 {
|
||||
print("\nAll tests passed.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("\n\(failures) test(s) failed.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum ClaudePlanGaugeTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func check(_ label: String, _ got: String, _ expected: String) {
|
||||
if got == expected {
|
||||
print(" ✓ \(label)")
|
||||
} else {
|
||||
print(" ✗ \(label)")
|
||||
print(" got: \(got.debugDescription)")
|
||||
print(" expected: \(expected.debugDescription)")
|
||||
failures += 1
|
||||
}
|
||||
}
|
||||
|
||||
static func checkTrue(_ label: String, _ value: Bool) {
|
||||
if value { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
static func main() {
|
||||
let futureEpoch: Double = Date().timeIntervalSince1970 + 7200 // 2 hours from now
|
||||
let pastEpoch: Double = Date().timeIntervalSince1970 - 100 // already passed
|
||||
|
||||
// ── parse ──────────────────────────────────────────────────────────────
|
||||
print("ClaudePlanGauge.parse")
|
||||
|
||||
let full: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": 23.5, "resets_at": futureEpoch],
|
||||
"seven_day": ["used_percentage": 67.0, "resets_at": futureEpoch],
|
||||
]]
|
||||
let usage = ClaudePlanGauge.parse(payload: full)
|
||||
checkTrue("parse full payload → non-nil", usage != nil)
|
||||
checkTrue("five_hour usedPct 23.5", usage?.fiveHour?.usedPct == 23.5)
|
||||
checkTrue("seven_day usedPct 67.0", usage?.sevenDay?.usedPct == 67.0)
|
||||
|
||||
// Missing rate_limits → nil
|
||||
checkTrue("missing rate_limits → nil", ClaudePlanGauge.parse(payload: [:]) == nil)
|
||||
|
||||
// Absurd pct (>200) → nil window (150 is now clamped to 100)
|
||||
let badPct: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": 250.0, "resets_at": futureEpoch],
|
||||
]]
|
||||
let badUsage = ClaudePlanGauge.parse(payload: badPct)
|
||||
checkTrue("absurd pct (250) → nil window", badUsage?.fiveHour == nil)
|
||||
|
||||
// pct 150 → clamped to 100 (not rejected)
|
||||
let clampPct: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": 150.0, "resets_at": futureEpoch],
|
||||
]]
|
||||
let clampUsage = ClaudePlanGauge.parse(payload: clampPct)
|
||||
checkTrue("pct 150 → clamped to 100", clampUsage?.fiveHour?.usedPct == 100.0)
|
||||
|
||||
// pct 201 → rejected
|
||||
let tooBig: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": 201.0, "resets_at": futureEpoch],
|
||||
]]
|
||||
checkTrue("pct 201 → nil window", ClaudePlanGauge.parse(payload: tooBig)?.fiveHour == nil)
|
||||
|
||||
// resets_at > 400 days → rejected
|
||||
let farFuture = Date().timeIntervalSince1970 + 401 * 86400
|
||||
let msEpoch: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": 50.0, "resets_at": farFuture],
|
||||
]]
|
||||
checkTrue("resets_at > 400d → nil window", ClaudePlanGauge.parse(payload: msEpoch)?.fiveHour == nil)
|
||||
|
||||
// Negative pct → nil window
|
||||
let negPct: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": -5.0, "resets_at": futureEpoch],
|
||||
]]
|
||||
checkTrue("negative pct → nil window", ClaudePlanGauge.parse(payload: negPct)?.fiveHour == nil)
|
||||
|
||||
// Int pct accepted
|
||||
let intPct: [String: Any] = ["rate_limits": [
|
||||
"five_hour": ["used_percentage": 42, "resets_at": Int(futureEpoch)],
|
||||
]]
|
||||
let intUsage = ClaudePlanGauge.parse(payload: intPct)
|
||||
checkTrue("Int pct parsed", intUsage?.fiveHour?.usedPct == 42.0)
|
||||
|
||||
// ── effectivePct (expired window) ─────────────────────────────────────
|
||||
print("ClaudePlanGauge.effectivePct")
|
||||
|
||||
let expiredWindow = PlanWindow(usedPct: 80, resetsAt: Date(timeIntervalSince1970: pastEpoch))
|
||||
let futureWindow = PlanWindow(usedPct: 80, resetsAt: Date(timeIntervalSince1970: futureEpoch))
|
||||
checkTrue("expired window → 0", ClaudePlanGauge.effectivePct(expiredWindow) == 0)
|
||||
checkTrue("future window → original pct", ClaudePlanGauge.effectivePct(futureWindow) == 80)
|
||||
|
||||
// ── dominantPct ────────────────────────────────────────────────────────
|
||||
print("ClaudePlanGauge.dominantPct")
|
||||
|
||||
let both = PlanUsage(fiveHour: PlanWindow(usedPct: 30, resetsAt: Date(timeIntervalSince1970: futureEpoch)),
|
||||
sevenDay: PlanWindow(usedPct: 70, resetsAt: Date(timeIntervalSince1970: futureEpoch)),
|
||||
updatedAt: Date())
|
||||
let fhOnly = PlanUsage(fiveHour: PlanWindow(usedPct: 55, resetsAt: Date(timeIntervalSince1970: futureEpoch)),
|
||||
sevenDay: nil, updatedAt: Date())
|
||||
let empty = PlanUsage(fiveHour: nil, sevenDay: nil, updatedAt: Date())
|
||||
|
||||
checkTrue("dominant picks higher (70)", ClaudePlanGauge.dominantPct(both) == 70)
|
||||
checkTrue("dominant with one window (55)", ClaudePlanGauge.dominantPct(fhOnly) == 55)
|
||||
checkTrue("dominant with no windows → nil", ClaudePlanGauge.dominantPct(empty) == nil)
|
||||
|
||||
// ── color ──────────────────────────────────────────────────────────────
|
||||
print("ClaudePlanGauge.color")
|
||||
|
||||
check("nil → grey", ClaudePlanGauge.color(for: nil), "#6B7079")
|
||||
check("0 → green", ClaudePlanGauge.color(for: 0), "#22C55E")
|
||||
check("49 → green", ClaudePlanGauge.color(for: 49), "#22C55E")
|
||||
check("50 → orange", ClaudePlanGauge.color(for: 50), "#F59E0B")
|
||||
check("79 → orange", ClaudePlanGauge.color(for: 79), "#F59E0B")
|
||||
check("80 → red", ClaudePlanGauge.color(for: 80), "#F4505E")
|
||||
check("100 → red", ClaudePlanGauge.color(for: 100), "#F4505E")
|
||||
|
||||
// ── pillLabel ──────────────────────────────────────────────────────────
|
||||
print("ClaudePlanGauge.pillLabel")
|
||||
|
||||
check("nil usage → 'Claude plan'", ClaudePlanGauge.pillLabel(nil), "Claude plan")
|
||||
check("no windows → 'Claude plan'", ClaudePlanGauge.pillLabel(empty), "Claude plan")
|
||||
check("70 pct → 'Claude 70%'", ClaudePlanGauge.pillLabel(both), "Claude 70%")
|
||||
check("55 pct → 'Claude 55%'", ClaudePlanGauge.pillLabel(fhOnly), "Claude 55%")
|
||||
|
||||
// ── finish ─────────────────────────────────────────────────────────────
|
||||
if failures == 0 {
|
||||
print("\nAll tests passed.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("\n\(failures) test(s) failed.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum DesktopMochiTests {
|
||||
static func main() {
|
||||
testShouldSleep()
|
||||
testIsOverBody()
|
||||
testLookOrigin()
|
||||
testClampOrigin()
|
||||
testShouldRetractOnLanding()
|
||||
print("DesktopMochiLogic: all cases passed")
|
||||
}
|
||||
|
||||
// MARK: - shouldSleep
|
||||
|
||||
static func testShouldSleep() {
|
||||
// Agent recently active → awake regardless of mouse distance
|
||||
precondition(!DesktopMochiLogic.shouldSleep(lastAgentActiveInterval: 5, mouseDistanceToPanelCenter: 300),
|
||||
"active agent must not sleep")
|
||||
|
||||
// Long idle but mouse near panel → awake
|
||||
precondition(!DesktopMochiLogic.shouldSleep(lastAgentActiveInterval: 200, mouseDistanceToPanelCenter: 50),
|
||||
"mouse near panel must not sleep")
|
||||
|
||||
// Long idle AND mouse far → sleep
|
||||
precondition(DesktopMochiLogic.shouldSleep(lastAgentActiveInterval: 200, mouseDistanceToPanelCenter: 200),
|
||||
"long idle + far mouse must sleep")
|
||||
|
||||
// Exactly at timeout boundary → still awake (strictly greater than)
|
||||
precondition(!DesktopMochiLogic.shouldSleep(lastAgentActiveInterval: 120, mouseDistanceToPanelCenter: 300),
|
||||
"exactly at timeout must not sleep")
|
||||
|
||||
// Just past timeout boundary + far mouse → sleep
|
||||
precondition(DesktopMochiLogic.shouldSleep(lastAgentActiveInterval: 120.1, mouseDistanceToPanelCenter: 300),
|
||||
"just over timeout must sleep")
|
||||
|
||||
// Far mouse at exact distance threshold → sleep
|
||||
precondition(DesktopMochiLogic.shouldSleep(lastAgentActiveInterval: 200, mouseDistanceToPanelCenter: 150),
|
||||
"at distance threshold must sleep")
|
||||
}
|
||||
|
||||
// MARK: - isOverBody
|
||||
|
||||
static func testIsOverBody() {
|
||||
let s: CGFloat = 120
|
||||
let r = s * DesktopMochiLogic.bodyRadiusFraction // 28.8
|
||||
|
||||
// Center → inside
|
||||
precondition(DesktopMochiLogic.isOverBody(localPoint: CGPoint(x: 60, y: 60), panelSize: s),
|
||||
"center must be inside body")
|
||||
|
||||
// Just inside radius
|
||||
precondition(DesktopMochiLogic.isOverBody(localPoint: CGPoint(x: 60 + r - 0.5, y: 60), panelSize: s),
|
||||
"inside radius must hit")
|
||||
|
||||
// Just outside radius
|
||||
precondition(!DesktopMochiLogic.isOverBody(localPoint: CGPoint(x: 60 + r + 0.5, y: 60), panelSize: s),
|
||||
"outside radius must miss")
|
||||
|
||||
// Corner → outside
|
||||
precondition(!DesktopMochiLogic.isOverBody(localPoint: CGPoint(x: 0, y: 0), panelSize: s),
|
||||
"corner must miss")
|
||||
|
||||
// Diagonal at radius — distance = r/√2 from each axis
|
||||
let diag = r / sqrt(2.0) - 0.5
|
||||
precondition(DesktopMochiLogic.isOverBody(localPoint: CGPoint(x: 60 + diag, y: 60 + diag), panelSize: s),
|
||||
"diagonal inside must hit")
|
||||
}
|
||||
|
||||
// MARK: - lookOrigin
|
||||
|
||||
static func testLookOrigin() {
|
||||
// Panel at (200, 300) on a 1440×900 screen starting at x=0
|
||||
let o = DesktopMochiLogic.lookOrigin(panelMinX: 200, panelMinY: 300,
|
||||
screenMinX: 0, screenHeight: 900,
|
||||
panelSize: 120)
|
||||
// cx = 200 + 60 = 260 → x = 260 - 0 = 260
|
||||
precondition(o.x == 260, "lookOrigin x must be panel center relative to screen left")
|
||||
// cy = 300 + 60 = 360 → y = 900 - 360 = 540
|
||||
precondition(o.y == 540, "lookOrigin y must be flipped from bottom-left to top-left")
|
||||
|
||||
// Panel on a secondary screen starting at x=1440
|
||||
let o2 = DesktopMochiLogic.lookOrigin(panelMinX: 1540, panelMinY: 100,
|
||||
screenMinX: 1440, screenHeight: 1080,
|
||||
panelSize: 120)
|
||||
// cx = 1540 + 60 = 1600 → x = 1600 - 1440 = 160
|
||||
precondition(o2.x == 160, "lookOrigin x must be relative to screen minX")
|
||||
// cy = 100 + 60 = 160 → y = 1080 - 160 = 920
|
||||
precondition(o2.y == 920, "lookOrigin y on secondary screen")
|
||||
}
|
||||
|
||||
// MARK: - shouldRetractOnLanding
|
||||
|
||||
static func testShouldRetractOnLanding() {
|
||||
precondition(DesktopMochiLogic.shouldRetractOnLanding(alertActive: true),
|
||||
"must retract when alert is active on landing")
|
||||
precondition(!DesktopMochiLogic.shouldRetractOnLanding(alertActive: false),
|
||||
"must not retract when no alert on landing")
|
||||
}
|
||||
|
||||
// MARK: - clampOrigin
|
||||
|
||||
static func testClampOrigin() {
|
||||
// Typical macOS visible frame (below menu bar)
|
||||
let vf = CGRect(x: 0, y: 23, width: 1440, height: 877) // maxX=1440, maxY=900
|
||||
let s: CGFloat = 120
|
||||
let m: CGFloat = 24
|
||||
|
||||
// Normal position — within bounds
|
||||
let normal = DesktopMochiLogic.clampOrigin(CGPoint(x: 600, y: 400), panelSize: s, visibleFrame: vf, margin: m)
|
||||
precondition(normal == CGPoint(x: 600, y: 400), "in-bounds origin must be unchanged")
|
||||
|
||||
// Too far left
|
||||
let left = DesktopMochiLogic.clampOrigin(CGPoint(x: -50, y: 400), panelSize: s, visibleFrame: vf, margin: m)
|
||||
precondition(left.x == vf.minX + m, "too-left must clamp to minX + margin")
|
||||
|
||||
// Too far right
|
||||
let right = DesktopMochiLogic.clampOrigin(CGPoint(x: 2000, y: 400), panelSize: s, visibleFrame: vf, margin: m)
|
||||
precondition(right.x == vf.maxX - s - m, "too-right must clamp to maxX - panelSize - margin")
|
||||
|
||||
// Too low
|
||||
let low = DesktopMochiLogic.clampOrigin(CGPoint(x: 400, y: -50), panelSize: s, visibleFrame: vf, margin: m)
|
||||
precondition(low.y == vf.minY + m, "too-low must clamp to minY + margin")
|
||||
|
||||
// Too high
|
||||
let high = DesktopMochiLogic.clampOrigin(CGPoint(x: 400, y: 2000), panelSize: s, visibleFrame: vf, margin: m)
|
||||
precondition(high.y == vf.maxY - s - m, "too-high must clamp to maxY - panelSize - margin")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum DiffEngineTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func checkTrue(_ label: String, _ value: Bool) {
|
||||
if value { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
static func checkInt(_ label: String, _ got: Int, _ expected: Int) {
|
||||
if got == expected { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label) got: \(got) expected: \(expected)"); failures += 1 }
|
||||
}
|
||||
|
||||
static func main() {
|
||||
|
||||
// ── DiffEngine.fromEdit — additions ───────────────────────────────────
|
||||
print("DiffEngine.fromEdit — additions")
|
||||
do {
|
||||
let d = DiffEngine.fromEdit(old: "", new: "hello\nworld\n", path: "/a/b.swift")
|
||||
checkInt("added == 2", d.added, 2)
|
||||
checkInt("removed == 0", d.removed, 0)
|
||||
checkTrue("!tooLarge", !d.tooLarge)
|
||||
checkTrue("1 hunk", d.hunks.count == 1)
|
||||
checkTrue("name == b.swift", d.name == "b.swift")
|
||||
}
|
||||
|
||||
// ── DiffEngine.fromEdit — removals ────────────────────────────────────
|
||||
print("DiffEngine.fromEdit — removals")
|
||||
do {
|
||||
let d = DiffEngine.fromEdit(old: "hello\nworld\n", new: "", path: "/x.py")
|
||||
checkInt("added == 0", d.added, 0)
|
||||
checkInt("removed == 2", d.removed, 2)
|
||||
checkTrue("1 hunk", d.hunks.count == 1)
|
||||
}
|
||||
|
||||
// ── DiffEngine.fromEdit — replacement ─────────────────────────────────
|
||||
print("DiffEngine.fromEdit — replacement")
|
||||
do {
|
||||
let d = DiffEngine.fromEdit(old: "foo\nbar\nbaz\n",
|
||||
new: "foo\nqux\nbaz\n",
|
||||
path: "/f.ts")
|
||||
checkInt("added == 1", d.added, 1)
|
||||
checkInt("removed == 1", d.removed, 1)
|
||||
checkTrue("has context lines",
|
||||
d.hunks.first?.lines.contains(where: { $0.kind == .context }) == true)
|
||||
}
|
||||
|
||||
// ── DiffEngine.fromNew — new file ──────────────────────────────────────
|
||||
print("DiffEngine.fromNew — new file")
|
||||
do {
|
||||
let d = DiffEngine.fromNew(content: "line1\nline2\nline3\n", path: "/new.rs")
|
||||
checkInt("added == 3", d.added, 3)
|
||||
checkInt("removed == 0", d.removed, 0)
|
||||
checkTrue("all lines .added",
|
||||
d.hunks.flatMap { $0.lines }.allSatisfy { $0.kind == .added })
|
||||
}
|
||||
|
||||
// ── DiffEngine.fromEdit — MultiEdit (two edits) ────────────────────────
|
||||
print("DiffEngine.fromEdit — MultiEdit (two edits)")
|
||||
do {
|
||||
let d1 = DiffEngine.fromEdit(old: "aaa\n", new: "bbb\n", path: "/m.kt")
|
||||
let d2 = DiffEngine.fromEdit(old: "ccc\n", new: "ddd\n", path: "/m.kt")
|
||||
checkInt("d1 added == 1", d1.added, 1)
|
||||
checkInt("d1 removed == 1", d1.removed, 1)
|
||||
checkInt("d2 added == 1", d2.added, 1)
|
||||
checkInt("d2 removed == 1", d2.removed, 1)
|
||||
}
|
||||
|
||||
// ── DiffEngine — tooLarge ──────────────────────────────────────────────
|
||||
print("DiffEngine — tooLarge")
|
||||
do {
|
||||
// Generate > 200 KB combined content
|
||||
let bigOld = String(repeating: "x", count: 150 * 1024)
|
||||
let bigNew = String(repeating: "y", count: 60 * 1024)
|
||||
let d = DiffEngine.fromEdit(old: bigOld, new: bigNew, path: "/big.swift")
|
||||
checkTrue("tooLarge == true", d.tooLarge)
|
||||
checkTrue("hunks.isEmpty", d.hunks.isEmpty)
|
||||
}
|
||||
|
||||
// ── DiffEngine — CRLF ─────────────────────────────────────────────────
|
||||
print("DiffEngine — CRLF")
|
||||
do {
|
||||
let d = DiffEngine.fromEdit(old: "a\r\nb\r\n", new: "a\r\nc\r\n", path: "/win.txt")
|
||||
checkInt("added == 1", d.added, 1)
|
||||
checkInt("removed == 1", d.removed, 1)
|
||||
}
|
||||
|
||||
// ── DiffEngine — no trailing newline ──────────────────────────────────
|
||||
print("DiffEngine — no trailing newline")
|
||||
do {
|
||||
let d = DiffEngine.fromEdit(old: "hello", new: "hello\nworld", path: "/t.txt")
|
||||
checkInt("added == 1", d.added, 1)
|
||||
checkInt("removed == 0", d.removed, 0)
|
||||
}
|
||||
|
||||
// ── DiffEngine — 3-line context ───────────────────────────────────────
|
||||
print("DiffEngine — 3-line context")
|
||||
do {
|
||||
// 10-line file, change line 5
|
||||
let oldContent = (1...10).map { "line\($0)" }.joined(separator: "\n") + "\n"
|
||||
let newContent = (1...10).map { $0 == 5 ? "changed" : "line\($0)" }.joined(separator: "\n") + "\n"
|
||||
let d = DiffEngine.fromEdit(old: oldContent, new: newContent, path: "/ctx.swift")
|
||||
checkInt("added == 1", d.added, 1)
|
||||
checkInt("removed == 1", d.removed, 1)
|
||||
checkTrue("1 hunk", d.hunks.count == 1)
|
||||
let hunk = d.hunks[0]
|
||||
let contextLines = hunk.lines.filter { $0.kind == .context }
|
||||
checkTrue("has context before change", contextLines.count >= 3)
|
||||
}
|
||||
|
||||
// ── DiffEngine — m*n > 1_000_000 → tooLarge ─────────────────────────
|
||||
print("DiffEngine — m*n > 1_000_000 → tooLarge")
|
||||
do {
|
||||
// 1001 old lines × 1001 new lines = > 1M
|
||||
let many = (0..<1001).map { "line\($0)" }.joined(separator: "\n")
|
||||
let d = DiffEngine.fromEdit(old: many, new: many + "\nextra", path: "/big.swift")
|
||||
checkTrue("tooLarge when m*n > 1M", d.tooLarge)
|
||||
checkTrue("hunks empty", d.hunks.isEmpty)
|
||||
}
|
||||
|
||||
// ── DiffEngine.fromNew — too large ────────────────────────────────────
|
||||
print("DiffEngine.fromNew — too large")
|
||||
do {
|
||||
let bigContent = String(repeating: "x\n", count: FileDiff.maxLines + 1)
|
||||
let d = DiffEngine.fromNew(content: bigContent, path: "/new.swift")
|
||||
checkTrue("fromNew tooLarge", d.tooLarge)
|
||||
checkTrue("fromNew isNewFile", d.isNewFile)
|
||||
checkTrue("fromNew hunks empty", d.hunks.isEmpty)
|
||||
checkTrue("fromNew added > 0", d.added > 0)
|
||||
}
|
||||
|
||||
// ── String.makeDiffStep / parseDiffStep ───────────────────────────────
|
||||
print("String.makeDiffStep / parseDiffStep")
|
||||
do {
|
||||
let s = String.makeDiffStep(filename: "foo.swift", added: 3, removed: 1, diffId: 7)
|
||||
checkTrue("isDiffStep", s.isDiffStep)
|
||||
let parsed = s.parseDiffStep()
|
||||
checkTrue("parsed != nil", parsed != nil)
|
||||
checkTrue("filename round-trips", parsed?.filename == "foo.swift")
|
||||
checkTrue("added round-trips", parsed?.added == 3)
|
||||
checkTrue("removed round-trips", parsed?.removed == 1)
|
||||
checkTrue("diffId round-trips", parsed?.diffId == 7)
|
||||
|
||||
// id > 9 (multi-digit) round-trips correctly
|
||||
let s2 = String.makeDiffStep(filename: "bar.ts", added: 0, removed: 2, diffId: 42)
|
||||
checkTrue("diffId 42 round-trips", s2.parseDiffStep()?.diffId == 42)
|
||||
|
||||
// Non-diff step should not parse
|
||||
checkTrue("normal step !isDiffStep", !"Edit foo.swift".isDiffStep)
|
||||
checkTrue("normal step parseDiffStep == nil", "Edit foo.swift".parseDiffStep() == nil)
|
||||
}
|
||||
|
||||
// ── DiffEngine.toOneLine ──────────────────────────────────────────────
|
||||
print("DiffEngine.toOneLine")
|
||||
do {
|
||||
// multi-line joined with space
|
||||
checkTrue("multi-line joined",
|
||||
DiffEngine.toOneLine("line one\nline two\nline three") == "line one line two line three")
|
||||
|
||||
// bold stripped
|
||||
checkTrue("bold stripped",
|
||||
DiffEngine.toOneLine("**hello** world") == "hello world")
|
||||
|
||||
// heading stripped
|
||||
checkTrue("heading stripped",
|
||||
DiffEngine.toOneLine("## My Title\nsome text") == "My Title some text")
|
||||
|
||||
// empty input → empty
|
||||
checkTrue("empty → empty", DiffEngine.toOneLine("").isEmpty)
|
||||
|
||||
// truncation
|
||||
let long = DiffEngine.toOneLine(String(repeating: "x ", count: 200), maxChars: 10)
|
||||
checkTrue("truncated to maxChars", long.count <= 10)
|
||||
|
||||
// stop at blank line
|
||||
checkTrue("blank line → first para only",
|
||||
DiffEngine.toOneLine("First para.\n\nSecond para.") == "First para.")
|
||||
|
||||
// stop at --- separator
|
||||
checkTrue("--- separator → first para only",
|
||||
DiffEngine.toOneLine("Done. Single commit 450a657 on github-pulse.\n\n---\n\nFiles touched (7)…")
|
||||
== "Done. Single commit 450a657 on github-pulse.")
|
||||
|
||||
// stop at *** separator
|
||||
checkTrue("*** separator → first para only",
|
||||
DiffEngine.toOneLine("Summary line.\n***\nMore details.") == "Summary line.")
|
||||
|
||||
// stop at table row (|)
|
||||
checkTrue("table row → first para only",
|
||||
DiffEngine.toOneLine("Result:\n| Col1 | Col2 |\n|---|---|\n| A | B |") == "Result:")
|
||||
|
||||
// strip leading bullet -
|
||||
checkTrue("strip bullet -",
|
||||
DiffEngine.toOneLine("- item one\n- item two") == "item one item two")
|
||||
|
||||
// strip leading bullet *
|
||||
checkTrue("strip bullet *",
|
||||
DiffEngine.toOneLine("* first\n* second") == "first second")
|
||||
|
||||
// strip ordered list
|
||||
checkTrue("strip ordered list",
|
||||
DiffEngine.toOneLine("1. step one\n2. step two") == "step one step two")
|
||||
|
||||
// first paragraph empty → fall through to next
|
||||
checkTrue("empty first para → next",
|
||||
DiffEngine.toOneLine("\n\nActual content.") == "Actual content.")
|
||||
}
|
||||
|
||||
// ── finish ─────────────────────────────────────────────────────────────
|
||||
if failures == 0 {
|
||||
print("\nAll tests passed.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("\n\(failures) test(s) failed.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum GitHubActivityTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func check(_ label: String, _ got: Bool) {
|
||||
if got { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
// MARK: - Fixture JSON
|
||||
//
|
||||
// Week 1 (complete, Jan 5–11 2026):
|
||||
// Jan 5 wd=0 NONE(0), Jan 6 wd=1 FIRST_QUARTILE(1), Jan 7 wd=2 SECOND_QUARTILE(2),
|
||||
// Jan 8 wd=3 THIRD_QUARTILE(3), Jan 9 wd=4 FOURTH_QUARTILE(4),
|
||||
// Jan 10 wd=5 FIRST_QUARTILE(1), Jan 11 wd=6 NONE(0)
|
||||
// counts: 0+1+4+8+12+2+0 = 27
|
||||
//
|
||||
// Week 2 (incomplete, Jan 12–13 2026):
|
||||
// Jan 12 wd=0 SECOND_QUARTILE(2), Jan 13 wd=1 THIRD_QUARTILE(3)
|
||||
// counts: 5+10 = 15
|
||||
//
|
||||
// Total: 42
|
||||
static let validJSON: Data = """
|
||||
{
|
||||
"data": {
|
||||
"viewer": {
|
||||
"login": "testuser",
|
||||
"contributionsCollection": {
|
||||
"contributionCalendar": {
|
||||
"totalContributions": 42,
|
||||
"weeks": [
|
||||
{
|
||||
"contributionDays": [
|
||||
{"date": "2026-01-05", "contributionCount": 0, "contributionLevel": "NONE", "weekday": 0},
|
||||
{"date": "2026-01-06", "contributionCount": 1, "contributionLevel": "FIRST_QUARTILE", "weekday": 1},
|
||||
{"date": "2026-01-07", "contributionCount": 4, "contributionLevel": "SECOND_QUARTILE", "weekday": 2},
|
||||
{"date": "2026-01-08", "contributionCount": 8, "contributionLevel": "THIRD_QUARTILE", "weekday": 3},
|
||||
{"date": "2026-01-09", "contributionCount": 12, "contributionLevel": "FOURTH_QUARTILE", "weekday": 4},
|
||||
{"date": "2026-01-10", "contributionCount": 2, "contributionLevel": "FIRST_QUARTILE", "weekday": 5},
|
||||
{"date": "2026-01-11", "contributionCount": 0, "contributionLevel": "NONE", "weekday": 6}
|
||||
]
|
||||
},
|
||||
{
|
||||
"contributionDays": [
|
||||
{"date": "2026-01-12", "contributionCount": 5, "contributionLevel": "SECOND_QUARTILE", "weekday": 0},
|
||||
{"date": "2026-01-13", "contributionCount": 10, "contributionLevel": "THIRD_QUARTILE", "weekday": 1}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
static let unknownLevelJSON: Data = """
|
||||
{
|
||||
"data": {
|
||||
"viewer": {
|
||||
"login": "testuser",
|
||||
"contributionsCollection": {
|
||||
"contributionCalendar": {
|
||||
"totalContributions": 1,
|
||||
"weeks": [
|
||||
{
|
||||
"contributionDays": [
|
||||
{"date": "2026-03-01", "contributionCount": 1, "contributionLevel": "EXTRA_SPECIAL", "weekday": 0}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
// MARK: - Main
|
||||
|
||||
static func main() {
|
||||
|
||||
// ── GitHubActivity.parse ──────────────────────────────────────────────
|
||||
print("GitHubActivity.parse")
|
||||
do {
|
||||
let act = GitHubActivity.parse(validJSON)
|
||||
check("parse returns non-nil", act != nil)
|
||||
check("total == 42", act?.total == 42)
|
||||
check("2 weeks", act?.weeks.count == 2)
|
||||
check("week 0 has 7 days", act?.weeks[0].count == 7)
|
||||
check("week 1 has 2 days (incomplete)", act?.weeks[1].count == 2)
|
||||
// Level mapping
|
||||
check("NONE → level 0", act?.weeks[0][0].level == 0)
|
||||
check("FIRST_QUARTILE → level 1", act?.weeks[0][1].level == 1)
|
||||
check("SECOND_QUARTILE → level 2", act?.weeks[0][2].level == 2)
|
||||
check("THIRD_QUARTILE → level 3", act?.weeks[0][3].level == 3)
|
||||
check("FOURTH_QUARTILE → level 4", act?.weeks[0][4].level == 4)
|
||||
// Dates and counts
|
||||
check("first day date", act?.weeks[0][0].date == "2026-01-05")
|
||||
check("first day count", act?.weeks[0][0].count == 0)
|
||||
check("first day wd", act?.weeks[0][0].weekday == 0)
|
||||
check("last day date", act?.weeks[1][1].date == "2026-01-13")
|
||||
check("last day count", act?.weeks[1][1].count == 10)
|
||||
}
|
||||
|
||||
// ── GitHubActivity.parse — unknown level ─────────────────────────────
|
||||
print("GitHubActivity.parse — unknown level → 0")
|
||||
do {
|
||||
let act = GitHubActivity.parse(unknownLevelJSON)
|
||||
check("unknown level → 0", act?.weeks[0][0].level == 0)
|
||||
}
|
||||
|
||||
// ── GitHubActivity.parse — bad data → nil ────────────────────────────
|
||||
print("GitHubActivity.parse — bad data")
|
||||
do {
|
||||
check("garbage → nil", GitHubActivity.parse("garbage".data(using: .utf8)!) == nil)
|
||||
check("no data key → nil", GitHubActivity.parse("{}".data(using: .utf8)!) == nil)
|
||||
}
|
||||
|
||||
// ── GitHubActivity.lastWeeks ─────────────────────────────────────────
|
||||
print("GitHubActivity.lastWeeks")
|
||||
do {
|
||||
let act = GitHubActivity.parse(validJSON)!
|
||||
check("lastWeeks(2) returns both", act.lastWeeks(2).count == 2)
|
||||
check("lastWeeks(1) returns last week", act.lastWeeks(1).count == 1)
|
||||
check("lastWeeks(1)[0] is incomplete", act.lastWeeks(1)[0].count == 2)
|
||||
check("lastWeeks(1)[0][0].date", act.lastWeeks(1)[0][0].date == "2026-01-12")
|
||||
check("lastWeeks(99) clamps to available", act.lastWeeks(99).count == 2)
|
||||
check("lastWeeks(0) returns empty", act.lastWeeks(0).isEmpty)
|
||||
}
|
||||
|
||||
// ── GitHubActivity.lastDays ──────────────────────────────────────────
|
||||
print("GitHubActivity.lastDays")
|
||||
do {
|
||||
let act = GitHubActivity.parse(validJSON)!
|
||||
// All 9 days flat, last 3 = Jan 11, Jan 12, Jan 13
|
||||
check("lastDays(3) count", act.lastDays(3).count == 3)
|
||||
check("lastDays(3)[0].date", act.lastDays(3)[0].date == "2026-01-11")
|
||||
check("lastDays(3)[2].date", act.lastDays(3)[2].date == "2026-01-13")
|
||||
check("lastDays(9) all days", act.lastDays(9).count == 9)
|
||||
check("lastDays(99) clamps to all", act.lastDays(99).count == 9)
|
||||
check("lastDays(0) empty", act.lastDays(0).isEmpty)
|
||||
}
|
||||
|
||||
// ── finish ────────────────────────────────────────────────────────────
|
||||
if failures == 0 {
|
||||
print("\nAll tests passed.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("\n\(failures) test(s) failed.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum GitHubPulseTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func check(_ label: String, _ got: Bool) {
|
||||
if got { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
// MARK: - Fixture JSON
|
||||
|
||||
// Valid response: PR #42 SUCCESS/APPROVED, PR #43 null CI (unknown)/null review,
|
||||
// repo "testuser/myrepo" PENDING, archived repo filtered out,
|
||||
// one to-review PR from another repo.
|
||||
static let validJSON: Data = """
|
||||
{
|
||||
"data": {
|
||||
"viewer": {
|
||||
"login": "testuser",
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"number": 42,
|
||||
"title": "Add feature",
|
||||
"url": "https://github.com/testuser/myrepo/pull/42",
|
||||
"isDraft": false,
|
||||
"reviewDecision": "APPROVED",
|
||||
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
|
||||
"commits": {"nodes": [{"commit": {"statusCheckRollup": {"state": "SUCCESS"}}}]}
|
||||
},
|
||||
{
|
||||
"number": 43,
|
||||
"title": "Fix bug",
|
||||
"url": "https://github.com/testuser/myrepo/pull/43",
|
||||
"isDraft": true,
|
||||
"reviewDecision": null,
|
||||
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
|
||||
"commits": {"nodes": [{"commit": {"statusCheckRollup": null}}]}
|
||||
}
|
||||
]
|
||||
},
|
||||
"repositories": {
|
||||
"nodes": [
|
||||
{
|
||||
"nameWithOwner": "testuser/myrepo",
|
||||
"url": "https://github.com/testuser/myrepo",
|
||||
"isArchived": false,
|
||||
"defaultBranchRef": {
|
||||
"name": "main",
|
||||
"target": {"statusCheckRollup": {"state": "PENDING"}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"nameWithOwner": "testuser/archived",
|
||||
"url": "https://github.com/testuser/archived",
|
||||
"isArchived": true,
|
||||
"defaultBranchRef": {
|
||||
"name": "main",
|
||||
"target": {"statusCheckRollup": {"state": "SUCCESS"}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"reviewRequested": {
|
||||
"issueCount": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"number": 7,
|
||||
"title": "Review this",
|
||||
"url": "https://github.com/other/repo/pull/7",
|
||||
"isDraft": false,
|
||||
"author": {"login": "otheruser"},
|
||||
"repository": {"nameWithOwner": "other/repo", "url": "https://github.com/other/repo"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
static let emptyJSON: Data = """
|
||||
{
|
||||
"data": {
|
||||
"viewer": {
|
||||
"login": "testuser",
|
||||
"pullRequests": {"nodes": []},
|
||||
"repositories": {"nodes": []}
|
||||
},
|
||||
"reviewRequested": {"issueCount": 0, "nodes": []}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
// MARK: - Main
|
||||
|
||||
static func main() {
|
||||
|
||||
// ── GitHubPulse.parse ─────────────────────────────────────────────────
|
||||
print("GitHubPulse.parse")
|
||||
do {
|
||||
let pulse = GitHubPulse.parse(validJSON)
|
||||
check("parse returns non-nil", pulse != nil)
|
||||
check("login", pulse?.login == "testuser")
|
||||
check("2 myPRs", pulse?.myPRs.count == 2)
|
||||
check("PR#42 id", pulse?.myPRs[0].id == "testuser/myrepo#42")
|
||||
check("PR#42 ci success", pulse?.myPRs[0].ci == .success)
|
||||
check("PR#42 review approved", pulse?.myPRs[0].review == .approved)
|
||||
check("PR#43 ci unknown (null rollup)",pulse?.myPRs[1].ci == .unknown)
|
||||
check("PR#43 review unknown (null)", pulse?.myPRs[1].review == .unknown)
|
||||
check("PR#43 isDraft", pulse?.myPRs[1].isDraft == true)
|
||||
check("1 mainCI (archived filtered)", pulse?.mainCI.count == 1)
|
||||
check("mainCI repo", pulse?.mainCI[0].repo == "testuser/myrepo")
|
||||
check("mainCI ci pending", pulse?.mainCI[0].ci == .pending)
|
||||
check("mainCI branch main", pulse?.mainCI[0].branch == "main")
|
||||
check("1 toReview", pulse?.toReview.count == 1)
|
||||
check("toReview id", pulse?.toReview[0].id == "other/repo#7")
|
||||
check("hasPending (mainCI pending)", pulse?.hasPending == true)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.parse — empty ─────────────────────────────────────────
|
||||
print("GitHubPulse.parse — empty")
|
||||
do {
|
||||
let pulse = GitHubPulse.parse(emptyJSON)
|
||||
check("empty parses ok", pulse != nil)
|
||||
check("no myPRs", pulse?.myPRs.isEmpty == true)
|
||||
check("no mainCI", pulse?.mainCI.isEmpty == true)
|
||||
check("no toReview", pulse?.toReview.isEmpty == true)
|
||||
check("!hasPending", pulse?.hasPending == false)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.parse — bad data ──────────────────────────────────────
|
||||
print("GitHubPulse.parse — bad data")
|
||||
do {
|
||||
check("garbage → nil", GitHubPulse.parse("garbage".data(using: .utf8)!) == nil)
|
||||
check("empty obj → nil", GitHubPulse.parse("{}".data(using: .utf8)!) == nil)
|
||||
}
|
||||
|
||||
// ── CIState init ──────────────────────────────────────────────────────
|
||||
print("CIState init")
|
||||
do {
|
||||
check("nil → unknown", CIState(rawGitHub: nil) == .unknown)
|
||||
check("PENDING → pending", CIState(rawGitHub: "PENDING") == .pending)
|
||||
check("EXPECTED → pending", CIState(rawGitHub: "EXPECTED") == .pending)
|
||||
check("SUCCESS → success", CIState(rawGitHub: "SUCCESS") == .success)
|
||||
check("FAILURE → failure", CIState(rawGitHub: "FAILURE") == .failure)
|
||||
check("ERROR → failure", CIState(rawGitHub: "ERROR") == .failure)
|
||||
check("other → unknown", CIState(rawGitHub: "WAITING") == .unknown)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — first poll silent ────────────────────────────
|
||||
print("GitHubPulse.events — first poll silent")
|
||||
do {
|
||||
let pulse = GitHubPulse.parse(validJSON)!
|
||||
let events = GitHubPulse.events(old: nil, new: pulse)
|
||||
check("first poll → no events", events.isEmpty)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — pending → success ────────────────────────────
|
||||
print("GitHubPulse.events — pending → success")
|
||||
do {
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
oldPulse.myPRs = [GitHubPR(id: "r/p#1", title: "T", url: "", repo: "r/p",
|
||||
number: 1, isDraft: false, ci: .pending, review: .unknown)]
|
||||
var newPulse = oldPulse
|
||||
newPulse.myPRs[0].ci = .success
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("pending→success → ciPassed", events == [.ciPassed(prId: "r/p#1")])
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — success → failure ────────────────────────────
|
||||
print("GitHubPulse.events — success → failure")
|
||||
do {
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
oldPulse.myPRs = [GitHubPR(id: "r/p#2", title: "T", url: "", repo: "r/p",
|
||||
number: 2, isDraft: false, ci: .success, review: .unknown)]
|
||||
var newPulse = oldPulse
|
||||
newPulse.myPRs[0].ci = .failure
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("success→failure → ciFailed", events == [.ciFailed(prId: "r/p#2")])
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — main CI failure ──────────────────────────────
|
||||
print("GitHubPulse.events — main CI failure")
|
||||
do {
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
oldPulse.mainCI = [GitHubRepoCI(repo: "a/b", url: "", branch: "main", ci: .success)]
|
||||
var newPulse = oldPulse
|
||||
newPulse.mainCI[0].ci = .failure
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("main success→failure → mainFailed", events == [.mainFailed(repo: "a/b")])
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — new review request ───────────────────────────
|
||||
print("GitHubPulse.events — new review request")
|
||||
do {
|
||||
let pr = GitHubPR(id: "o/r#7", title: "R", url: "", repo: "o/r",
|
||||
number: 7, isDraft: false, ci: .unknown, review: .pending)
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
var newPulse = oldPulse
|
||||
newPulse.toReview = [pr]
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("new review → reviewRequested", events == [.reviewRequested(prId: "o/r#7")])
|
||||
|
||||
// Already-known review: no new event
|
||||
oldPulse.toReview = [pr]
|
||||
newPulse.toReview = [pr]
|
||||
let noEvents = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("known review → no event", noEvents.isEmpty)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.parse — duplicate ids ────────────────────────────────────
|
||||
print("GitHubPulse.parse — duplicate ids")
|
||||
do {
|
||||
// JSON with PR #42 duplicated: should keep only first occurrence
|
||||
let dupJSON: Data = """
|
||||
{
|
||||
"data": {
|
||||
"viewer": {
|
||||
"login": "testuser",
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"number": 42, "title": "Add feature",
|
||||
"url": "https://github.com/testuser/myrepo/pull/42",
|
||||
"isDraft": false, "reviewDecision": "APPROVED",
|
||||
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
|
||||
"commits": {"nodes": [{"commit": {"statusCheckRollup": {"state": "SUCCESS"}}}]}
|
||||
},
|
||||
{
|
||||
"number": 42, "title": "Duplicate entry",
|
||||
"url": "https://github.com/testuser/myrepo/pull/42",
|
||||
"isDraft": true, "reviewDecision": null,
|
||||
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
|
||||
"commits": {"nodes": [{"commit": {"statusCheckRollup": {"state": "FAILURE"}}}]}
|
||||
}
|
||||
]
|
||||
},
|
||||
"repositories": {"nodes": []}
|
||||
},
|
||||
"reviewRequested": {"issueCount": 0, "nodes": []}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let pulse = GitHubPulse.parse(dupJSON)
|
||||
check("dup PR → 1 myPR", pulse?.myPRs.count == 1)
|
||||
check("dup PR → keeps first title", pulse?.myPRs[0].title == "Add feature")
|
||||
check("dup PR → keeps first ci", pulse?.myPRs[0].ci == .success)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — duplicate ids in old pulse ───────────────────
|
||||
print("GitHubPulse.events — duplicate-safe Dictionary")
|
||||
do {
|
||||
// OLD has duplicate ids (uniquingKeysWith keeps first); NEW has one entry → fires once
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
let pr1 = GitHubPR(id: "r/p#1", title: "T", url: "", repo: "r/p",
|
||||
number: 1, isDraft: false, ci: .pending, review: .unknown)
|
||||
oldPulse.myPRs = [pr1, pr1] // intentional duplicate in old
|
||||
var newPulse = GitHubPulse.parse(emptyJSON)!
|
||||
var pr1Passed = pr1; pr1Passed.ci = .success
|
||||
newPulse.myPRs = [pr1Passed] // single entry in new
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("dup old ids → no crash", true)
|
||||
check("dup old ids → ciPassed fires once",
|
||||
events.filter { if case .ciPassed = $0 { return true }; return false }.count == 1)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — headSha: new PR already green ───────────────
|
||||
print("GitHubPulse.events — headSha: new PR already green")
|
||||
do {
|
||||
let old = GitHubPulse.parse(emptyJSON)!
|
||||
var newPulse = old
|
||||
newPulse.myPRs = [GitHubPR(id: "r/p#10", title: "T", url: "", repo: "r/p",
|
||||
number: 10, isDraft: false, ci: .success, review: .unknown,
|
||||
headSha: "abc111")]
|
||||
let events = GitHubPulse.events(old: old, new: newPulse)
|
||||
check("new PR green → ciPassed", events == [.ciPassed(prId: "r/p#10")])
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — headSha: new PR pending → nothing ───────────
|
||||
print("GitHubPulse.events — headSha: new PR pending → nothing")
|
||||
do {
|
||||
let old = GitHubPulse.parse(emptyJSON)!
|
||||
var newPulse = old
|
||||
newPulse.myPRs = [GitHubPR(id: "r/p#11", title: "T", url: "", repo: "r/p",
|
||||
number: 11, isDraft: false, ci: .pending, review: .unknown,
|
||||
headSha: "abc222")]
|
||||
let events = GitHubPulse.events(old: old, new: newPulse)
|
||||
check("new PR pending → no events", events.isEmpty)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — headSha: new commit already red ─────────────
|
||||
print("GitHubPulse.events — headSha: new commit already red → ciFailed")
|
||||
do {
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
oldPulse.myPRs = [GitHubPR(id: "r/p#12", title: "T", url: "", repo: "r/p",
|
||||
number: 12, isDraft: false, ci: .success, review: .unknown,
|
||||
headSha: "sha-old")]
|
||||
var newPulse = oldPulse
|
||||
newPulse.myPRs[0].ci = .failure
|
||||
newPulse.myPRs[0].headSha = "sha-new"
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("new commit red → ciFailed", events == [.ciFailed(prId: "r/p#12")])
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — headSha: same sha success→success → nothing ─
|
||||
print("GitHubPulse.events — headSha: same sha success→success → nothing")
|
||||
do {
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
oldPulse.myPRs = [GitHubPR(id: "r/p#13", title: "T", url: "", repo: "r/p",
|
||||
number: 13, isDraft: false, ci: .success, review: .unknown,
|
||||
headSha: "same")]
|
||||
let newPulse = oldPulse // identical SHA and CI
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("same sha success→success → no event", events.isEmpty)
|
||||
}
|
||||
|
||||
// ── GitHubPulse.events — headSha: new commit on main red → mainFailed ─
|
||||
print("GitHubPulse.events — headSha: new commit on main red → mainFailed")
|
||||
do {
|
||||
var oldPulse = GitHubPulse.parse(emptyJSON)!
|
||||
oldPulse.mainCI = [GitHubRepoCI(repo: "a/b", url: "", branch: "main",
|
||||
ci: .success, headSha: "sha-old")]
|
||||
var newPulse = oldPulse
|
||||
newPulse.mainCI[0].ci = .failure
|
||||
newPulse.mainCI[0].headSha = "sha-new"
|
||||
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
|
||||
check("new commit on main red → mainFailed", events == [.mainFailed(repo: "a/b")])
|
||||
}
|
||||
|
||||
// ── GitHubPulse.isStale ───────────────────────────────────────────────
|
||||
print("GitHubPulse.isStale")
|
||||
do {
|
||||
let now = Date()
|
||||
check("nil fetchedAt → stale",
|
||||
GitHubPulse.isStale(fetchedAt: nil, now: now, maxAge: 60))
|
||||
check("fresh (same instant) → not stale",
|
||||
!GitHubPulse.isStale(fetchedAt: now, now: now, maxAge: 60))
|
||||
check("61 s ago → stale",
|
||||
GitHubPulse.isStale(fetchedAt: now.addingTimeInterval(-61), now: now, maxAge: 60))
|
||||
}
|
||||
|
||||
// ── finish ─────────────────────────────────────────────────────────────
|
||||
if failures == 0 {
|
||||
print("\nAll tests passed.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("\n\(failures) test(s) failed.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum IslandScreenGeometryTests {
|
||||
static func main() {
|
||||
// Regression: absent auxiliary areas must never mean "screen-wide notch".
|
||||
for screenWidth: CGFloat in [1080, 1920, 2560, 3840] {
|
||||
let geometry = IslandScreenGeometry(
|
||||
screenWidth: screenWidth, safeAreaTop: 0,
|
||||
auxiliaryLeftWidth: nil, auxiliaryRightWidth: nil, menuBarHeight: 30
|
||||
)
|
||||
precondition(!geometry.hasNotch)
|
||||
precondition(geometry.width == 80)
|
||||
precondition(geometry.height == 24)
|
||||
}
|
||||
|
||||
// A shorter menu bar must also contain the resting island.
|
||||
let shortMenuBar = IslandScreenGeometry(
|
||||
screenWidth: 1920, safeAreaTop: 0,
|
||||
auxiliaryLeftWidth: nil, auxiliaryRightWidth: nil, menuBarHeight: 22
|
||||
)
|
||||
precondition(shortMenuBar.height == 22)
|
||||
|
||||
// Real MacBook notch measurements retain their physical dimensions.
|
||||
let macBook = IslandScreenGeometry(
|
||||
screenWidth: 1512, safeAreaTop: 32,
|
||||
auxiliaryLeftWidth: 660, auxiliaryRightWidth: 660, menuBarHeight: 32
|
||||
)
|
||||
precondition(macBook.hasNotch)
|
||||
precondition(macBook.width == 192 && macBook.height == 32)
|
||||
|
||||
// Incomplete or invalid measurements use the notch fallback, not the screen.
|
||||
for auxiliaryWidth: CGFloat? in [nil, 0, 1000] {
|
||||
let geometry = IslandScreenGeometry(
|
||||
screenWidth: 1512, safeAreaTop: 32,
|
||||
auxiliaryLeftWidth: auxiliaryWidth, auxiliaryRightWidth: auxiliaryWidth,
|
||||
menuBarHeight: 32
|
||||
)
|
||||
precondition(geometry.width == 184 && geometry.height == 32)
|
||||
}
|
||||
// Compact/greeting destinations share the measured resting height.
|
||||
for height: CGFloat in [22, 24, 32, 38] {
|
||||
let compact = IslandRestingLayout(width: 240, height: height)
|
||||
precondition(compact.botCenterY == height / 2)
|
||||
precondition(compact.botDiameter == min(20, height - 6))
|
||||
precondition(compact.botCenterY - compact.botDiameter / 2 >= 3)
|
||||
precondition(compact.botCenterY + compact.botDiameter / 2 <= height - 3)
|
||||
precondition(compact.miniGridCenterX == 200)
|
||||
precondition(compact.miniGridScale * 28 <= height - 4)
|
||||
}
|
||||
print("Island screen geometry and resting layout: 13 cases passed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum MochiWardrobeTests {
|
||||
|
||||
static var failures = 0
|
||||
|
||||
static func check(_ label: String, _ got: Bool) {
|
||||
if got { print(" ✓ \(label)") }
|
||||
else { print(" ✗ \(label)"); failures += 1 }
|
||||
}
|
||||
|
||||
static func date(year: Int, month: Int, day: Int) -> Date {
|
||||
var c = DateComponents(); c.year = year; c.month = month; c.day = day
|
||||
return Calendar(identifier: .gregorian).date(from: c)!
|
||||
}
|
||||
|
||||
static func seasonal(_ year: Int, _ month: Int, _ day: Int) -> Outfit {
|
||||
Outfit.seasonal(for: date(year: year, month: month, day: day),
|
||||
calendar: Calendar(identifier: .gregorian))
|
||||
}
|
||||
|
||||
static func main() {
|
||||
print("Outfit.seasonal — witch hat (Oct 1 – Nov 1)")
|
||||
check("Sep 30 → none", seasonal(2026, 9, 30) == .none)
|
||||
check("Oct 1 → witchHat", seasonal(2026, 10, 1) == .witchHat)
|
||||
check("Oct 31 → witchHat", seasonal(2026, 10, 31) == .witchHat)
|
||||
check("Nov 1 → witchHat", seasonal(2026, 11, 1) == .witchHat)
|
||||
check("Nov 2 → none", seasonal(2026, 11, 2) == .none)
|
||||
|
||||
print("Outfit.seasonal — santa hat (Dec 1–26)")
|
||||
check("Nov 30 → none", seasonal(2026, 11, 30) == .none)
|
||||
check("Dec 1 → santaHat", seasonal(2026, 12, 1) == .santaHat)
|
||||
check("Dec 26 → santaHat", seasonal(2026, 12, 26) == .santaHat)
|
||||
check("Dec 27 → none", seasonal(2026, 12, 27) == .none)
|
||||
|
||||
print("Outfit.seasonal — party hat (Dec 31 – Jan 2)")
|
||||
check("Dec 30 → none", seasonal(2026, 12, 30) == .none)
|
||||
check("Dec 31 → partyHat", seasonal(2026, 12, 31) == .partyHat)
|
||||
check("Jan 1 → partyHat", seasonal(2027, 1, 1) == .partyHat)
|
||||
check("Jan 2 → partyHat", seasonal(2027, 1, 2) == .partyHat)
|
||||
check("Jan 3 → none", seasonal(2027, 1, 3) == .none)
|
||||
|
||||
print("Outfit.seasonal — Feb 13–16 → none (heartsHeadband removed)")
|
||||
check("Feb 12 → none", seasonal(2026, 2, 12) == .none)
|
||||
check("Feb 13 → none", seasonal(2026, 2, 13) == .none)
|
||||
check("Feb 15 → none", seasonal(2026, 2, 15) == .none)
|
||||
check("Feb 16 → none", seasonal(2026, 2, 16) == .none)
|
||||
|
||||
print("Outfit.seasonal — bunny ears (Easter ±)")
|
||||
// Easter 2026 = April 5
|
||||
check("Apr 3 2026 → bunnyEars", seasonal(2026, 4, 3) == .bunnyEars) // -2
|
||||
check("Apr 4 2026 → bunnyEars", seasonal(2026, 4, 4) == .bunnyEars) // -1
|
||||
check("Apr 5 2026 → bunnyEars", seasonal(2026, 4, 5) == .bunnyEars) // 0
|
||||
check("Apr 6 2026 → bunnyEars", seasonal(2026, 4, 6) == .bunnyEars) // +1
|
||||
check("Apr 7 2026 → none", seasonal(2026, 4, 7) == .none) // +2
|
||||
check("Apr 2 2026 → none", seasonal(2026, 4, 2) == .none) // -3
|
||||
// Easter 2027 = March 28
|
||||
check("Mar 26 2027 → bunnyEars", seasonal(2027, 3, 26) == .bunnyEars)
|
||||
check("Mar 29 2027 → bunnyEars", seasonal(2027, 3, 29) == .bunnyEars)
|
||||
check("Mar 30 2027 → none", seasonal(2027, 3, 30) == .none)
|
||||
// Easter 2028 = April 16
|
||||
check("Apr 14 2028 → bunnyEars", seasonal(2028, 4, 14) == .bunnyEars)
|
||||
check("Apr 17 2028 → bunnyEars", seasonal(2028, 4, 17) == .bunnyEars)
|
||||
check("Apr 18 2028 → none", seasonal(2028, 4, 18) == .none)
|
||||
|
||||
print("Outfit.seasonal — sunglasses (Jun 21 – Aug 31)")
|
||||
check("Jun 20 → none", seasonal(2026, 6, 20) == .none)
|
||||
check("Jun 21 → sunglasses", seasonal(2026, 6, 21) == .sunglasses)
|
||||
check("Aug 31 → sunglasses", seasonal(2026, 8, 31) == .sunglasses)
|
||||
check("Sep 1 → none", seasonal(2026, 9, 1) == .none)
|
||||
|
||||
print("Outfit.resolved")
|
||||
let d = date(year: 2026, month: 10, day: 15)
|
||||
let cal = Calendar(identifier: .gregorian)
|
||||
check("auto → seasonal", Outfit.resolved(selection: .auto, date: d, calendar: cal) == .witchHat)
|
||||
check("none → none", Outfit.resolved(selection: .none, date: d, calendar: cal) == .none)
|
||||
check("beanie → beanie", Outfit.resolved(selection: .beanie, date: d, calendar: cal) == .beanie)
|
||||
|
||||
print("Outfit.rawValue stability")
|
||||
check("auto rawValue", Outfit.auto.rawValue == "auto")
|
||||
check("none rawValue", Outfit.none.rawValue == "none")
|
||||
check("partyHat rawValue", Outfit.partyHat.rawValue == "partyHat")
|
||||
check("beanie rawValue", Outfit.beanie.rawValue == "beanie")
|
||||
check("crown rawValue", Outfit.crown.rawValue == "crown")
|
||||
check("sunglasses rawValue", Outfit.sunglasses.rawValue == "sunglasses")
|
||||
check("roundGlasses rawValue", Outfit.roundGlasses.rawValue == "roundGlasses")
|
||||
check("bow rawValue", Outfit.bow.rawValue == "bow")
|
||||
check("scarf rawValue", Outfit.scarf.rawValue == "scarf")
|
||||
check("witchHat rawValue", Outfit.witchHat.rawValue == "witchHat")
|
||||
check("pumpkin rawValue", Outfit.pumpkin.rawValue == "pumpkin")
|
||||
check("santaHat rawValue", Outfit.santaHat.rawValue == "santaHat")
|
||||
check("bunnyEars rawValue", Outfit.bunnyEars.rawValue == "bunnyEars")
|
||||
|
||||
print("Outfit.stored — removed rawValues → auto")
|
||||
for removed in ["topHat", "cap", "heartsHeadband", "strawHat"] {
|
||||
UserDefaults.standard.set(removed, forKey: "mochiOutfit")
|
||||
check("'\(removed)' stored → .auto", Outfit.stored == .auto)
|
||||
}
|
||||
|
||||
print("Outfit.stored unknown → auto")
|
||||
UserDefaults.standard.set("totallyUnknown", forKey: "mochiOutfit")
|
||||
check("unknown → .auto", Outfit.stored == .auto)
|
||||
UserDefaults.standard.removeObject(forKey: "mochiOutfit")
|
||||
check("missing → .auto", Outfit.stored == .auto)
|
||||
|
||||
if failures == 0 { print("\nAll tests passed.") }
|
||||
else { print("\n\(failures) test(s) FAILED."); exit(1) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum SafeWebURLTests {
|
||||
static func main() {
|
||||
// Accepted: plain http/https URLs
|
||||
precondition(safeWebURL("https://example.com") != nil)
|
||||
precondition(safeWebURL("http://example.com/a?b=1") != nil)
|
||||
precondition(safeWebURL("HTTPS://EXAMPLE.COM") != nil)
|
||||
precondition(safeWebURL(" https://example.com \n") != nil)
|
||||
precondition(safeWebURL("http://localhost:5678") != nil)
|
||||
|
||||
// Rejected: nil, empty, non-web schemes, malformed
|
||||
precondition(safeWebURL(nil) == nil)
|
||||
precondition(safeWebURL("") == nil)
|
||||
precondition(safeWebURL("file:///etc/hosts") == nil)
|
||||
precondition(safeWebURL("javascript:alert(1)") == nil)
|
||||
precondition(safeWebURL("vscode://file/etc/hosts") == nil)
|
||||
precondition(safeWebURL("smb://server/share") == nil)
|
||||
precondition(safeWebURL("mailto:a@b.c") == nil)
|
||||
precondition(safeWebURL("https://") == nil)
|
||||
precondition(safeWebURL("https:example.com") == nil)
|
||||
precondition(safeWebURL("//example.com") == nil)
|
||||
|
||||
print("Safe web links: 15 cases passed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
enum ShortcutTests {
|
||||
static func main() {
|
||||
testDefaultsExhaustive()
|
||||
testAllDefaultsHaveModifier()
|
||||
testNoDefaultDuplicates()
|
||||
testCarbonModifiers()
|
||||
testDisplayString()
|
||||
testDuplicateDetection()
|
||||
testLoadSaveRoundTrip()
|
||||
testLegacyKeysForToggleIsland()
|
||||
testEnabledByDefault()
|
||||
testKeyCodeToString()
|
||||
testCardNavigation()
|
||||
print("ShortcutLogic: all cases passed")
|
||||
}
|
||||
|
||||
// MARK: - testDefaultsExhaustive
|
||||
|
||||
static func testDefaultsExhaustive() {
|
||||
for action in ShortcutAction.allCases {
|
||||
precondition(ShortcutLogic.defaults[action] != nil,
|
||||
"missing default for \(action.rawValue)")
|
||||
}
|
||||
precondition(ShortcutLogic.defaults.count == ShortcutAction.allCases.count,
|
||||
"defaults table must cover every ShortcutAction")
|
||||
}
|
||||
|
||||
// MARK: - testAllDefaultsHaveModifier
|
||||
|
||||
static func testAllDefaultsHaveModifier() {
|
||||
for (action, spec) in ShortcutLogic.defaults {
|
||||
precondition(spec.hasModifier,
|
||||
"default for \(action.rawValue) has no modifier — bare keys not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - testNoDefaultDuplicates
|
||||
|
||||
static func testNoDefaultDuplicates() {
|
||||
let dups = ShortcutLogic.duplicates(in: ShortcutLogic.defaults)
|
||||
precondition(dups.isEmpty,
|
||||
"duplicate default shortcuts: \(dups.map { $0.rawValue })")
|
||||
}
|
||||
|
||||
// MARK: - testCarbonModifiers
|
||||
|
||||
static func testCarbonModifiers() {
|
||||
// cmdShift = ⌘⇧
|
||||
let cmdShiftCarbon = ShortcutLogic.carbonModifiers(fromNS: ShortcutSpec.cmdShift)
|
||||
// Carbon: cmdKey = 256, shiftKey = 512
|
||||
precondition(cmdShiftCarbon & 256 != 0, "⌘ must set cmdKey bit in Carbon")
|
||||
precondition(cmdShiftCarbon & 512 != 0, "⇧ must set shiftKey bit in Carbon")
|
||||
precondition(cmdShiftCarbon & 2048 == 0, "⌘⇧ must not set optionKey")
|
||||
precondition(cmdShiftCarbon & 4096 == 0, "⌘⇧ must not set controlKey")
|
||||
|
||||
// ctrlOpt = ⌃⌥
|
||||
let ctrlOptCarbon = ShortcutLogic.carbonModifiers(fromNS: ShortcutSpec.ctrlOpt)
|
||||
precondition(ctrlOptCarbon & 256 == 0, "⌃⌥ must not set cmdKey")
|
||||
precondition(ctrlOptCarbon & 512 == 0, "⌃⌥ must not set shiftKey")
|
||||
precondition(ctrlOptCarbon & 2048 != 0, "⌥ must set optionKey bit in Carbon")
|
||||
precondition(ctrlOptCarbon & 4096 != 0, "⌃ must set controlKey bit in Carbon")
|
||||
|
||||
// Zero flags → zero Carbon flags
|
||||
precondition(ShortcutLogic.carbonModifiers(fromNS: 0) == 0,
|
||||
"no modifiers must produce zero Carbon flags")
|
||||
|
||||
// All four modifiers
|
||||
let allNS = ShortcutSpec.cmdBit | ShortcutSpec.shiftBit |
|
||||
ShortcutSpec.optBit | ShortcutSpec.ctrlBit
|
||||
let allCarbon = ShortcutLogic.carbonModifiers(fromNS: allNS)
|
||||
precondition(allCarbon & (256 | 512 | 2048 | 4096) == (256 | 512 | 2048 | 4096),
|
||||
"all four NS modifiers must map to all four Carbon bits")
|
||||
}
|
||||
|
||||
// MARK: - testDisplayString
|
||||
|
||||
static func testDisplayString() {
|
||||
// ⌘⇧N
|
||||
let cmdShiftN = ShortcutSpec(keyCode: 45, nsFlags: ShortcutSpec.cmdShift)
|
||||
let s1 = ShortcutLogic.displayString(for: cmdShiftN)
|
||||
precondition(s1.contains("⌘"), "⌘⇧N display must contain ⌘")
|
||||
precondition(s1.contains("⇧"), "⌘⇧N display must contain ⇧")
|
||||
precondition(s1.contains("N"), "⌘⇧N display must contain N")
|
||||
|
||||
// ⌃⌥A
|
||||
let ctrlOptA = ShortcutSpec(keyCode: 0, nsFlags: ShortcutSpec.ctrlOpt)
|
||||
let s2 = ShortcutLogic.displayString(for: ctrlOptA)
|
||||
precondition(s2.contains("⌃"), "⌃⌥A display must contain ⌃")
|
||||
precondition(s2.contains("⌥"), "⌃⌥A display must contain ⌥")
|
||||
precondition(s2.contains("A"), "⌃⌥A display must contain A")
|
||||
|
||||
// No duplicate modifiers in the string
|
||||
precondition(s1.filter { $0 == "⌘" }.count == 1, "⌘ must appear exactly once")
|
||||
|
||||
// ⌃⌥Space
|
||||
let ctrlOptSpace = ShortcutSpec(keyCode: 49, nsFlags: ShortcutSpec.ctrlOpt)
|
||||
let s3 = ShortcutLogic.displayString(for: ctrlOptSpace)
|
||||
precondition(s3.contains("Space"), "⌃⌥Space display must contain 'Space'")
|
||||
}
|
||||
|
||||
// MARK: - testDuplicateDetection
|
||||
|
||||
static func testDuplicateDetection() {
|
||||
// No duplicates in an empty dict
|
||||
precondition(ShortcutLogic.duplicates(in: [:]).isEmpty,
|
||||
"empty map must have no duplicates")
|
||||
|
||||
// Single entry — no duplicate
|
||||
let specA = ShortcutSpec(keyCode: 0, nsFlags: ShortcutSpec.ctrlOpt)
|
||||
precondition(ShortcutLogic.duplicates(in: [.goToAlert: specA]).isEmpty,
|
||||
"single entry must have no duplicate")
|
||||
|
||||
// Two different specs — no duplicate
|
||||
let specB = ShortcutSpec(keyCode: 17, nsFlags: ShortcutSpec.ctrlOpt)
|
||||
let twoUnique: [ShortcutAction: ShortcutSpec] = [.goToAlert: specA, .jumpToTerminal: specB]
|
||||
precondition(ShortcutLogic.duplicates(in: twoUnique).isEmpty,
|
||||
"two distinct specs must have no duplicate")
|
||||
|
||||
// Same spec for two actions → both flagged
|
||||
let twoSame: [ShortcutAction: ShortcutSpec] = [.goToAlert: specA, .jumpToTerminal: specA]
|
||||
let dups = ShortcutLogic.duplicates(in: twoSame)
|
||||
precondition(dups.contains(.goToAlert), "first duped action must be flagged")
|
||||
precondition(dups.contains(.jumpToTerminal), "second duped action must be flagged")
|
||||
precondition(dups.count == 2, "exactly 2 actions must be flagged for one duplicate pair")
|
||||
|
||||
// Same key code but different flags → not a duplicate
|
||||
let specC = ShortcutSpec(keyCode: 0, nsFlags: ShortcutSpec.cmdShift)
|
||||
let diffFlags: [ShortcutAction: ShortcutSpec] = [.goToAlert: specA, .jumpToTerminal: specC]
|
||||
precondition(ShortcutLogic.duplicates(in: diffFlags).isEmpty,
|
||||
"same key code with different modifiers must not be a duplicate")
|
||||
}
|
||||
|
||||
// MARK: - testLoadSaveRoundTrip
|
||||
|
||||
static func testLoadSaveRoundTrip() {
|
||||
// Use a test-only suite to avoid polluting the app UserDefaults
|
||||
let suiteName = "com.coucou.ShortcutTests.\(Int.random(in: 10000...99999))"
|
||||
guard let ud = UserDefaults(suiteName: suiteName) else {
|
||||
preconditionFailure("could not create test UserDefaults suite")
|
||||
}
|
||||
|
||||
// Save a custom spec for .openChat
|
||||
let custom = ShortcutSpec(keyCode: 5, nsFlags: ShortcutSpec.cmdShift)
|
||||
ud.set(Int(custom.keyCode), forKey: ShortcutAction.openChat.udKeyCode)
|
||||
ud.set(Int(custom.nsFlags), forKey: ShortcutAction.openChat.udFlags)
|
||||
|
||||
// Load it back using the raw UserDefaults API (same path ShortcutLogic.hotKey uses)
|
||||
let loadedKC = ud.object(forKey: ShortcutAction.openChat.udKeyCode) as? Int
|
||||
let loadedFl = ud.object(forKey: ShortcutAction.openChat.udFlags) as? Int
|
||||
precondition(loadedKC == Int(custom.keyCode), "saved key code must round-trip")
|
||||
precondition(loadedFl == Int(custom.nsFlags), "saved flags must round-trip")
|
||||
|
||||
// Cleanup
|
||||
ud.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
|
||||
// MARK: - testLegacyKeysForToggleIsland
|
||||
|
||||
static func testLegacyKeysForToggleIsland() {
|
||||
precondition(ShortcutAction.toggleIsland.udKeyCode == "hotkeyCode",
|
||||
"toggleIsland must use legacy 'hotkeyCode' key")
|
||||
precondition(ShortcutAction.toggleIsland.udFlags == "hotkeyFlags",
|
||||
"toggleIsland must use legacy 'hotkeyFlags' key")
|
||||
precondition(ShortcutAction.toggleIsland.udEnabled == "hotkeyEnabled",
|
||||
"toggleIsland must use legacy 'hotkeyEnabled' key")
|
||||
|
||||
// All other actions must NOT use the legacy keys
|
||||
for action in ShortcutAction.allCases where action != .toggleIsland {
|
||||
precondition(action.udKeyCode != "hotkeyCode",
|
||||
"\(action.rawValue) must not reuse legacy hotkeyCode key")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - testEnabledByDefault
|
||||
|
||||
static func testEnabledByDefault() {
|
||||
// toggleIsland is OFF by default (existing behaviour)
|
||||
precondition(!ShortcutAction.toggleIsland.enabledByDefault,
|
||||
"toggleIsland must be disabled by default")
|
||||
|
||||
// All other global shortcuts are ON by default
|
||||
for action in ShortcutAction.allCases where action != .toggleIsland {
|
||||
precondition(action.enabledByDefault,
|
||||
"\(action.rawValue) must be enabled by default")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - testKeyCodeToString
|
||||
|
||||
static func testKeyCodeToString() {
|
||||
precondition(ShortcutLogic.keyCodeToString(0) == "A", "keyCode 0 = A")
|
||||
precondition(ShortcutLogic.keyCodeToString(45) == "N", "keyCode 45 = N")
|
||||
precondition(ShortcutLogic.keyCodeToString(49) == "Space", "keyCode 49 = Space")
|
||||
precondition(ShortcutLogic.keyCodeToString(30) == "]", "keyCode 30 = ]")
|
||||
precondition(ShortcutLogic.keyCodeToString(33) == "[", "keyCode 33 = [")
|
||||
precondition(ShortcutLogic.keyCodeToString(2) == "D", "keyCode 2 = D")
|
||||
// Unknown key code returns a placeholder
|
||||
let unknown = ShortcutLogic.keyCodeToString(200)
|
||||
precondition(!unknown.isEmpty, "unknown key code must return non-empty string")
|
||||
}
|
||||
|
||||
// MARK: - testCardNavigation
|
||||
|
||||
static func testCardNavigation() {
|
||||
// nil + down → first item
|
||||
precondition(ShortcutLogic.navigate(selection: nil, delta: +1, itemCount: 3) == 0,
|
||||
"nil+down must select first item")
|
||||
// nil + up → last item
|
||||
precondition(ShortcutLogic.navigate(selection: nil, delta: -1, itemCount: 3) == 2,
|
||||
"nil+up must select last item")
|
||||
// clamp at top
|
||||
precondition(ShortcutLogic.navigate(selection: 0, delta: -1, itemCount: 3) == 0,
|
||||
"selection 0 + up must clamp to 0")
|
||||
// clamp at bottom
|
||||
precondition(ShortcutLogic.navigate(selection: 2, delta: +1, itemCount: 3) == 2,
|
||||
"selection last + down must clamp to last")
|
||||
// normal step down
|
||||
precondition(ShortcutLogic.navigate(selection: 1, delta: +1, itemCount: 3) == 2,
|
||||
"1+down in 3-item list must yield 2")
|
||||
// normal step up
|
||||
precondition(ShortcutLogic.navigate(selection: 1, delta: -1, itemCount: 3) == 0,
|
||||
"1+up in 3-item list must yield 0")
|
||||
// delta 0 — no move
|
||||
precondition(ShortcutLogic.navigate(selection: 1, delta: 0, itemCount: 3) == 1,
|
||||
"delta 0 must not move selection")
|
||||
// empty card → nil (navigateCard guards on cardItemCount > 0, but test the pure function)
|
||||
precondition(ShortcutLogic.navigate(selection: nil, delta: +1, itemCount: 0) == nil,
|
||||
"empty card must return nil")
|
||||
precondition(ShortcutLogic.navigate(selection: 0, delta: +1, itemCount: 0) == nil,
|
||||
"any selection on empty card must return nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal fake Ollama / LM Studio server for CI testing.
|
||||
|
||||
Usage: python3 fake_local_llm.py <port-file>
|
||||
Writes the bound port to <port-file>, then serves until killed.
|
||||
|
||||
Routes:
|
||||
GET /v1/models → two models (one embed, should be filtered out)
|
||||
POST /v1/chat/completions → SSE stream with <think> block, reasoning-only
|
||||
deltas (content=null), and Markdown answer
|
||||
POST /v1/chat/completions → 404 when model == "unknown-model"
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import socket
|
||||
import socketserver
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
# Two models; "nomic-embed-text" must be filtered by the Swift client
|
||||
MODELS_RESPONSE = {
|
||||
"data": [
|
||||
{"id": "llama3.2", "object": "model"},
|
||||
{"id": "nomic-embed-text", "object": "model"},
|
||||
]
|
||||
}
|
||||
|
||||
# SSE events that make up the streamed reply:
|
||||
# - events 0-2: a <think> block split over two chunks (should be hidden from UI)
|
||||
# - event 3: a delta with reasoning_content only (content is null → ignored)
|
||||
# - events 4-7: real Markdown answer
|
||||
SSE_EVENTS = [
|
||||
{"choices": [{"delta": {"content": "<think>\nstep "}}]},
|
||||
{"choices": [{"delta": {"content": "one\n</think>"}}]},
|
||||
# reasoning-only delta: content key present but value is null
|
||||
{"choices": [{"delta": {"content": None, "reasoning_content": "internal"}}]},
|
||||
{"choices": [{"delta": {"content": "## Answer\n\n"}}]},
|
||||
{"choices": [{"delta": {"content": "Here is the result:\n\n"}}]},
|
||||
{"choices": [{"delta": {"content": "- **item 1**\n"}}]},
|
||||
{"choices": [{"delta": {"content": "- item 2\n\n"}}]},
|
||||
{"choices": [{"delta": {"content": "```python\nprint('hello')\n```"}}]},
|
||||
]
|
||||
|
||||
EXPECTED_RESPONSE = "## Answer\n\nHere is the result:\n\n- **item 1**\n- item 2\n\n```python\nprint('hello')\n```"
|
||||
|
||||
|
||||
class FakeLLMHandler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
pass # suppress request logging
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/v1/models":
|
||||
body = json.dumps(MODELS_RESPONSE).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
raw = self.rfile.read(length)
|
||||
try:
|
||||
req_body = json.loads(raw)
|
||||
except Exception:
|
||||
req_body = {}
|
||||
|
||||
model = req_body.get("model", "")
|
||||
|
||||
if self.path != "/v1/chat/completions":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
# Unknown model → 404 with OpenAI-format error
|
||||
if model == "unknown-model":
|
||||
err = json.dumps({
|
||||
"error": {
|
||||
"message": f"model '{model}' not found, try pulling it first",
|
||||
"type": "not_found",
|
||||
}
|
||||
}).encode()
|
||||
self.send_response(404)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(err)))
|
||||
self.end_headers()
|
||||
self.wfile.write(err)
|
||||
return
|
||||
|
||||
# SSE stream
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
|
||||
for event in SSE_EVENTS:
|
||||
line = ("data: " + json.dumps(event) + "\n\n").encode()
|
||||
self.wfile.write(line)
|
||||
self.wfile.flush()
|
||||
time.sleep(0.005)
|
||||
|
||||
self.wfile.write(b"data: [DONE]\n\n")
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
class FastBindHTTPServer(HTTPServer):
|
||||
"""HTTPServer that skips the reverse-DNS lookup in server_bind.
|
||||
|
||||
The default HTTPServer.server_bind calls socket.getfqdn(), which triggers
|
||||
a reverse-DNS lookup for the bound address. On some CI runners this can
|
||||
block for several seconds and cause the port-file handshake to time out.
|
||||
We bypass it by calling TCPServer.server_bind directly and hard-coding
|
||||
server_name to the loopback address we already know we are binding to.
|
||||
"""
|
||||
|
||||
def server_bind(self):
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
self.server_name = "127.0.0.1"
|
||||
self.server_port = self.server_address[1]
|
||||
|
||||
|
||||
def find_free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = find_free_port()
|
||||
server = FastBindHTTPServer(("127.0.0.1", port), FakeLLMHandler)
|
||||
|
||||
# Write port to the file passed as argv[1] so the caller can read it
|
||||
if len(sys.argv) > 1:
|
||||
with open(sys.argv[1], "w") as f:
|
||||
f.write(str(port))
|
||||
f.flush()
|
||||
|
||||
server.serve_forever()
|
||||
Reference in New Issue
Block a user