aven 0.1.16

Local-first task manager CLI and sync server
Documentation
import AvenLocalProofCore
import AvenUniFFI
import Foundation
import XCTest

final class LocalProofTests: XCTestCase {
    func testTypedMappingsCoverEveryStatusAndPriority() {
        XCTAssertEqual(
            TaskStatus.allProofValues.map(ProofValueMapping.statusName),
            ["inbox", "backlog", "todo", "active", "done", "canceled"]
        )
        XCTAssertEqual(
            TaskPriority.allProofValues.map(ProofValueMapping.priorityName),
            ["none", "low", "medium", "high", "urgent"]
        )
    }

    func testOpaqueClientSurvivesWorkerClosureAndDatabaseReopens() async throws {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent("aven-swift-test-\(UUID().uuidString)")
        try FileManager.default.createDirectory(
            at: directory,
            withIntermediateDirectories: true
        )
        defer { try? FileManager.default.removeItem(at: directory) }
        let databasePath = directory.appendingPathComponent("local.sqlite").path
        let worker = RustWorker(label: "dev.aven.swift-local-proof.test")

        let created = try await worker.withClient(at: databasePath) { client in
            let workspace = try client.resolveWorkspace(nameOrKey: "default")
            let task = try client.createTask(
                workspaceId: workspace.id,
                input: CreateTask(
                    title: "Lifetime proof",
                    description: "",
                    project: "swift-proof",
                    status: .todo,
                    priority: .medium,
                    availableAt: nil,
                    dueOn: nil
                )
            )
            return TaskIdentity(workspaceId: workspace.id, taskId: task.id)
        }

        let reopenedTask = try await worker.withClient(at: databasePath) { client in
            try client.fetchTask(
                workspaceId: created.workspaceId,
                taskId: created.taskId
            )
        }
        XCTAssertEqual(reopenedTask.id, created.taskId)
        XCTAssertNil(reopenedTask.availableAt)
        XCTAssertNil(reopenedTask.dueOn)
    }

    func testLocalProofRunsFacadeCallsOffMainThread() async throws {
        let worker = RustWorker(label: "dev.aven.swift-local-proof.thread-test")
        let usedMainThread = try await worker.run { Thread.isMainThread }
        XCTAssertFalse(usedMainThread)
    }

    func testURLSessionTransportForwardsOpaqueRequestWithoutBlockingSiblingTask() async throws {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent("aven-swift-transport-test-\(UUID().uuidString)")
        try FileManager.default.createDirectory(
            at: directory,
            withIntermediateDirectories: true
        )
        defer { try? FileManager.default.removeItem(at: directory) }
        let worker = RustWorker(label: "dev.aven.swift-transport-test.rust")
        let prepared = try await worker.withClient(
            at: directory.appendingPathComponent("transport.sqlite").path
        ) { client in
            let workspace = try client.resolveWorkspace(nameOrKey: "default")
            _ = try client.createTask(
                workspaceId: workspace.id,
                input: CreateTask(
                    title: "opaque transport fixture",
                    description: "",
                    project: "swift-proof",
                    status: .todo,
                    priority: .medium,
                    availableAt: nil,
                    dueOn: nil
                )
            )
            let session = try client.startSyncSession(
                server: "https://sync.invalid",
                authToken: "private-token",
                pageBudget: nil
            )
            guard let request = try session.prepareRequest() else {
                throw ProofFailure.invariant("transport test had no request")
            }
            return request
        }

        let state = BlockingURLProtocol.state
        state.reset()
        let configuration = URLSessionConfiguration.ephemeral
        configuration.protocolClasses = [BlockingURLProtocol.self]
        let session = URLSession(configuration: configuration)
        defer { session.invalidateAndCancel() }
        let transport = URLSessionTransport(session: session)
        let sendTask = Task { try await transport.send(prepared) }

        XCTAssertTrue(state.waitUntilStarted(timeout: 2))
        let siblingTask = Task { 42 }
        let siblingValue = await siblingTask.value
        XCTAssertEqual(siblingValue, 42)
        state.releaseResponse()
        let response = try await sendTask.value

        let captured = try XCTUnwrap(state.capturedRequest())
        XCTAssertEqual(captured.method, prepared.method)
        XCTAssertEqual(captured.url, prepared.url)
        XCTAssertEqual(captured.body, prepared.body)
        XCTAssertEqual(captured.authorization, "Bearer private-token")
        XCTAssertEqual(response.status, 201)
        XCTAssertEqual(response.body, Data([0x01, 0x02, 0x03]))
        XCTAssertEqual(
            response.headers,
            [
                SyncHttpHeader(name: "content-encoding", value: "gzip"),
                SyncHttpHeader(name: "content-length", value: "99"),
                SyncHttpHeader(name: "content-type", value: "application/octet-stream"),
            ]
        )

        state.reset()
        let limitedTransport = URLSessionTransport(session: session, maxResponseBytes: 2)
        let limitedTask = Task { try await limitedTransport.send(prepared) }
        XCTAssertTrue(state.waitUntilStarted(timeout: 2))
        state.releaseResponse()
        do {
            _ = try await limitedTask.value
            XCTFail("oversized response must fail")
        } catch let error as URLSessionTransportError {
            XCTAssertEqual(error, .responseTooLarge(limit: 2))
        }
    }
}

