Files
oai-swift/oAITests/CLIServerServiceTests.swift
T
rune 897bfdce10 Add local CLI access via Unix-socket server
New CLIServerService listens on a Unix domain socket
(~/Library/Application Support/oAI/cli.sock) speaking a minimal
HTTP/1.1 subset, for one-shot non-streaming shell access to a single
fixed model — e.g. an `ai "prompt"` zsh function — without opening
the app window and without going through the tool-calling loop.

Configured in Settings > MCP > CLI Access (toggle, provider, model —
deliberately independent of the chat UI's active model). JSON
request/response envelope rather than raw text so new fields (model
override, streaming, tool support) can be added later without a
breaking wire-format change.

Verified live end-to-end against a real OpenRouter request, error
paths, and clean-shutdown socket cleanup. 10 new unit tests cover
the HTTP framing/parsing logic.
2026-08-05 13:33:02 +02:00

108 lines
4.5 KiB
Swift

//
// CLIServerServiceTests.swift
// oAITests
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
import Testing
import Foundation
@testable import Confab
@Suite("CLIServerService pure HTTP helpers")
struct CLIServerServiceTests {
// MARK: - parseRequestBody
@Test("Returns the body once headers and full Content-Length body have arrived")
func parsesCompleteRequest() {
let body = "{\"prompt\":\"hi\"}"
let raw = "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: \(body.utf8.count)\r\n\r\n\(body)"
let result = CLIServerService.parseRequestBody(from: Data(raw.utf8))
#expect(result.map { String(data: $0, encoding: .utf8) } == body)
}
@Test("Returns nil when the header block hasn't fully arrived yet")
func returnsNilForIncompleteHeaders() {
let raw = "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5"
#expect(CLIServerService.parseRequestBody(from: Data(raw.utf8)) == nil)
}
@Test("Returns nil when the body hasn't fully arrived yet")
func returnsNilForIncompleteBody() {
let raw = "POST / HTTP/1.1\r\nContent-Length: 20\r\n\r\n{\"prompt\":\"hi\"}"
#expect(CLIServerService.parseRequestBody(from: Data(raw.utf8)) == nil)
}
@Test("Content-Length header name match is case-insensitive")
func headerNameIsCaseInsensitive() {
let body = "abc"
let raw = "POST / HTTP/1.1\r\ncontent-length: 3\r\n\r\n\(body)"
let result = CLIServerService.parseRequestBody(from: Data(raw.utf8))
#expect(result.map { String(data: $0, encoding: .utf8) } == body)
}
@Test("Missing Content-Length is treated as a zero-length body")
func missingContentLengthIsEmptyBody() {
let raw = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
let result = CLIServerService.parseRequestBody(from: Data(raw.utf8))
#expect(result?.isEmpty == true)
}
@Test("Extra trailing bytes beyond Content-Length don't prevent extraction (pipelined data ignored)")
func extraTrailingBytesStillExtractsBody() {
let body = "abc"
let raw = "POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\n\(body)EXTRA"
let result = CLIServerService.parseRequestBody(from: Data(raw.utf8))
#expect(result.map { String(data: $0, encoding: .utf8) } == body)
}
// MARK: - Response encoding
@Test("successResponse produces a 200 with the response field set")
func successResponseShape() throws {
let data = CLIServerService.successResponse("hello world")
let text = String(data: data, encoding: .utf8)!
#expect(text.hasPrefix("HTTP/1.1 200 OK\r\n"))
#expect(text.contains("Content-Type: application/json"))
let bodyStart = text.range(of: "\r\n\r\n")!.upperBound
let bodyJSON = Data(text[bodyStart...].utf8)
let decoded = try JSONDecoder().decode(CLIServerService.AskResponseBody.self, from: bodyJSON)
#expect(decoded.response == "hello world")
#expect(decoded.error == nil)
}
@Test("errorResponse defaults to 400 Bad Request")
func errorResponseDefaultStatus() throws {
let data = CLIServerService.errorResponse("bad input")
let text = String(data: data, encoding: .utf8)!
#expect(text.hasPrefix("HTTP/1.1 400 Bad Request\r\n"))
let bodyStart = text.range(of: "\r\n\r\n")!.upperBound
let bodyJSON = Data(text[bodyStart...].utf8)
let decoded = try JSONDecoder().decode(CLIServerService.AskResponseBody.self, from: bodyJSON)
#expect(decoded.error == "bad input")
#expect(decoded.response == nil)
}
@Test("errorResponse supports a custom status code")
func errorResponseCustomStatus() {
let data = CLIServerService.errorResponse("provider failed", statusCode: 500)
let text = String(data: data, encoding: .utf8)!
#expect(text.hasPrefix("HTTP/1.1 500 Internal Server Error\r\n"))
}
@Test("Content-Length in the response header matches the actual JSON body byte count")
func responseContentLengthMatchesBody() {
let data = CLIServerService.successResponse("hello")
let text = String(data: data, encoding: .utf8)!
let headerEnd = text.range(of: "\r\n\r\n")!.upperBound
let bodyByteCount = Data(text[headerEnd...].utf8).count
let lengthLine = text.split(separator: "\r\n").first { $0.hasPrefix("Content-Length:") }!
let declaredLength = Int(lengthLine.split(separator: " ")[1])!
#expect(declaredLength == bodyByteCount)
}
}