@dvai-bridge/ios-llama-core 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +341 -34
- package/Package.swift +71 -71
- package/ios/Sources/DVAILlamaCore/AudioDecoder.swift +112 -112
- package/ios/Sources/DVAILlamaCore/ContentPartsTranslator.swift +232 -232
- package/ios/Sources/DVAILlamaCore/ImageDecoder.swift +91 -91
- package/ios/Sources/DVAILlamaCore/LlamaCppBridgeProtocol.swift +59 -59
- package/ios/Sources/DVAILlamaCore/LlamaHandlers.swift +422 -422
- package/ios/Sources/DVAILlamaCore/ModelDownloader.swift +445 -445
- package/ios/Sources/DVAILlamaCore/PluginState.swift +158 -158
- package/ios/Sources/DVAILlamaCoreObjC/LlamaCppBridge.mm +649 -649
- package/ios/Sources/DVAILlamaCoreObjC/include/LlamaCppBridge.h +101 -101
- package/ios/Tests/DVAILlamaCoreTests/AudioDecoderTest.swift +46 -46
- package/ios/Tests/DVAILlamaCoreTests/ContentPartsTranslatorTest.swift +361 -361
- package/ios/Tests/DVAILlamaCoreTests/ImageDecoderTest.swift +139 -139
- package/ios/Tests/DVAILlamaCoreTests/LlamaCppBridgeTest.swift +131 -131
- package/ios/Tests/DVAILlamaCoreTests/LlamaHandlersTest.swift +515 -515
- package/ios/Tests/DVAILlamaCoreTests/ModelDownloaderTest.swift +89 -89
- package/ios/Tests/DVAILlamaCoreTests/PluginStateTest.swift +51 -51
- package/package.json +3 -3
- package/README.md +0 -199
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAILlamaCore
|
|
3
|
-
|
|
4
|
-
final class ImageDecoderTest: XCTestCase {
|
|
5
|
-
/// `data:image/png;base64,...` round-trips to bytes whose first 8 bytes
|
|
6
|
-
/// are the canonical PNG magic header.
|
|
7
|
-
func testDataURLBase64() async throws {
|
|
8
|
-
let url = try String(contentsOf: imageFixtureURL("tiny-test-base64.txt"), encoding: .utf8)
|
|
9
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
10
|
-
let bytes = try await ImageDecoder.resolve(url: url)
|
|
11
|
-
XCTAssertEqual(
|
|
12
|
-
Array(bytes.prefix(8)),
|
|
13
|
-
[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
|
|
14
|
-
"expected PNG magic header"
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/// `file://` URLs return the raw bytes off disk.
|
|
19
|
-
func testFileURL() async throws {
|
|
20
|
-
let pngURL = imageFixtureURL("tiny-test.png")
|
|
21
|
-
let result = try await ImageDecoder.resolve(url: pngURL.absoluteString)
|
|
22
|
-
let raw = try Data(contentsOf: pngURL)
|
|
23
|
-
XCTAssertEqual(result, raw)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// Unsupported schemes throw `invalidScheme`.
|
|
27
|
-
func testInvalidScheme() async {
|
|
28
|
-
do {
|
|
29
|
-
_ = try await ImageDecoder.resolve(url: "ftp://example.com/x.png")
|
|
30
|
-
XCTFail("Expected throw")
|
|
31
|
-
} catch ImageSourceError.invalidScheme {
|
|
32
|
-
// expected
|
|
33
|
-
} catch {
|
|
34
|
-
XCTFail("Unexpected error type: \(error)")
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/// `data:` URL with no comma → `malformedDataURL`.
|
|
39
|
-
func testMalformedDataURL() async {
|
|
40
|
-
do {
|
|
41
|
-
_ = try await ImageDecoder.resolve(url: "data:image/png;base64")
|
|
42
|
-
XCTFail("Expected throw")
|
|
43
|
-
} catch ImageSourceError.malformedDataURL {
|
|
44
|
-
// expected
|
|
45
|
-
} catch {
|
|
46
|
-
XCTFail("Unexpected error type: \(error)")
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/// `https://` URL fetches response body bytes verbatim. Mocked at the
|
|
51
|
-
/// URLSession layer via `URLProtocol.registerClass` so no real network
|
|
52
|
-
/// is touched.
|
|
53
|
-
func testHTTPSFetchesBytes() async throws {
|
|
54
|
-
let payload = try Data(contentsOf: imageFixtureURL("tiny-test.png"))
|
|
55
|
-
URLProtocol.registerClass(MockURLProtocol.self)
|
|
56
|
-
defer {
|
|
57
|
-
URLProtocol.unregisterClass(MockURLProtocol.self)
|
|
58
|
-
MockURLProtocol.handler = nil
|
|
59
|
-
}
|
|
60
|
-
MockURLProtocol.handler = { request in
|
|
61
|
-
let response = HTTPURLResponse(
|
|
62
|
-
url: request.url!,
|
|
63
|
-
statusCode: 200,
|
|
64
|
-
httpVersion: "HTTP/1.1",
|
|
65
|
-
headerFields: nil
|
|
66
|
-
)!
|
|
67
|
-
return (response, payload)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let bytes = try await ImageDecoder.resolve(url: "https://example.invalid/img.png")
|
|
71
|
-
XCTAssertEqual(bytes, payload)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/// HTTP non-2xx → `ImageSourceError.httpError(status:)` carrying the code.
|
|
75
|
-
func testHTTPErrorThrowsHttpError() async {
|
|
76
|
-
URLProtocol.registerClass(MockURLProtocol.self)
|
|
77
|
-
defer {
|
|
78
|
-
URLProtocol.unregisterClass(MockURLProtocol.self)
|
|
79
|
-
MockURLProtocol.handler = nil
|
|
80
|
-
}
|
|
81
|
-
MockURLProtocol.handler = { request in
|
|
82
|
-
let response = HTTPURLResponse(
|
|
83
|
-
url: request.url!,
|
|
84
|
-
statusCode: 404,
|
|
85
|
-
httpVersion: "HTTP/1.1",
|
|
86
|
-
headerFields: nil
|
|
87
|
-
)!
|
|
88
|
-
return (response, Data())
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
do {
|
|
92
|
-
_ = try await ImageDecoder.resolve(url: "https://example.invalid/missing.png")
|
|
93
|
-
XCTFail("Expected throw")
|
|
94
|
-
} catch ImageSourceError.httpError(let status) {
|
|
95
|
-
XCTAssertEqual(status, 404)
|
|
96
|
-
} catch {
|
|
97
|
-
XCTFail("Unexpected error type: \(error)")
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/// Walks up from this test source file until it finds the repo-root
|
|
102
|
-
/// `fixtures/` directory — same pattern as `AudioDecoderTest`.
|
|
103
|
-
private func imageFixtureURL(_ name: String) -> URL {
|
|
104
|
-
var dir = URL(fileURLWithPath: #file).deletingLastPathComponent()
|
|
105
|
-
while !FileManager.default.fileExists(atPath: dir.appendingPathComponent("fixtures").path) {
|
|
106
|
-
let parent = dir.deletingLastPathComponent()
|
|
107
|
-
if parent.path == dir.path {
|
|
108
|
-
fatalError("fixtures dir not found walking up from \(#file)")
|
|
109
|
-
}
|
|
110
|
-
dir = parent
|
|
111
|
-
}
|
|
112
|
-
return dir.appendingPathComponent("fixtures").appendingPathComponent("images").appendingPathComponent(name)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/// In-process `URLProtocol` stub that intercepts every URLSession request
|
|
117
|
-
/// and dispatches it to a per-test handler. Registered globally via
|
|
118
|
-
/// `URLProtocol.registerClass`, which `URLSession.shared` consults — so the
|
|
119
|
-
/// production code under test (which uses `URLSession.shared`) is exercised
|
|
120
|
-
/// without any actual network I/O.
|
|
121
|
-
private final class MockURLProtocol: URLProtocol {
|
|
122
|
-
static var handler: ((URLRequest) -> (HTTPURLResponse, Data))?
|
|
123
|
-
|
|
124
|
-
override class func canInit(with request: URLRequest) -> Bool { true }
|
|
125
|
-
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
|
126
|
-
|
|
127
|
-
override func startLoading() {
|
|
128
|
-
guard let handler = MockURLProtocol.handler else {
|
|
129
|
-
client?.urlProtocol(self, didFailWithError: NSError(domain: "MockURLProtocol", code: -1))
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
let (response, data) = handler(request)
|
|
133
|
-
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
134
|
-
client?.urlProtocol(self, didLoad: data)
|
|
135
|
-
client?.urlProtocolDidFinishLoading(self)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
override func stopLoading() {}
|
|
139
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAILlamaCore
|
|
3
|
+
|
|
4
|
+
final class ImageDecoderTest: XCTestCase {
|
|
5
|
+
/// `data:image/png;base64,...` round-trips to bytes whose first 8 bytes
|
|
6
|
+
/// are the canonical PNG magic header.
|
|
7
|
+
func testDataURLBase64() async throws {
|
|
8
|
+
let url = try String(contentsOf: imageFixtureURL("tiny-test-base64.txt"), encoding: .utf8)
|
|
9
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
10
|
+
let bytes = try await ImageDecoder.resolve(url: url)
|
|
11
|
+
XCTAssertEqual(
|
|
12
|
+
Array(bytes.prefix(8)),
|
|
13
|
+
[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
|
|
14
|
+
"expected PNG magic header"
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// `file://` URLs return the raw bytes off disk.
|
|
19
|
+
func testFileURL() async throws {
|
|
20
|
+
let pngURL = imageFixtureURL("tiny-test.png")
|
|
21
|
+
let result = try await ImageDecoder.resolve(url: pngURL.absoluteString)
|
|
22
|
+
let raw = try Data(contentsOf: pngURL)
|
|
23
|
+
XCTAssertEqual(result, raw)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Unsupported schemes throw `invalidScheme`.
|
|
27
|
+
func testInvalidScheme() async {
|
|
28
|
+
do {
|
|
29
|
+
_ = try await ImageDecoder.resolve(url: "ftp://example.com/x.png")
|
|
30
|
+
XCTFail("Expected throw")
|
|
31
|
+
} catch ImageSourceError.invalidScheme {
|
|
32
|
+
// expected
|
|
33
|
+
} catch {
|
|
34
|
+
XCTFail("Unexpected error type: \(error)")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// `data:` URL with no comma → `malformedDataURL`.
|
|
39
|
+
func testMalformedDataURL() async {
|
|
40
|
+
do {
|
|
41
|
+
_ = try await ImageDecoder.resolve(url: "data:image/png;base64")
|
|
42
|
+
XCTFail("Expected throw")
|
|
43
|
+
} catch ImageSourceError.malformedDataURL {
|
|
44
|
+
// expected
|
|
45
|
+
} catch {
|
|
46
|
+
XCTFail("Unexpected error type: \(error)")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// `https://` URL fetches response body bytes verbatim. Mocked at the
|
|
51
|
+
/// URLSession layer via `URLProtocol.registerClass` so no real network
|
|
52
|
+
/// is touched.
|
|
53
|
+
func testHTTPSFetchesBytes() async throws {
|
|
54
|
+
let payload = try Data(contentsOf: imageFixtureURL("tiny-test.png"))
|
|
55
|
+
URLProtocol.registerClass(MockURLProtocol.self)
|
|
56
|
+
defer {
|
|
57
|
+
URLProtocol.unregisterClass(MockURLProtocol.self)
|
|
58
|
+
MockURLProtocol.handler = nil
|
|
59
|
+
}
|
|
60
|
+
MockURLProtocol.handler = { request in
|
|
61
|
+
let response = HTTPURLResponse(
|
|
62
|
+
url: request.url!,
|
|
63
|
+
statusCode: 200,
|
|
64
|
+
httpVersion: "HTTP/1.1",
|
|
65
|
+
headerFields: nil
|
|
66
|
+
)!
|
|
67
|
+
return (response, payload)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let bytes = try await ImageDecoder.resolve(url: "https://example.invalid/img.png")
|
|
71
|
+
XCTAssertEqual(bytes, payload)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// HTTP non-2xx → `ImageSourceError.httpError(status:)` carrying the code.
|
|
75
|
+
func testHTTPErrorThrowsHttpError() async {
|
|
76
|
+
URLProtocol.registerClass(MockURLProtocol.self)
|
|
77
|
+
defer {
|
|
78
|
+
URLProtocol.unregisterClass(MockURLProtocol.self)
|
|
79
|
+
MockURLProtocol.handler = nil
|
|
80
|
+
}
|
|
81
|
+
MockURLProtocol.handler = { request in
|
|
82
|
+
let response = HTTPURLResponse(
|
|
83
|
+
url: request.url!,
|
|
84
|
+
statusCode: 404,
|
|
85
|
+
httpVersion: "HTTP/1.1",
|
|
86
|
+
headerFields: nil
|
|
87
|
+
)!
|
|
88
|
+
return (response, Data())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
do {
|
|
92
|
+
_ = try await ImageDecoder.resolve(url: "https://example.invalid/missing.png")
|
|
93
|
+
XCTFail("Expected throw")
|
|
94
|
+
} catch ImageSourceError.httpError(let status) {
|
|
95
|
+
XCTAssertEqual(status, 404)
|
|
96
|
+
} catch {
|
|
97
|
+
XCTFail("Unexpected error type: \(error)")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Walks up from this test source file until it finds the repo-root
|
|
102
|
+
/// `fixtures/` directory — same pattern as `AudioDecoderTest`.
|
|
103
|
+
private func imageFixtureURL(_ name: String) -> URL {
|
|
104
|
+
var dir = URL(fileURLWithPath: #file).deletingLastPathComponent()
|
|
105
|
+
while !FileManager.default.fileExists(atPath: dir.appendingPathComponent("fixtures").path) {
|
|
106
|
+
let parent = dir.deletingLastPathComponent()
|
|
107
|
+
if parent.path == dir.path {
|
|
108
|
+
fatalError("fixtures dir not found walking up from \(#file)")
|
|
109
|
+
}
|
|
110
|
+
dir = parent
|
|
111
|
+
}
|
|
112
|
+
return dir.appendingPathComponent("fixtures").appendingPathComponent("images").appendingPathComponent(name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// In-process `URLProtocol` stub that intercepts every URLSession request
|
|
117
|
+
/// and dispatches it to a per-test handler. Registered globally via
|
|
118
|
+
/// `URLProtocol.registerClass`, which `URLSession.shared` consults — so the
|
|
119
|
+
/// production code under test (which uses `URLSession.shared`) is exercised
|
|
120
|
+
/// without any actual network I/O.
|
|
121
|
+
private final class MockURLProtocol: URLProtocol {
|
|
122
|
+
static var handler: ((URLRequest) -> (HTTPURLResponse, Data))?
|
|
123
|
+
|
|
124
|
+
override class func canInit(with request: URLRequest) -> Bool { true }
|
|
125
|
+
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
|
126
|
+
|
|
127
|
+
override func startLoading() {
|
|
128
|
+
guard let handler = MockURLProtocol.handler else {
|
|
129
|
+
client?.urlProtocol(self, didFailWithError: NSError(domain: "MockURLProtocol", code: -1))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
let (response, data) = handler(request)
|
|
133
|
+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
134
|
+
client?.urlProtocol(self, didLoad: data)
|
|
135
|
+
client?.urlProtocolDidFinishLoading(self)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override func stopLoading() {}
|
|
139
|
+
}
|
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAILlamaCore
|
|
3
|
-
import DVAILlamaCoreObjC
|
|
4
|
-
|
|
5
|
-
final class LlamaCppBridgeTest: XCTestCase {
|
|
6
|
-
func testInitiallyNotLoaded() {
|
|
7
|
-
let bridge = LlamaCppBridge()
|
|
8
|
-
XCTAssertFalse(bridge.isLoaded)
|
|
9
|
-
XCTAssertNil(bridge.currentModelPath)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
func testLoadEmptyPathFails() {
|
|
13
|
-
let bridge = LlamaCppBridge()
|
|
14
|
-
XCTAssertThrowsError(
|
|
15
|
-
try bridge.loadModel(
|
|
16
|
-
atPath: "",
|
|
17
|
-
mmprojPath: nil,
|
|
18
|
-
gpuLayers: 99,
|
|
19
|
-
contextSize: 2048,
|
|
20
|
-
threads: 4,
|
|
21
|
-
embeddingMode: false
|
|
22
|
-
)
|
|
23
|
-
)
|
|
24
|
-
XCTAssertFalse(bridge.isLoaded)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
func testLoadFakePathFails() {
|
|
28
|
-
let bridge = LlamaCppBridge()
|
|
29
|
-
XCTAssertThrowsError(
|
|
30
|
-
try bridge.loadModel(
|
|
31
|
-
atPath: "/tmp/definitely-does-not-exist.gguf",
|
|
32
|
-
mmprojPath: nil,
|
|
33
|
-
gpuLayers: 99,
|
|
34
|
-
contextSize: 2048,
|
|
35
|
-
threads: 4,
|
|
36
|
-
embeddingMode: false
|
|
37
|
-
)
|
|
38
|
-
)
|
|
39
|
-
XCTAssertFalse(bridge.isLoaded)
|
|
40
|
-
XCTAssertNil(bridge.currentModelPath)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func testVersionStringContainsLlama() {
|
|
44
|
-
let bridge = LlamaCppBridge()
|
|
45
|
-
let version = bridge.versionString()
|
|
46
|
-
let prefix = "llama.cpp "
|
|
47
|
-
XCTAssertTrue(
|
|
48
|
-
version.hasPrefix(prefix),
|
|
49
|
-
"expected versionString to start with '\(prefix)', got: \(version)"
|
|
50
|
-
)
|
|
51
|
-
XCTAssertGreaterThan(
|
|
52
|
-
version.count,
|
|
53
|
-
prefix.count,
|
|
54
|
-
"expected versionString to include system info after prefix, got: \(version)"
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// MARK: - Multimodal stubs (Phase 2A Pass 1)
|
|
59
|
-
|
|
60
|
-
func testInitiallyMmprojNotLoaded() {
|
|
61
|
-
let bridge = LlamaCppBridge()
|
|
62
|
-
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
func testLoadMmprojRequiresMainModel() {
|
|
66
|
-
let bridge = LlamaCppBridge()
|
|
67
|
-
// Main model never loaded -> should fail with code 31.
|
|
68
|
-
XCTAssertThrowsError(try bridge.loadMmproj(atPath: "/tmp/fake-mmproj.gguf"))
|
|
69
|
-
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
func testEmptyMmprojPathFails() {
|
|
73
|
-
let bridge = LlamaCppBridge()
|
|
74
|
-
XCTAssertThrowsError(try bridge.loadMmproj(atPath: ""))
|
|
75
|
-
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
func testUnloadMmprojIsIdempotent() {
|
|
79
|
-
let bridge = LlamaCppBridge()
|
|
80
|
-
// Repeated unload calls on a never-loaded bridge must not crash.
|
|
81
|
-
bridge.unloadMmproj()
|
|
82
|
-
bridge.unloadMmproj()
|
|
83
|
-
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// MARK: - Multimodal — Phase 2A Pass 2
|
|
87
|
-
|
|
88
|
-
/// Without a loaded model, completeMultimodalPrompt must return an error
|
|
89
|
-
/// (code 50, "Model not loaded"). We can verify this without a real model.
|
|
90
|
-
func testCompleteMultimodalRequiresModelLoaded() {
|
|
91
|
-
let bridge = LlamaCppBridge()
|
|
92
|
-
XCTAssertFalse(bridge.isLoaded)
|
|
93
|
-
XCTAssertThrowsError(
|
|
94
|
-
try bridge.completeMultimodalPrompt(
|
|
95
|
-
"ignored <__media__>",
|
|
96
|
-
media: [Data([0x00, 0x01, 0x02])],
|
|
97
|
-
maxTokens: 8,
|
|
98
|
-
temperature: 0.0,
|
|
99
|
-
topP: 1.0
|
|
100
|
-
)
|
|
101
|
-
) { error in
|
|
102
|
-
let nsErr = error as NSError
|
|
103
|
-
XCTAssertEqual(nsErr.domain, "DVAIBridgeLlama")
|
|
104
|
-
XCTAssertEqual(nsErr.code, 50, "expected code 50 (Model not loaded)")
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/// applyChatTemplate without a loaded model returns the "Model not loaded"
|
|
109
|
-
/// error (code 40). Same can-run-without-model property as above.
|
|
110
|
-
func testApplyChatTemplateRequiresModelLoaded() {
|
|
111
|
-
let bridge = LlamaCppBridge()
|
|
112
|
-
XCTAssertFalse(bridge.isLoaded)
|
|
113
|
-
XCTAssertThrowsError(
|
|
114
|
-
try bridge.applyChatTemplate(
|
|
115
|
-
nil,
|
|
116
|
-
messages: [["role": "user", "content": "hi"]],
|
|
117
|
-
addAssistant: true
|
|
118
|
-
)
|
|
119
|
-
) { error in
|
|
120
|
-
let nsErr = error as NSError
|
|
121
|
-
XCTAssertEqual(nsErr.domain, "DVAIBridgeLlama")
|
|
122
|
-
XCTAssertEqual(nsErr.code, 40, "expected code 40 (Model not loaded)")
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/// hasAudioEncoder is always false when no mmproj is loaded.
|
|
127
|
-
func testHasAudioEncoderFalseWithoutMmproj() {
|
|
128
|
-
let bridge = LlamaCppBridge()
|
|
129
|
-
XCTAssertFalse(bridge.hasAudioEncoder())
|
|
130
|
-
}
|
|
131
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAILlamaCore
|
|
3
|
+
import DVAILlamaCoreObjC
|
|
4
|
+
|
|
5
|
+
final class LlamaCppBridgeTest: XCTestCase {
|
|
6
|
+
func testInitiallyNotLoaded() {
|
|
7
|
+
let bridge = LlamaCppBridge()
|
|
8
|
+
XCTAssertFalse(bridge.isLoaded)
|
|
9
|
+
XCTAssertNil(bridge.currentModelPath)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func testLoadEmptyPathFails() {
|
|
13
|
+
let bridge = LlamaCppBridge()
|
|
14
|
+
XCTAssertThrowsError(
|
|
15
|
+
try bridge.loadModel(
|
|
16
|
+
atPath: "",
|
|
17
|
+
mmprojPath: nil,
|
|
18
|
+
gpuLayers: 99,
|
|
19
|
+
contextSize: 2048,
|
|
20
|
+
threads: 4,
|
|
21
|
+
embeddingMode: false
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
XCTAssertFalse(bridge.isLoaded)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testLoadFakePathFails() {
|
|
28
|
+
let bridge = LlamaCppBridge()
|
|
29
|
+
XCTAssertThrowsError(
|
|
30
|
+
try bridge.loadModel(
|
|
31
|
+
atPath: "/tmp/definitely-does-not-exist.gguf",
|
|
32
|
+
mmprojPath: nil,
|
|
33
|
+
gpuLayers: 99,
|
|
34
|
+
contextSize: 2048,
|
|
35
|
+
threads: 4,
|
|
36
|
+
embeddingMode: false
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
XCTAssertFalse(bridge.isLoaded)
|
|
40
|
+
XCTAssertNil(bridge.currentModelPath)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func testVersionStringContainsLlama() {
|
|
44
|
+
let bridge = LlamaCppBridge()
|
|
45
|
+
let version = bridge.versionString()
|
|
46
|
+
let prefix = "llama.cpp "
|
|
47
|
+
XCTAssertTrue(
|
|
48
|
+
version.hasPrefix(prefix),
|
|
49
|
+
"expected versionString to start with '\(prefix)', got: \(version)"
|
|
50
|
+
)
|
|
51
|
+
XCTAssertGreaterThan(
|
|
52
|
+
version.count,
|
|
53
|
+
prefix.count,
|
|
54
|
+
"expected versionString to include system info after prefix, got: \(version)"
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Multimodal stubs (Phase 2A Pass 1)
|
|
59
|
+
|
|
60
|
+
func testInitiallyMmprojNotLoaded() {
|
|
61
|
+
let bridge = LlamaCppBridge()
|
|
62
|
+
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func testLoadMmprojRequiresMainModel() {
|
|
66
|
+
let bridge = LlamaCppBridge()
|
|
67
|
+
// Main model never loaded -> should fail with code 31.
|
|
68
|
+
XCTAssertThrowsError(try bridge.loadMmproj(atPath: "/tmp/fake-mmproj.gguf"))
|
|
69
|
+
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func testEmptyMmprojPathFails() {
|
|
73
|
+
let bridge = LlamaCppBridge()
|
|
74
|
+
XCTAssertThrowsError(try bridge.loadMmproj(atPath: ""))
|
|
75
|
+
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func testUnloadMmprojIsIdempotent() {
|
|
79
|
+
let bridge = LlamaCppBridge()
|
|
80
|
+
// Repeated unload calls on a never-loaded bridge must not crash.
|
|
81
|
+
bridge.unloadMmproj()
|
|
82
|
+
bridge.unloadMmproj()
|
|
83
|
+
XCTAssertFalse(bridge.isMmprojLoaded)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Multimodal — Phase 2A Pass 2
|
|
87
|
+
|
|
88
|
+
/// Without a loaded model, completeMultimodalPrompt must return an error
|
|
89
|
+
/// (code 50, "Model not loaded"). We can verify this without a real model.
|
|
90
|
+
func testCompleteMultimodalRequiresModelLoaded() {
|
|
91
|
+
let bridge = LlamaCppBridge()
|
|
92
|
+
XCTAssertFalse(bridge.isLoaded)
|
|
93
|
+
XCTAssertThrowsError(
|
|
94
|
+
try bridge.completeMultimodalPrompt(
|
|
95
|
+
"ignored <__media__>",
|
|
96
|
+
media: [Data([0x00, 0x01, 0x02])],
|
|
97
|
+
maxTokens: 8,
|
|
98
|
+
temperature: 0.0,
|
|
99
|
+
topP: 1.0
|
|
100
|
+
)
|
|
101
|
+
) { error in
|
|
102
|
+
let nsErr = error as NSError
|
|
103
|
+
XCTAssertEqual(nsErr.domain, "DVAIBridgeLlama")
|
|
104
|
+
XCTAssertEqual(nsErr.code, 50, "expected code 50 (Model not loaded)")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// applyChatTemplate without a loaded model returns the "Model not loaded"
|
|
109
|
+
/// error (code 40). Same can-run-without-model property as above.
|
|
110
|
+
func testApplyChatTemplateRequiresModelLoaded() {
|
|
111
|
+
let bridge = LlamaCppBridge()
|
|
112
|
+
XCTAssertFalse(bridge.isLoaded)
|
|
113
|
+
XCTAssertThrowsError(
|
|
114
|
+
try bridge.applyChatTemplate(
|
|
115
|
+
nil,
|
|
116
|
+
messages: [["role": "user", "content": "hi"]],
|
|
117
|
+
addAssistant: true
|
|
118
|
+
)
|
|
119
|
+
) { error in
|
|
120
|
+
let nsErr = error as NSError
|
|
121
|
+
XCTAssertEqual(nsErr.domain, "DVAIBridgeLlama")
|
|
122
|
+
XCTAssertEqual(nsErr.code, 40, "expected code 40 (Model not loaded)")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// hasAudioEncoder is always false when no mmproj is loaded.
|
|
127
|
+
func testHasAudioEncoderFalseWithoutMmproj() {
|
|
128
|
+
let bridge = LlamaCppBridge()
|
|
129
|
+
XCTAssertFalse(bridge.hasAudioEncoder())
|
|
130
|
+
}
|
|
131
|
+
}
|