private final class BlockingURLProtocol: URLProtocol, @unchecked Sendable {
    static let state = BlockingURLProtocolState()

    override class func canInit(with _: URLRequest) -> Bool {
        true
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        request
    }

    override func startLoading() {
        Self.state.record(request)
        Self.state.waitForRelease()
        guard let url = request.url,
              let response = HTTPURLResponse(
                  url: url,
                  statusCode: 201,
                  httpVersion: nil,
                  headerFields: [
                      "Content-Type": "application/octet-stream",
                      "Content-Encoding": "gzip",
                      "Content-Length": "99",
                  ]
              )
        else {
            client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
            return
        }
        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        client?.urlProtocol(self, didLoad: Data([0x01, 0x02, 0x03]))
        client?.urlProtocolDidFinishLoading(self)
    }

    override func stopLoading() {
        Self.state.releaseResponse()
    }
}

private final class BlockingURLProtocolState: @unchecked Sendable {
    struct CapturedRequest: Sendable {
        let method: String?
        let url: String?
        let body: Data?
        let authorization: String?
    }

    private let lock = NSLock()
    private var captured: CapturedRequest?
    private var started = DispatchSemaphore(value: 0)
    private var release = DispatchSemaphore(value: 0)

    func reset() {
        lock.lock()
        captured = nil
        started = DispatchSemaphore(value: 0)
        release = DispatchSemaphore(value: 0)
        lock.unlock()
    }

    func record(_ request: URLRequest) {
        lock.lock()
        captured = CapturedRequest(
            method: request.httpMethod,
            url: request.url?.absoluteString,
            body: request.httpBody ?? readBody(from: request.httpBodyStream),
            authorization: request.value(forHTTPHeaderField: "Authorization")
        )
        lock.unlock()
        started.signal()
    }

    func waitUntilStarted(timeout: TimeInterval) -> Bool {
        started.wait(timeout: .now() + timeout) == .success
    }

    func waitForRelease() {
        release.wait()
    }

    func releaseResponse() {
        release.signal()
    }

    func capturedRequest() -> CapturedRequest? {
        lock.lock()
        defer { lock.unlock() }
        return captured
    }

    private func readBody(from stream: InputStream?) -> Data? {
        guard let stream else { return nil }
        stream.open()
        defer { stream.close() }
        var data = Data()
        var buffer = [UInt8](repeating: 0, count: 4096)
        while stream.hasBytesAvailable {
            let count = stream.read(&buffer, maxLength: buffer.count)
            guard count > 0 else { break }
            data.append(buffer, count: count)
        }
        return data
    }
}

private struct TaskIdentity: Sendable {
    let workspaceId: String
    let taskId: String
}

private extension TaskStatus {
    static let allProofValues: [TaskStatus] = [
        .inbox, .backlog, .todo, .active, .done, .canceled,
    ]
}

private extension TaskPriority {
    static let allProofValues: [TaskPriority] = [
        .none, .low, .medium, .high, .urgent,
    ]
}