Skip to content

Commit 8fe6463

Browse files
author
Daniel Nata Nugraha
authored
feat(intelligence) Add Swift tests and CI checks (#5041)
1 parent d7febf7 commit 8fe6463

File tree

5 files changed

+221
-5
lines changed

5 files changed

+221
-5
lines changed

.github/workflows/intelligence-docs.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,28 @@ env:
2323

2424
jobs:
2525
build_and_deploy:
26-
runs-on: ubuntu-22.04
26+
runs-on: macos-14
2727
name: Build and deploy
2828
steps:
2929
- uses: actions/checkout@v4
3030
- name: Bootstrap
3131
uses: ./.github/actions/bootstrap
3232
- name: Install pandoc
33-
run: sudo apt install pandoc
33+
run: brew install pandoc
3434
- name: Install Flower dependencies (mandatory only)
3535
run: python -m poetry install --extras "simulation"
36+
- uses: swift-actions/setup-swift@v2
37+
with:
38+
swift-version: "6.0.0"
39+
- uses: maxim-lobanov/setup-xcode@v1
40+
with:
41+
xcode-version: "16.2"
3642
- name: Setup Node.js
3743
uses: actions/setup-node@v3
3844
with:
3945
node-version: '22.14.0'
46+
- name: Install SourceDocs
47+
run: brew install sourcedocs
4048
- name: Install pnpm
4149
run: |
4250
npm install -g pnpm
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Intelligence Swift
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'intelligence/swift/**/*'
9+
- '.github/workflows/intelligence-swift.yml'
10+
pull_request:
11+
branches:
12+
- main
13+
paths:
14+
- 'intelligence/swift/**/*'
15+
- '.github/workflows/intelligence-swift.yml'
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
19+
cancel-in-progress: true
20+
21+
env:
22+
FLWR_TELEMETRY_ENABLED: 0
23+
24+
jobs:
25+
fi_swift_format_lint:
26+
name: Format and Lint Check
27+
runs-on: macos-latest
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v4
31+
32+
- name: Install SwiftFormat
33+
run: brew install swift-format
34+
35+
- name: Check formatting and lint
36+
working-directory: intelligence/swift
37+
run: swift-format lint --recursive .
38+
39+
fi_swift_tests:
40+
name: Tests
41+
runs-on: macos-14
42+
steps:
43+
- name: Checkout code
44+
uses: actions/checkout@v4
45+
46+
- uses: swift-actions/setup-swift@v2
47+
with:
48+
swift-version: "6.0.0"
49+
50+
- uses: maxim-lobanov/setup-xcode@v1
51+
with:
52+
xcode-version: "16.2"
53+
54+
- name: Run tests
55+
run: xcodebuild test -scheme flower -destination 'platform=macOS'

Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ let package = Package(
3838
.product(name: "MLXLLM", package: "mlx-swift-examples"),
3939
.product(name: "Crypto", package: "swift-crypto")
4040
],
41-
path: "intelligence/swift/src"),
41+
path: "intelligence/swift/src"
42+
),
43+
.testTarget(
44+
name: "FlowerIntelligenceTests",
45+
dependencies: ["FlowerIntelligence"],
46+
path: "intelligence/swift/tests"
47+
)
4248
]
4349
)

intelligence/dev/build-docs.sh

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
set -e
33
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../
44

5+
# Build TS docs
56
cd ts && \
67
pnpm build:docs --readme none --name "TypeScript API" && \
78
cd ..
89

9-
cd docs
10+
# Build Swift docs
11+
cd .. && \
12+
sourcedocs generate --module-name FlowerIntelligence -- -scheme flower -destination 'platform=macOS' && \
13+
mkdir -p intelligence/docs/source/swift-api-ref && \
14+
cp -r Documentation/Reference/* intelligence/docs/source/swift-api-ref/ && \
15+
cd intelligence/docs && \
16+
mv source/swift-api-ref/README.md source/swift-api-ref/index.md && \
17+
sed -i.bak '1 s/^# .*/# Swift API/' source/swift-api-ref/index.md && \
18+
sed -i.bak '/^This file was generated by/d' source/swift-api-ref/index.md && \
19+
rm -f source/swift-api-ref/index.md.bak
1020

1121
{
1222
echo ''
@@ -17,6 +27,6 @@ cd docs
1727
echo ''
1828
echo '*/*'
1929
echo '```'
20-
} | tee -a source/ts-api-ref/index.md
30+
} | tee -a source/ts-api-ref/index.md source/swift-api-ref/index.md
2131

