Implement session cache upgrade phase 1 foundation

This commit is contained in:
2026-03-20 08:35:37 +01:00
parent 41199cb9bc
commit e98e5fd88b
14 changed files with 552 additions and 34 deletions

View File

@@ -6,10 +6,20 @@ Native macOS SwiftUI app for local LLMs on Apple Silicon via MLX. Provides a cha
**Always use `./build.sh` to build the project** — never call `xcodebuild` directly. The script runs xcodegen first (to pick up new/removed files) and uses the correct scheme, destination, and build directory. **Always use `./build.sh` to build the project** — never call `xcodebuild` directly. The script runs xcodegen first (to pick up new/removed files) and uses the correct scheme, destination, and build directory.
**Always use `./test.sh` to run tests** — it regenerates the Xcode project first and runs the shared `MLXServer` test scheme so test runs are reproducible.
Tests are required for finished work when the change is reasonably testable.
Relevant tests must exist and must pass before work is considered complete.
Pre-existing errors don't exist: every error is your responsibility and you have to fix it before claiming you are done.
```bash ```bash
# Build (requires xcodegen: brew install xcodegen) # Build (requires xcodegen: brew install xcodegen)
./build.sh ./build.sh
# Test
./test.sh
# Run # Run
open "build/Debug/MLX Server.app" open "build/Debug/MLX Server.app"
``` ```

View File

