@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.
@@ -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
+ }