mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-09 19:29:41 +00:00
103 lines
4.1 KiB
Swift
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()
|
|
}
|