2232
make html
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import Testing
2+
@testable import FlowerIntelligence
3+
4+
class MockMlxEngine: Engine {
5+
var lastMessages: [Message]?
6+
var lastModel: String?
7+
var lastTemperature: Float?
8+
var lastMaxCompletionTokens: Int?
9+
var lastStream: Bool?
10+
var lastTools: [Tool]?
11+
12+
func chat(
13+
_ messages: [Message],
14+
model: String?,
15+
temperature: Float?,
16+
maxCompletionTokens: Int?,
17+
stream: Bool,
18+
onStreamEvent: ((StreamEvent) -> Void)?,
19+
tools: [Tool]?
20+
) async throws -> Message {
21+
lastMessages = messages
22+
return Message(role: "assistant", content: "Mock Local Engine Response")
23+
}
24+
}
25+
26+
class MockRemoteEngine: RemoteEngineProtocol {
27+
var apiKey: String = ""
28+
var lastMessages: [Message]?
29+
var lastModel: String?
30+
var lastTemperature: Float?
31+
var lastMaxCompletionTokens: Int?
32+
var lastStream: Bool?
33+
var lastTools: [Tool]?
34+
35+
func chat(
36+
_ messages: [Message],
37+
model: String?,
38+
temperature: Float?,
39+
maxCompletionTokens: Int?,
40+
stream: Bool,
41+
onStreamEvent: ((StreamEvent) -> Void)?,
42+
tools: [Tool]?
43+
) async throws -> Message {
44+
lastMessages = messages
45+
return Message(role: "assistant", content: "Mock Remote Engine Response")
46+
}
47+
}
48+
49+
class FlowerIntelligenceTests {
50+
var flowerIntelligence: FlowerIntelligence
51+
var mockMlxEngine: MockMlxEngine
52+
var mockRemoteEngine: MockRemoteEngine
53+
54+
init() {
55+
mockMlxEngine = MockMlxEngine()
56+
mockRemoteEngine = MockRemoteEngine()
57+
flowerIntelligence = FlowerIntelligence(mlxEngine: mockMlxEngine, remoteEngine: mockRemoteEngine)
58+
}
59+
60+
@Test
61+
func testUsesCorrectMlxEngine() async throws {
62+
flowerIntelligence.remoteHandoff = false
63+
_ = await flowerIntelligence.chat("Hello")
64+
try #require(mockMlxEngine.lastMessages != nil)
65+
try #require(mockRemoteEngine.lastMessages == nil)
66+
}
67+
68+
@Test
69+
func testUsesCorrectRemoteEngine() async throws {
70+
flowerIntelligence.remoteHandoff = true
71+
_ = await flowerIntelligence.chat("Hello")
72+
try #require(mockRemoteEngine.lastMessages != nil)
73+
try #require(mockMlxEngine.lastMessages == nil)
74+
}
75+
76+
@Test
77+
func testForceRemoteOverridesRemoteHandoff() async throws {
78+
flowerIntelligence.remoteHandoff = false
79+
_ = await flowerIntelligence.chat("Hello", maybeOptions: ChatOptions(forceRemote: true))
80+
try #require(mockRemoteEngine.lastMessages != nil)
81+
try #require(mockMlxEngine.lastMessages == nil)
82+
}
83+
84+
@Test
85+
func testForceLocalOverridesRemoteHandoff() async throws {
86+
flowerIntelligence.remoteHandoff = true
87+
_ = await flowerIntelligence.chat("Hello", maybeOptions: ChatOptions(forceLocal: true))
88+
try #require(mockMlxEngine.lastMessages != nil)
89+
try #require(mockRemoteEngine.lastMessages == nil)
90+
}
91+
92+
@Test
93+
func testForceRemoteAndForceLocalReturnFailure() async throws {
94+
let result = await flowerIntelligence.chat("Hello", maybeOptions: ChatOptions(forceRemote: true, forceLocal: true))
95+
switch result {
96+
case .failure(let error):
97+
try #require(error.message == "Cannot set both forceRemote and forceLocal to true")
98+
default:
99+
Issue.record("Expected failure but got success")
100+
}
101+
}
102+
103+
@Test
104+
func testChatFunctionWithSingleMessage() async throws {
105+
let result = await flowerIntelligence.chat("Test message")
106+
switch result {
107+
case .success(let response):
108+
let messages = try #require(mockMlxEngine.lastMessages)
109+
try #require(messages.first?.content == "Test message")
110+
try #require(response.role == "assistant")
111+
try #require(response.content == "Mock Local Engine Response")
112+
default:
113+
Issue.record("Expected success but got failure")
114+
}
115+
}
116+
117+
@Test
118+
func testChatFunctionWithArrayOfMessages() async throws {
119+
let messages = [
120+
Message(role: "system", content: "You are an AI"),
121+
Message(role: "user", content: "What is Swift?")
122+
]
123+
let options = ChatOptions(model: "meta/llama3.2-1b")
124+
let result = await flowerIntelligence.chat(options: (messages, options))
125+
126+
switch result {
127+
case .success(let response):
128+
let messages = try #require(mockMlxEngine.lastMessages)
129+
try #require(messages.count == 2)
130+
try #require(messages.last?.content == "What is Swift?")
131+
try #require(response.role == "assistant")
132+
try #require(response.content == "Mock Local Engine Response")
133+
default:
134+
Issue.record("Expected success but got failure")
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)