Files
hapi/ios/Packages/HapiKit/Tests/HapiClientTests/Files/FilesTestSupport.swift
T

103 lines
4.1 KiB
Swift

import Foundation
import HapiClient
import HapiProtocol
// Shared fakes/helpers of the files-feature model suites (A-M4a), mirroring
// the Android `FakeFilesGateway`.
struct DiffFileCall: Equatable, Sendable {
var path: String
var staged: Bool?
}
struct SearchCall: Equatable, Sendable {
var query: String
var limit: Int
}
/// A transport-level failure with a message, like the Android
/// `IllegalStateException("offline")` used by the reference tests.
struct FakeTransportError: Error, LocalizedError {
let message: String
var errorDescription: String? { message }
}
/// Scripted `FilesRequesting` double: fixed responses per endpoint plus call
/// recording. An actor so the models' concurrent loads stay data-race free.
actor FakeFilesGateway: FilesRequesting {
private var statusResult = GitCommandResponse(success: true, stdout: "")
private var unstagedNumstat = GitCommandResponse(success: true, stdout: "")
private var stagedNumstat = GitCommandResponse(success: true, stdout: "")
private var diffFileResult = GitCommandResponse(success: true, stdout: "")
private var readFileResult = FileReadResponse(success: true, content: "")
private var searchResult = FileSearchResponse(success: true, files: [])
/// Keyed by requested path; nil key = session root.
private var directories: [String?: ListDirectoryResponse] = [:]
private var statusError: (any Error)?
private(set) var diffFileCalls: [DiffFileCall] = []
private(set) var searchCalls: [SearchCall] = []
private(set) var listDirectoryCalls: [String?] = []
private(set) var numstatCalls: [Bool] = []
// MARK: Scripting
func setStatus(_ response: GitCommandResponse) { statusResult = response }
func setUnstagedNumstat(_ response: GitCommandResponse) { unstagedNumstat = response }
func setStagedNumstat(_ response: GitCommandResponse) { stagedNumstat = response }
func setDiffFile(_ response: GitCommandResponse) { diffFileResult = response }
func setReadFile(_ response: FileReadResponse) { readFileResult = response }
func setSearch(_ response: FileSearchResponse) { searchResult = response }
func setDirectory(_ path: String?, _ response: ListDirectoryResponse) { directories[path] = response }
func setStatusError(_ error: any Error) { statusError = error }
// MARK: FilesRequesting
func gitStatus(sessionId: String) async throws -> GitCommandResponse {
if let statusError { throw statusError }
return statusResult
}
func gitDiffNumstat(sessionId: String, staged: Bool) async throws -> GitCommandResponse {
numstatCalls.append(staged)
return staged ? stagedNumstat : unstagedNumstat
}
func gitDiffFile(sessionId: String, path: String, staged: Bool?) async throws -> GitCommandResponse {
diffFileCalls.append(DiffFileCall(path: path, staged: staged))
return diffFileResult
}
func readSessionFile(sessionId: String, path: String) async throws -> FileReadResponse {
readFileResult
}
func searchSessionFiles(sessionId: String, query: String, limit: Int) async throws -> FileSearchResponse {
searchCalls.append(SearchCall(query: query, limit: limit))
return searchResult
}
func listSessionDirectory(sessionId: String, path: String?) async throws -> ListDirectoryResponse {
listDirectoryCalls.append(path)
return directories[path] ?? ListDirectoryResponse(success: true, entries: [])
}
}
/// Polls `condition` (10 ms cadence) until true or `timeout`; returns the
/// final verdict for `#expect` (same shape as the ChatInteractor suite's
/// helper; distinct name — `expectEventually`/`waitUntil` already exist in
/// this module with sync/@Sendable closures).
@MainActor
func filesEventually(
timeout: Duration = .seconds(5),
_ condition: @MainActor () async -> Bool
) async -> Bool {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: timeout)
while clock.now < deadline {
if await condition() { return true }
try? await Task.sleep(for: .milliseconds(10))
}
return await condition()
}