@dvai-bridge/ios-shared-core 4.0.0

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.
@@ -0,0 +1,163 @@
1
+ import XCTest
2
+ @testable import DVAISharedCore
3
+
4
+ /// v3.2.x — drive a live HTTP request through the Hummingbird-backed
5
+ /// `HttpServer` end-to-end. Counterpart to the (Telegraph-era)
6
+ /// `HttpServerTest` lifecycle suite, which only exercises bind / stop
7
+ /// + port-fallback. This suite proves the request → dispatchRoute →
8
+ /// response → byte-on-the-wire path actually works for both buffered
9
+ /// (JSON) and streaming (SSE) handlers.
10
+ @available(iOS 17.0, macOS 14.0, *)
11
+ final class HttpServerIntegrationTest: XCTestCase {
12
+
13
+ // MARK: - Test handlers
14
+
15
+ /// Replies to chat/completions with an SSE stream of three chunks
16
+ /// + [DONE]. Used to verify that streaming responses flush
17
+ /// incrementally — we observe the bytes arriving via
18
+ /// `URLSession.bytes(for:)` and assert each chunk lands separately.
19
+ final class StreamingHandlers: DVAIHandlers, @unchecked Sendable {
20
+ let chunkInterval: TimeInterval
21
+
22
+ init(chunkInterval: TimeInterval = 0.05) {
23
+ self.chunkInterval = chunkInterval
24
+ }
25
+
26
+ func handleChatCompletion(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse {
27
+ let interval = chunkInterval
28
+ let (stream, cont) = AsyncStream<String>.makeStream()
29
+ Task {
30
+ for chunk in ["data: chunk1\n\n", "data: chunk2\n\n", "data: chunk3\n\n", "data: [DONE]\n\n"] {
31
+ cont.yield(chunk)
32
+ try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
33
+ }
34
+ cont.finish()
35
+ }
36
+ return .sse(stream)
37
+ }
38
+ func handleCompletion(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse { .json(200, [:]) }
39
+ func handleEmbeddings(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse { .json(200, [:]) }
40
+ func handleModels(ctx: HandlerContext) async throws -> HandlerResponse {
41
+ .json(200, ["object": "list", "data": [["id": ctx.modelId]]])
42
+ }
43
+ }
44
+
45
+ // MARK: - Helpers
46
+
47
+ /// Bring up an HttpServer on a free port, hand back the bound URL.
48
+ /// Caller must `stop()` when done.
49
+ private func startServer(
50
+ handlers: DVAIHandlers = StreamingHandlers(),
51
+ cors: CORSConfig = .wildcard
52
+ ) async throws -> (server: HttpServer, baseUrl: URL) {
53
+ let server = HttpServer()
54
+ let ctx = HandlerContext(modelId: "integration-test-model", backendName: "test")
55
+ await server.installRoutes(handlers: handlers, ctx: ctx, corsConfig: cors)
56
+ // Use a port range well above other tests' bands to avoid
57
+ // collisions when the suite runs concurrently.
58
+ let port = try await server.tryBind(basePort: 39200, maxAttempts: 32, host: "127.0.0.1")
59
+ let url = URL(string: "http://127.0.0.1:\(port)")!
60
+ return (server, url)
61
+ }
62
+
63
+ // MARK: - Buffered (JSON) path
64
+
65
+ func testGetModelsReturnsJsonOverTheWire() async throws {
66
+ let (server, baseUrl) = try await startServer()
67
+ defer { Task { await server.stop() } }
68
+
69
+ let url = baseUrl.appendingPathComponent("v1/models")
70
+ let (data, response) = try await URLSession.shared.data(from: url)
71
+ let http = response as! HTTPURLResponse
72
+
73
+ XCTAssertEqual(http.statusCode, 200)
74
+ XCTAssertEqual(http.value(forHTTPHeaderField: "Content-Type"), "application/json")
75
+ XCTAssertEqual(http.value(forHTTPHeaderField: "Access-Control-Allow-Origin"), "*")
76
+
77
+ let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
78
+ XCTAssertEqual(json?["object"] as? String, "list")
79
+ let array = json?["data"] as? [[String: Any]]
80
+ XCTAssertEqual(array?.first?["id"] as? String, "integration-test-model")
81
+ }
82
+
83
+ func testCorsPreflightReturns204() async throws {
84
+ let (server, baseUrl) = try await startServer()
85
+ defer { Task { await server.stop() } }
86
+
87
+ var req = URLRequest(url: baseUrl.appendingPathComponent("v1/chat/completions"))
88
+ req.httpMethod = "OPTIONS"
89
+ req.setValue("https://app.example.com", forHTTPHeaderField: "Origin")
90
+
91
+ let (_, response) = try await URLSession.shared.data(for: req)
92
+ let http = response as! HTTPURLResponse
93
+ XCTAssertEqual(http.statusCode, 204)
94
+ XCTAssertEqual(http.value(forHTTPHeaderField: "Access-Control-Allow-Private-Network"), "true")
95
+ XCTAssertNotNil(http.value(forHTTPHeaderField: "Access-Control-Allow-Methods"))
96
+ }
97
+
98
+ func testUnknownPathReturns404WithCors() async throws {
99
+ let (server, baseUrl) = try await startServer()
100
+ defer { Task { await server.stop() } }
101
+
102
+ let (_, response) = try await URLSession.shared.data(from: baseUrl.appendingPathComponent("nope"))
103
+ let http = response as! HTTPURLResponse
104
+ XCTAssertEqual(http.statusCode, 404)
105
+ XCTAssertEqual(http.value(forHTTPHeaderField: "Access-Control-Allow-Origin"), "*")
106
+ }
107
+
108
+ // MARK: - Streaming (SSE) path
109
+
110
+ func testSseStreamFlushesChunksIncrementally() async throws {
111
+ // 50ms between chunks — small enough to keep the test fast,
112
+ // large enough that "all chunks at once" would be detectable.
113
+ let (server, baseUrl) = try await startServer(handlers: StreamingHandlers(chunkInterval: 0.05))
114
+ defer { Task { await server.stop() } }
115
+
116
+ var req = URLRequest(url: baseUrl.appendingPathComponent("v1/chat/completions"))
117
+ req.httpMethod = "POST"
118
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
119
+ req.httpBody = "{}".data(using: .utf8)
120
+
121
+ // bytes(for:) gives us an AsyncSequence<UInt8> we can consume
122
+ // chunk-by-chunk to observe streaming behavior. A non-streaming
123
+ // proxy would deliver everything in one TCP read; Hummingbird's
124
+ // ResponseBody writer should flush each `data: …\n\n` chunk
125
+ // separately.
126
+ let (stream, response) = try await URLSession.shared.bytes(for: req)
127
+ let http = response as! HTTPURLResponse
128
+ XCTAssertEqual(http.statusCode, 200)
129
+ XCTAssertEqual(http.value(forHTTPHeaderField: "Content-Type"), "text/event-stream")
130
+ XCTAssertEqual(http.value(forHTTPHeaderField: "Cache-Control"), "no-cache")
131
+
132
+ // Collect all body bytes into a buffer + record per-chunk
133
+ // arrival timestamps via newline boundaries. We count distinct
134
+ // ChunkArrivalEvents to verify streaming.
135
+ var collected = Data()
136
+ var firstByteAt: Date? = nil
137
+ var doneAt: Date? = nil
138
+ for try await byte in stream {
139
+ collected.append(byte)
140
+ if firstByteAt == nil { firstByteAt = Date() }
141
+ if collected.range(of: "[DONE]".data(using: .utf8)!) != nil {
142
+ doneAt = Date()
143
+ // Read a couple more bytes (trailing \n\n) then exit.
144
+ continue
145
+ }
146
+ }
147
+ let asString = String(data: collected, encoding: .utf8) ?? ""
148
+ XCTAssertTrue(asString.contains("chunk1"))
149
+ XCTAssertTrue(asString.contains("chunk2"))
150
+ XCTAssertTrue(asString.contains("chunk3"))
151
+ XCTAssertTrue(asString.contains("[DONE]"))
152
+
153
+ // Smoke-check: total elapsed should be ≥ 3 × chunkInterval
154
+ // (since 4 chunks emit at 50ms apart). If the server were
155
+ // buffering the whole body server-side, time-to-first-byte
156
+ // would equal time-to-last-byte rather than streaming through.
157
+ if let first = firstByteAt, let done = doneAt {
158
+ let elapsed = done.timeIntervalSince(first)
159
+ XCTAssertGreaterThan(elapsed, 0.05,
160
+ "Streaming should take ≥ 1 chunkInterval; got \(elapsed)s")
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,94 @@
1
+ import XCTest
2
+ @testable import DVAISharedCore
3
+
4
+ /// Stub handlers — the bind-lifecycle tests only need the HttpServer
5
+ /// to accept SOMETHING for `installRoutes` so it knows which routes
6
+ /// to register. They never actually send requests through.
7
+ private final class StubHandlers: DVAIHandlers, @unchecked Sendable {
8
+ func handleChatCompletion(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse {
9
+ .json(200, [:])
10
+ }
11
+ func handleCompletion(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse {
12
+ .json(200, [:])
13
+ }
14
+ func handleEmbeddings(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse {
15
+ .json(200, [:])
16
+ }
17
+ func handleModels(ctx: HandlerContext) async throws -> HandlerResponse {
18
+ .json(200, [:])
19
+ }
20
+ }
21
+
22
+ /// Helper to install the stub triple before `tryBind` in tests.
23
+ private func makeReadyServer() async -> HttpServer {
24
+ let server = HttpServer()
25
+ let ctx = HandlerContext(modelId: "test", backendName: "test")
26
+ await server.installRoutes(handlers: StubHandlers(), ctx: ctx, corsConfig: .wildcard)
27
+ return server
28
+ }
29
+
30
+ final class HttpServerTest: XCTestCase {
31
+ func testTryBindBindsBasePort() async throws {
32
+ let server = await makeReadyServer()
33
+ let port = try await server.tryBind(basePort: 39001, maxAttempts: 4, host: "127.0.0.1")
34
+ XCTAssertEqual(port, 39001)
35
+ await server.stop()
36
+ }
37
+
38
+ func testTryBindFallsBackOnPortInUse() async throws {
39
+ // Block port 39010 with another server.
40
+ let blocker = await makeReadyServer()
41
+ _ = try await blocker.tryBind(basePort: 39010, maxAttempts: 1, host: "127.0.0.1")
42
+
43
+ let server = await makeReadyServer()
44
+ let port = try await server.tryBind(basePort: 39010, maxAttempts: 4, host: "127.0.0.1")
45
+ XCTAssertEqual(port, 39011)
46
+ await server.stop()
47
+ await blocker.stop()
48
+ }
49
+
50
+ func testStopIsIdempotent() async throws {
51
+ let server = await makeReadyServer()
52
+ await server.stop() // before start — should not throw
53
+ _ = try await server.tryBind(basePort: 39020, maxAttempts: 1, host: "127.0.0.1")
54
+ await server.stop()
55
+ await server.stop() // double-stop should not throw
56
+ }
57
+
58
+ func testThrowsActionableErrorWhenAllPortsBlocked() async throws {
59
+ // Block 39030..39032.
60
+ var blockers: [HttpServer] = []
61
+ for i in 0..<3 {
62
+ let s = await makeReadyServer()
63
+ _ = try await s.tryBind(basePort: 39030 + i, maxAttempts: 1, host: "127.0.0.1")
64
+ blockers.append(s)
65
+ }
66
+ defer {
67
+ Task { for b in blockers { await b.stop() } }
68
+ }
69
+
70
+ let server = await makeReadyServer()
71
+ do {
72
+ _ = try await server.tryBind(basePort: 39030, maxAttempts: 3, host: "127.0.0.1")
73
+ XCTFail("should have thrown")
74
+ } catch let error as NSError {
75
+ XCTAssertTrue(error.localizedDescription.contains("39030..39032"),
76
+ "Error should name the tried range; got: \(error.localizedDescription)")
77
+ }
78
+ }
79
+
80
+ func testTryBindWithoutInstallRoutesThrows() async throws {
81
+ // Calling tryBind without installRoutes should fail-fast with
82
+ // an actionable error.
83
+ let server = HttpServer()
84
+ do {
85
+ _ = try await server.tryBind(basePort: 39040, maxAttempts: 1, host: "127.0.0.1")
86
+ XCTFail("should have thrown")
87
+ } catch let error as NSError {
88
+ XCTAssertTrue(
89
+ error.localizedDescription.lowercased().contains("installroutes"),
90
+ "Expected error to mention installRoutes; got: \(error.localizedDescription)"
91
+ )
92
+ }
93
+ }
94
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@dvai-bridge/ios-shared-core",
3
+ "version": "4.0.0",
4
+ "description": "DVAI-Bridge iOS shared HTTP-server + handler-dispatch types used by all backend cores. Extracted from ios-llama-core so non-llama backends don't transitively inherit llama.xcframework.",
5
+ "author": "Deep Chakraborty <https://github.com/dk013>",
6
+ "license": "Custom (See LICENSE)",
7
+ "main": "Package.swift",
8
+ "files": [
9
+ "Package.swift",
10
+ "ios",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "publishConfig": {
15
+ "registry": "https://registry.npmjs.org/",
16
+ "access": "public"
17
+ }
18
+ }