@@ -25,12 +25,15 @@
5946258F1DE88CE904584E0B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944C699FBB76C734C9DF2F2E /* ContentView.swift */; }; 5946258F1DE88CE904584E0B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944C699FBB76C734C9DF2F2E /* ContentView.swift */; };
5C1E8FE1C521914CEF98D3AA /* ChatMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */; }; 5C1E8FE1C521914CEF98D3AA /* ChatMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */; };
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0EAB35D7130D56B9E7484BA /* StatusBarView.swift */; }; 621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0EAB35D7130D56B9E7484BA /* StatusBarView.swift */; };
67262C5E24739F1FE0011439 /* StreamingSSEEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */; };
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733A0D1D4AC25DDDA6C8684 /* LocalModelResolver.swift */; }; 6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733A0D1D4AC25DDDA6C8684 /* LocalModelResolver.swift */; };
7CD765C1E2F9F4D7504C8D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B629DA084A9A40E54F8EA5FA /* Assets.xcassets */; }; 7CD765C1E2F9F4D7504C8D09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B629DA084A9A40E54F8EA5FA /* Assets.xcassets */; };
80646C5066BF79BC76E1D9D7 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DFC212AF4359A45FBE22BA /* ModelConfig.swift */; }; 80646C5066BF79BC76E1D9D7 /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DFC212AF4359A45FBE22BA /* ModelConfig.swift */; };
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */; }; 84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */; };
85FB1EB49D76A9F21E181346 /* ChatScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04EE8E6418EC6E9B66999B0 /* ChatScene.swift */; }; 85FB1EB49D76A9F21E181346 /* ChatScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04EE8E6418EC6E9B66999B0 /* ChatScene.swift */; };
945474365D0B3E961811909A /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = D5E8E1C2DD8D8AABB4306193 /* MLXVLM */; }; 945474365D0B3E961811909A /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = D5E8E1C2DD8D8AABB4306193 /* MLXVLM */; };
962083CCCC4AC848E0BBBC99 /* CancellationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */; };
A146BBA70CFBEC505BDCDF0D /* ImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1A89C076E717F87A60397D /* ImageDecoder.swift */; };
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */; }; B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */; };
B1D9BC407DB7DB1489230C20 /* MonitorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4239CFF94B819C35A8D4D617 /* MonitorView.swift */; }; B1D9BC407DB7DB1489230C20 /* MonitorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4239CFF94B819C35A8D4D617 /* MonitorView.swift */; };
B5AA6E3B4BE21676226B342B /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD93859F0291F1A3E09DA5 /* ChatViewModel.swift */; }; B5AA6E3B4BE21676226B342B /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD93859F0291F1A3E09DA5 /* ChatViewModel.swift */; };
@@ -42,12 +45,25 @@
D666A311788375E8A061C832 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4147321383E94E9F17A0154E /* SettingsView.swift */; }; D666A311788375E8A061C832 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4147321383E94E9F17A0154E /* SettingsView.swift */; };
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A52E2C9964ADA9D841A89B /* APIModels.swift */; }; D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A52E2C9964ADA9D841A89B /* APIModels.swift */; };
DF5C525DBD2E3153256951C1 /* SceneManagementWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1592FD260014C4FBDB6995 /* SceneManagementWindow.swift */; }; DF5C525DBD2E3153256951C1 /* SceneManagementWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1592FD260014C4FBDB6995 /* SceneManagementWindow.swift */; };
E199D0BB09B61AC128AB093A /* CancellationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3489501F2F8E1BA382347CFA /* CancellationToken.swift */; };
E92B6656C251EDA246B8F582 /* ImageDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */; };
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */; }; F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */; };
F546CE5955ED253D8A793D5E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A98257123539E9E738213BFA /* MarkdownUI */; }; F546CE5955ED253D8A793D5E /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A98257123539E9E738213BFA /* MarkdownUI */; };
FAF7D4714AC6D02674920208 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B359324B5FD8D106C74338 /* ChatMessage.swift */; }; FAF7D4714AC6D02674920208 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B359324B5FD8D106C74338 /* ChatMessage.swift */; };
FCD48F8C132A2B830A15EEB4 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 3F5A4AC6DBAF7CA686ECA74E /* MLXLLM */; }; FCD48F8C132A2B830A15EEB4 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 3F5A4AC6DBAF7CA686ECA74E /* MLXLLM */; };
FE4405F66873C75CD6FA19A5 /* StreamingSSEEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
9F9E4F692B655CD8CE88479C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 938BC479816FCA8527B731F9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BCD7107EE884C9B2F4C2C40E;
remoteInfo = MLXServer;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0F03A123A8908714A89315FE /* SceneCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneCommands.swift; sourceTree = "<group>"; }; 0F03A123A8908714A89315FE /* SceneCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneCommands.swift; sourceTree = "<group>"; };
145B888FBDD4F931512C5473 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; }; 145B888FBDD4F931512C5473 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
@@ -56,6 +72,7 @@
24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentMigration.swift; sourceTree = "<group>"; }; 24E29065DD29C17D20B0400D /* ChatDocumentMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentMigration.swift; sourceTree = "<group>"; };
2DC8C86D397B1FCA08E07CBD /* DownloadModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModalView.swift; sourceTree = "<group>"; }; 2DC8C86D397B1FCA08E07CBD /* DownloadModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModalView.swift; sourceTree = "<group>"; };
2E2FCA55CEBEBCED78D9479A /* SaveChatCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChatCommands.swift; sourceTree = "<group>"; }; 2E2FCA55CEBEBCED78D9479A /* SaveChatCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChatCommands.swift; sourceTree = "<group>"; };
3489501F2F8E1BA382347CFA /* CancellationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationToken.swift; sourceTree = "<group>"; };
37FEB592E5E717F817B03151 /* SceneManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneManagementView.swift; sourceTree = "<group>"; }; 37FEB592E5E717F817B03151 /* SceneManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneManagementView.swift; sourceTree = "<group>"; };
386CD08DC6338F42460DFBE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 386CD08DC6338F42460DFBE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
38DFC212AF4359A45FBE22BA /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = "<group>"; }; 38DFC212AF4359A45FBE22BA /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = "<group>"; };
@@ -63,8 +80,11 @@
3D08828E16B17EF02C14243E /* APIServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServer.swift; sourceTree = "<group>"; }; 3D08828E16B17EF02C14243E /* APIServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServer.swift; sourceTree = "<group>"; };
4147321383E94E9F17A0154E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 4147321383E94E9F17A0154E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
4239CFF94B819C35A8D4D617 /* MonitorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitorView.swift; sourceTree = "<group>"; }; 4239CFF94B819C35A8D4D617 /* MonitorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitorView.swift; sourceTree = "<group>"; };
49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingSSEEncoderTests.swift; sourceTree = "<group>"; };
615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingSSEEncoder.swift; sourceTree = "<group>"; };
6B3AA91D2C7842D7366F9A41 /* ChatDocumentPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentPackage.swift; sourceTree = "<group>"; }; 6B3AA91D2C7842D7366F9A41 /* ChatDocumentPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDocumentPackage.swift; sourceTree = "<group>"; };
6EE59189918D06B8D2F588FC /* MLXServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLXServer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6EE59189918D06B8D2F588FC /* MLXServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLXServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
7C1A89C076E717F87A60397D /* ImageDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDecoder.swift; sourceTree = "<group>"; };
922CBDC9206737BD04AF2874 /* ModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.swift; sourceTree = "<group>"; }; 922CBDC9206737BD04AF2874 /* ModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.swift; sourceTree = "<group>"; };
944C699FBB76C734C9DF2F2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 944C699FBB76C734C9DF2F2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A4B359324B5FD8D106C74338 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; }; A4B359324B5FD8D106C74338 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
@@ -82,10 +102,13 @@
D7C9BAD674E29688ACE53B0B /* ChatExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatExporter.swift; sourceTree = "<group>"; }; D7C9BAD674E29688ACE53B0B /* ChatExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatExporter.swift; sourceTree = "<group>"; };
DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesView.swift; sourceTree = "<group>"; }; DB1A5E8B1C9F2BC4D262C53A /* ChatMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesView.swift; sourceTree = "<group>"; };
E35452B166893B25E765FF70 /* InferenceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceStats.swift; sourceTree = "<group>"; }; E35452B166893B25E765FF70 /* InferenceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceStats.swift; sourceTree = "<group>"; };
E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDecoderTests.swift; sourceTree = "<group>"; };
E5E6AD02CDF23BDAB64700A7 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = "<group>"; }; E5E6AD02CDF23BDAB64700A7 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = "<group>"; };
E73B165A1822729C907791AE /* ToolCallParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolCallParser.swift; sourceTree = "<group>"; }; E73B165A1822729C907791AE /* ToolCallParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolCallParser.swift; sourceTree = "<group>"; };
EF518FEBF3A38E830E3CE1A5 /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; }; EF518FEBF3A38E830E3CE1A5 /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
F1A52E2C9964ADA9D841A89B /* APIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIModels.swift; sourceTree = "<group>"; }; F1A52E2C9964ADA9D841A89B /* APIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIModels.swift; sourceTree = "<group>"; };
F4CE2D594F7433C76169151A /* MLXServerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MLXServerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationTokenTests.swift; sourceTree = "<group>"; };
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSessionCache.swift; sourceTree = "<group>"; }; FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSessionCache.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -104,6 +127,14 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
03BB61C0F16FAD47436AA178 /* MLXServerTests */ = {
isa = PBXGroup;
children = (
154AF0C071A7DC02EB5F6F49 /* Server */,
);
path = MLXServerTests;
sourceTree = "<group>";
};
05B1BAE308E64D2FB2E73823 /* Utilities */ = { 05B1BAE308E64D2FB2E73823 /* Utilities */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -126,10 +157,21 @@
path = Documents; path = Documents;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
154AF0C071A7DC02EB5F6F49 /* Server */ = {
isa = PBXGroup;
children = (
FEFF6168B2283FEC87B4BB8C /* CancellationTokenTests.swift */,
E4573DC9314915F4C7963B4E /* ImageDecoderTests.swift */,
49C383DD5224F3420EB98DB2 /* StreamingSSEEncoderTests.swift */,
);
path = Server;
sourceTree = "<group>";
};
652987C2A419DBFC79E32CDE /* Products */ = { 652987C2A419DBFC79E32CDE /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6EE59189918D06B8D2F588FC /* MLXServer.app */, 6EE59189918D06B8D2F588FC /* MLXServer.app */,
F4CE2D594F7433C76169151A /* MLXServerTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -205,7 +247,10 @@
children = ( children = (
F1A52E2C9964ADA9D841A89B /* APIModels.swift */, F1A52E2C9964ADA9D841A89B /* APIModels.swift */,
3D08828E16B17EF02C14243E /* APIServer.swift */, 3D08828E16B17EF02C14243E /* APIServer.swift */,
3489501F2F8E1BA382347CFA /* CancellationToken.swift */,
FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */, FFBB16D3AF2E61D001FD6051 /* ConversationSessionCache.swift */,
7C1A89C076E717F87A60397D /* ImageDecoder.swift */,
615F8A7C9ABCADEB215D31BD /* StreamingSSEEncoder.swift */,
E73B165A1822729C907791AE /* ToolCallParser.swift */, E73B165A1822729C907791AE /* ToolCallParser.swift */,
16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */, 16AE82A64D1D07AE3CD8D33A /* ToolPromptBuilder.swift */,
); );
@@ -216,6 +261,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6816BF8EF7C92384DD7C9177 /* MLXServer */, 6816BF8EF7C92384DD7C9177 /* MLXServer */,
03BB61C0F16FAD47436AA178 /* MLXServerTests */,
652987C2A419DBFC79E32CDE /* Products */, 652987C2A419DBFC79E32CDE /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@@ -246,6 +292,24 @@
productReference = 6EE59189918D06B8D2F588FC /* MLXServer.app */; productReference = 6EE59189918D06B8D2F588FC /* MLXServer.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
CE11F8C258BB944F38A5840D /* MLXServerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = A2168D037766ED36A199C6F7 /* Build configuration list for PBXNativeTarget "MLXServerTests" */;
buildPhases = (
6DEBF8BBA4F6DB333E0C55B0 /* Sources */,
);
buildRules = (
);
dependencies = (
8870DD8F1917C831FD4FD595 /* PBXTargetDependency */,
);
name = MLXServerTests;
packageProductDependencies = (
);
productName = MLXServerTests;
productReference = F4CE2D594F7433C76169151A /* MLXServerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -276,6 +340,7 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
BCD7107EE884C9B2F4C2C40E /* MLXServer */, BCD7107EE884C9B2F4C2C40E /* MLXServer */,
CE11F8C258BB944F38A5840D /* MLXServerTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -292,12 +357,23 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
6DEBF8BBA4F6DB333E0C55B0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
962083CCCC4AC848E0BBBC99 /* CancellationTokenTests.swift in Sources */,
E92B6656C251EDA246B8F582 /* ImageDecoderTests.swift in Sources */,
FE4405F66873C75CD6FA19A5 /* StreamingSSEEncoderTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
BC03844286F51DFAEF96B823 /* Sources */ = { BC03844286F51DFAEF96B823 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */, D96DDE66F76FDDA642629E17 /* APIModels.swift in Sources */,
50DD129CCF2843482DEC3B96 /* APIServer.swift in Sources */, 50DD129CCF2843482DEC3B96 /* APIServer.swift in Sources */,
E199D0BB09B61AC128AB093A /* CancellationToken.swift in Sources */,
2E3A02DF9C6A5109E532D5E2 /* ChatDocumentController.swift in Sources */, 2E3A02DF9C6A5109E532D5E2 /* ChatDocumentController.swift in Sources */,
1A8833E3CCD3289C95E282A2 /* ChatDocumentManifest.swift in Sources */, 1A8833E3CCD3289C95E282A2 /* ChatDocumentManifest.swift in Sources */,
B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */, B13FFE238613BFBFC72E0CC8 /* ChatDocumentMigration.swift in Sources */,
@@ -312,6 +388,7 @@
F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */, F141B91A64F7DAD73CE2910A /* ConversationSessionCache.swift in Sources */,
C07A377244DCD67F4FE709FE /* DownloadModalView.swift in Sources */, C07A377244DCD67F4FE709FE /* DownloadModalView.swift in Sources */,
4DC033E45880B2948B47DEB1 /* FocusedValues.swift in Sources */, 4DC033E45880B2948B47DEB1 /* FocusedValues.swift in Sources */,
A146BBA70CFBEC505BDCDF0D /* ImageDecoder.swift in Sources */,
2D08769282BD71C170DB0943 /* InferenceStats.swift in Sources */, 2D08769282BD71C170DB0943 /* InferenceStats.swift in Sources */,
6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */, 6828CCA8B78AB40906F87CAB /* LocalModelResolver.swift in Sources */,
50B6861FF8610B3ED4FFAD9D /* MLXServerApp.swift in Sources */, 50B6861FF8610B3ED4FFAD9D /* MLXServerApp.swift in Sources */,
@@ -328,6 +405,7 @@
CFEE79815DFB80E51FE3745A /* SceneStore.swift in Sources */, CFEE79815DFB80E51FE3745A /* SceneStore.swift in Sources */,
D666A311788375E8A061C832 /* SettingsView.swift in Sources */, D666A311788375E8A061C832 /* SettingsView.swift in Sources */,
621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */, 621B7E4382199AC1378F5F9C /* StatusBarView.swift in Sources */,
67262C5E24739F1FE0011439 /* StreamingSSEEncoder.swift in Sources */,
189362AAE2CDE5D4B3428334 /* ToolCallParser.swift in Sources */, 189362AAE2CDE5D4B3428334 /* ToolCallParser.swift in Sources */,
84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */, 84D32315B418B5243E017350 /* ToolPromptBuilder.swift in Sources */,
); );
@@ -335,7 +413,49 @@
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
8870DD8F1917C831FD4FD595 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BCD7107EE884C9B2F4C2C40E /* MLXServer */;
targetProxy = 9F9E4F692B655CD8CE88479C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
18921C5B777D8B7FEF662D6F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mlxserver.MLXServerTests;
SDKROOT = macosx;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MLX Server.app/Contents/MacOS/MLX Server";
};
name = Release;
};
2B83417701A93BF554428C56 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mlxserver.MLXServerTests;
SDKROOT = macosx;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MLX Server.app/Contents/MacOS/MLX Server";
};
name = Debug;
};
6C0C08FC4653A138A768ECF0 /* Release */ = { 6C0C08FC4653A138A768ECF0 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -524,6 +644,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug; defaultConfigurationName = Debug;
}; };
A2168D037766ED36A199C6F7 /* Build configuration list for PBXNativeTarget "MLXServerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2B83417701A93BF554428C56 /* Debug */,
18921C5B777D8B7FEF662D6F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
BuildableName = "MLXServer.app"
BlueprintName = "MLXServer"
ReferencedContainer = "container:MLXServer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CE11F8C258BB944F38A5840D"
BuildableName = "MLXServerTests.xctest"
BlueprintName = "MLXServerTests"
ReferencedContainer = "container:MLXServer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
BuildableName = "MLXServer.app"
BlueprintName = "MLXServer"
ReferencedContainer = "container:MLXServer.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CE11F8C258BB944F38A5840D"
BuildableName = "MLXServerTests.xctest"
BlueprintName = "MLXServerTests"
ReferencedContainer = "container:MLXServer.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
BuildableName = "MLXServer.app"
BlueprintName = "MLXServer"
ReferencedContainer = "container:MLXServer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BCD7107EE884C9B2F4C2C40E"
BuildableName = "MLXServer.app"
BlueprintName = "MLXServer"
ReferencedContainer = "container:MLXServer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -15,6 +15,10 @@ struct ContentView: View {
@State private var documentErrorMessage: String? @State private var documentErrorMessage: String?
@State private var exportErrorMessage: String? @State private var exportErrorMessage: String?
private var isRunningTests: Bool {
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}
var body: some View { var body: some View {
exportedContent exportedContent
} }
@@ -30,11 +34,11 @@ struct ContentView: View {
delegate.chatViewModel = vm delegate.chatViewModel = vm
} }
// Auto-start API server if configured // Auto-start API server if configured
if Preferences.apiAutoStart { if Preferences.apiAutoStart && !isRunningTests {
vm.startAPIServer() vm.startAPIServer()
} }
// Restore autosaved session if no document is being opened // Restore autosaved session if no document is being opened
if !documentController.hasPendingOpenRequests { if !documentController.hasPendingOpenRequests && !isRunningTests {
Task { Task {
await vm.restoreFromAutosave() await vm.restoreFromAutosave()
} }

View File

@@ -1,4 +1,3 @@
import AppKit
import Foundation import Foundation
import MLXLMCommon import MLXLMCommon
import Network import Network
@@ -288,7 +287,7 @@ final class APIServer {
var messageImages: [UserInput.Image] = [] var messageImages: [UserInput.Image] = []
var messageImageBytes = 0 var messageImageBytes = 0
for urlString in imageURLs { for urlString in imageURLs {
if let decoded = decodeBase64Image(urlString) { if let decoded = ImageDecoder.decode(urlString) {
messageImages.append(decoded.image) messageImages.append(decoded.image)
messageImageBytes += decoded.estimatedBytes messageImageBytes += decoded.estimatedBytes
} }
@@ -448,28 +447,6 @@ final class APIServer {
modelManager.touchActivity() modelManager.touchActivity()
} }
/// Decode a base64 data URI (data:image/png;base64,...) into a UserInput.Image.
private func decodeBase64Image(_ urlString: String) -> DecodedImage? {
// Handle data URIs: data:image/png;base64,<data>
let base64String: String
if urlString.hasPrefix("data:") {
guard let commaIndex = urlString.firstIndex(of: ",") else { return nil }
base64String = String(urlString[urlString.index(after: commaIndex)...])
} else {
// Could be a plain base64 string
base64String = urlString
}
guard let data = Data(base64Encoded: base64String),
let nsImage = NSImage(data: data),
let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return nil
}
let estimatedBytes = max(data.count, cgImage.width * cgImage.height * 4)
return DecodedImage(image: .ciImage(CIImage(cgImage: cgImage)), estimatedBytes: estimatedBytes)
}
// MARK: - Non-streaming response // MARK: - Non-streaming response
private func handleNonStreamingResponse( private func handleNonStreamingResponse(
@@ -887,11 +864,6 @@ final class APIServer {
} }
} }
private struct DecodedImage {
let image: UserInput.Image
let estimatedBytes: Int
}
// MARK: - HTTP request parser // MARK: - HTTP request parser
private struct HTTPRequest { private struct HTTPRequest {

View File

@@ -0,0 +1,14 @@
import os
/// Thread-safe cancellation flag for cooperative stream shutdown.
final class CancellationToken: @unchecked Sendable {
private let lock = OSAllocatedUnfairLock(initialState: false)
var isCancelled: Bool {
lock.withLock { $0 }
}
func cancel() {
lock.withLock { $0 = true }
}
}

View File

@@ -0,0 +1,31 @@
import AppKit
import CoreImage
import Foundation
import MLXLMCommon
/// Extracted from APIServer decodes data URIs to UserInput.Image.
enum ImageDecoder {
struct DecodedImage {
let image: UserInput.Image
let estimatedBytes: Int
}
static func decode(_ urlString: String) -> DecodedImage? {
let base64String: String
if urlString.hasPrefix("data:") {
guard let commaIndex = urlString.firstIndex(of: ",") else { return nil }
base64String = String(urlString[urlString.index(after: commaIndex)...])
} else {
base64String = urlString
}
guard let data = Data(base64Encoded: base64String),
let nsImage = NSImage(data: data),
let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return nil
}
let estimatedBytes = max(data.count, cgImage.width * cgImage.height * 4)
return DecodedImage(image: .ciImage(CIImage(cgImage: cgImage)), estimatedBytes: estimatedBytes)
}
}

View File

@@ -0,0 +1,72 @@
import Foundation
/// Pre-computes static JSON parts for SSE streaming.
/// Only the dynamic delta payload is serialized per token.
struct StreamingSSEEncoder: Sendable {
private let requestId: String
private let created: Int
private let modelName: String
init(requestId: String, created: Int, modelName: String) {
self.requestId = requestId
self.created = created
self.modelName = modelName
}
func encodeContentDelta(_ text: String) -> Data {
Self.encodeChunk(
APIChatCompletionChunk(
id: requestId,
object: "chat.completion.chunk",
created: created,
model: modelName,
choices: [
APIStreamChoice(
index: 0,
delta: APIDeltaMessage(role: nil, content: text, tool_calls: nil),
finish_reason: nil
)
],
usage: nil
)
)
}
func encodeRoleDelta(_ role: String) -> Data {
Self.encodeChunk(
APIChatCompletionChunk(
id: requestId,
object: "chat.completion.chunk",
created: created,
model: modelName,
choices: [
APIStreamChoice(
index: 0,
delta: APIDeltaMessage(role: role, content: nil, tool_calls: nil),
finish_reason: nil
)
],
usage: nil
)
)
}
static func encodeFinalChunk(_ chunk: APIChatCompletionChunk) -> Data {
encodeChunk(chunk)
}
private static func encodeChunk(_ chunk: APIChatCompletionChunk) -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
guard let json = try? encoder.encode(chunk) else {
return Data("data: {}\n\n".utf8)
}
var data = Data(capacity: json.count + 8)
data.append(Data("data: ".utf8))
data.append(json)
data.append(Data("\n\n".utf8))
return data
}
}

View File

@@ -0,0 +1,18 @@
import XCTest
@testable import MLX_Server
final class CancellationTokenTests: XCTestCase {
func testStartsNotCancelled() {
let token = CancellationToken()
XCTAssertFalse(token.isCancelled)
}
func testCancelSetsFlag() {
let token = CancellationToken()
token.cancel()
XCTAssertTrue(token.isCancelled)
}
}

View File

@@ -0,0 +1,20 @@
import XCTest
@testable import MLX_Server
final class ImageDecoderTests: XCTestCase {
private let onePixelPNGBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lRyXWQAAAABJRU5ErkJggg=="
func testDecodeDataURI() {
let image = ImageDecoder.decode("data:image/png;base64,\(onePixelPNGBase64)")
XCTAssertNotNil(image)
XCTAssertGreaterThanOrEqual(image?.estimatedBytes ?? 0, 4)
}
func testDecodePlainBase64() {
let image = ImageDecoder.decode(onePixelPNGBase64)
XCTAssertNotNil(image)
XCTAssertGreaterThanOrEqual(image?.estimatedBytes ?? 0, 4)
}
}

View File

@@ -0,0 +1,82 @@
import XCTest
@testable import MLX_Server
final class StreamingSSEEncoderTests: XCTestCase {
func testEncodeContentDeltaMatchesJSONEncoderOutput() throws {
let encoder = StreamingSSEEncoder(requestId: "chatcmpl-test", created: 1_234_567, modelName: "qwen\"model")
let text = "line 1\nline 2\t\"quoted\"\\slash"
let actual = encoder.encodeContentDelta(text)
let expected = try baselineData(
for: APIChatCompletionChunk(
id: "chatcmpl-test",
object: "chat.completion.chunk",
created: 1_234_567,
model: "qwen\"model",
choices: [
APIStreamChoice(
index: 0,
delta: APIDeltaMessage(role: nil, content: text, tool_calls: nil),
finish_reason: nil
)
],
usage: nil
)
)
XCTAssertEqual(actual, expected)
}
func testEncodeRoleDeltaMatchesJSONEncoderOutput() throws {
let encoder = StreamingSSEEncoder(requestId: "chatcmpl-role", created: 99, modelName: "gemma")
let actual = encoder.encodeRoleDelta("assistant")
let expected = try baselineData(
for: APIChatCompletionChunk(
id: "chatcmpl-role",
object: "chat.completion.chunk",
created: 99,
model: "gemma",
choices: [
APIStreamChoice(
index: 0,
delta: APIDeltaMessage(role: "assistant", content: nil, tool_calls: nil),
finish_reason: nil
)
],
usage: nil
)
)
XCTAssertEqual(actual, expected)
}
func testEncodeFinalChunkMatchesBaseline() throws {
let chunk = APIChatCompletionChunk(
id: "chatcmpl-final",
object: "chat.completion.chunk",
created: 7,
model: "gemma",
choices: [
APIStreamChoice(
index: 0,
delta: APIDeltaMessage(role: nil, content: nil, tool_calls: nil),
finish_reason: "stop"
)
],
usage: APIUsageInfo(prompt_tokens: 10, completion_tokens: 3, total_tokens: 13)
)
XCTAssertEqual(StreamingSSEEncoder.encodeFinalChunk(chunk), try baselineData(for: chunk))
}
private func baselineData(for chunk: APIChatCompletionChunk) throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let json = try encoder.encode(chunk)
var data = Data("data: ".utf8)
data.append(json)
data.append(Data("\n\n".utf8))
return data
}
}

View File

@@ -2558,9 +2558,9 @@ Each step should be independently buildable and testable.
### Phase 1: Foundation (no behavior change yet) ### Phase 1: Foundation (no behavior change yet)
1. **`CancellationToken.swift`** — Standalone utility, no dependencies. Write + unit test. 1. [x] **`CancellationToken.swift`** — Standalone utility, no dependencies. Write + unit test.
2. **`ImageDecoder.swift`** — Extract from APIServer. Mechanical move. 2. [x] **`ImageDecoder.swift`** — Extract from APIServer. Mechanical move.
3. **`StreamingSSEEncoder.swift`** — Standalone, testable in isolation. Verify JSON output matches current `JSONEncoder` output. 3. [x] **`StreamingSSEEncoder.swift`** — Standalone, testable in isolation. Verify JSON output matches current `JSONEncoder` output.
### Phase 2: Core Engine ### Phase 2: Core Engine

View File

@@ -42,3 +42,25 @@ targets:
product: MLXLMCommon product: MLXLMCommon
- package: MarkdownUI - package: MarkdownUI
product: MarkdownUI product: MarkdownUI
MLXServerTests:
type: bundle.unit-test
platform: macOS
sources:
- MLXServerTests
settings:
base:
GENERATE_INFOPLIST_FILE: "YES"
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/MLX Server.app/Contents/MacOS/MLX Server"
BUNDLE_LOADER: "$(TEST_HOST)"
dependencies:
- target: MLXServer
schemes:
MLXServer:
build:
targets:
MLXServer: all
MLXServerTests: [test]
test:
targets:
- MLXServerTests

28
test.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$PROJECT_DIR/build"
CONFIG="${1:-Debug}"
APP_NAME="MLX Server"
DESTINATION="${TEST_DESTINATION:-platform=macOS,arch=arm64}"
echo "==> Testing $APP_NAME ($CONFIG)"
# Regenerate Xcode project from project.yml (picks up any new/removed files)
if command -v xcodegen &>/dev/null; then
xcodegen generate --spec "$PROJECT_DIR/project.yml" --project "$PROJECT_DIR" 2>&1 | grep -v '^$'
fi
# Run tests — filter to test progress, app warnings, build failures, and final result
xcodebuild \
-project "$PROJECT_DIR/MLXServer.xcodeproj" \
-scheme MLXServer \
-destination "$DESTINATION" \
-configuration "$CONFIG" \
SYMROOT="$BUILD_DIR" \
test 2>&1 | \
grep -E "(Test Suite|Test Case|Executed [0-9]+ tests|Testing started|Testing failed|Testing passed|error:|warning:.*MLXServer/|\*\* TEST|BUILD )"
echo ""
echo "==> Tests passed"