Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f9631077b | |||
| 4acf538d8f | |||
| 979747c1ea |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -119,3 +119,4 @@ GIT_SYNC_PHASE1_COMPLETE.md
|
|||||||
build-dmg.sh
|
build-dmg.sh
|
||||||
.claude/
|
.claude/
|
||||||
*.sh
|
*.sh
|
||||||
|
RELEASE_NOTES.md
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -10,7 +10,6 @@ A powerful native macOS AI chat application with support for multiple providers,
|
|||||||
- **OpenAI** - GPT models with native API support
|
- **OpenAI** - GPT models with native API support
|
||||||
- **Anthropic** - Claude models with OAuth integration
|
- **Anthropic** - Claude models with OAuth integration
|
||||||
- **OpenRouter** - Access to 300+ AI models from multiple providers
|
- **OpenRouter** - Access to 300+ AI models from multiple providers
|
||||||
- **Google** - Gemini models with native integration
|
|
||||||
- **Ollama** - Local model inference for privacy
|
- **Ollama** - Local model inference for privacy
|
||||||
|
|
||||||
### 💬 Core Chat Capabilities
|
### 💬 Core Chat Capabilities
|
||||||
@@ -71,12 +70,43 @@ Automated email responses powered by AI:
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Not available.
|
### Download
|
||||||
|
|
||||||
|
Download the latest release from the [Releases page](https://gitlab.pm/rune/oai-swift/releases). Two builds are available:
|
||||||
|
|
||||||
|
- **oAI-x.x.x-AppleSilicon.dmg** — for Macs with an Apple Silicon chip (M1 and later)
|
||||||
|
- **oAI-x.x.x-Universal.dmg** — runs natively on both Apple Silicon and Intel Macs
|
||||||
|
|
||||||
|
### Installing from DMG
|
||||||
|
|
||||||
|
1. Open the downloaded `.dmg` file
|
||||||
|
2. Drag **oAI.app** into the **Applications** folder
|
||||||
|
3. Eject the DMG
|
||||||
|
4. Launch oAI from Applications or Spotlight
|
||||||
|
|
||||||
|
### First Launch — Gatekeeper Warning
|
||||||
|
|
||||||
|
oAI is **signed by the developer** but has **not yet been notarized by Apple**. Notarization is Apple's automated malware scan — the app itself is safe, but macOS Gatekeeper may block it on first launch with a message saying the app "cannot be opened because the developer cannot be verified."
|
||||||
|
|
||||||
|
To open the app, you have two options:
|
||||||
|
|
||||||
|
**Option A — Right-click to open (quickest):**
|
||||||
|
1. Right-click (or Control-click) `oAI.app` in Applications
|
||||||
|
2. Select **Open** from the context menu
|
||||||
|
3. Click **Open** in the dialog that appears
|
||||||
|
4. After doing this once, the app opens normally from then on
|
||||||
|
|
||||||
|
**Option B — Remove the quarantine flag via Terminal:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xattr -dr com.apple.quarantine /Applications/oAI.app
|
||||||
|
```
|
||||||
|
|
||||||
|
This command removes the quarantine attribute that macOS attaches to files downloaded from the internet. The `-d` flag deletes the attribute, `-r` applies it recursively to the app bundle. Once removed, macOS no longer blocks the app from launching.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
- macOS 14.0+
|
- macOS 14.0 (Sonoma) or later
|
||||||
- Xcode 15.0+
|
- An API key for at least one supported provider (OpenRouter, Anthropic, OpenAI, or Google), or Ollama running locally
|
||||||
- Swift 5.9+
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -85,8 +115,8 @@ Add your API keys in Settings (⌘,) → General tab:
|
|||||||
- **OpenAI** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
|
- **OpenAI** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
|
||||||
- **Anthropic** - Get from [Anthropic Console](https://console.anthropic.com/) or use OAuth
|
- **Anthropic** - Get from [Anthropic Console](https://console.anthropic.com/) or use OAuth
|
||||||
- **OpenRouter** - Get from [OpenRouter Keys](https://openrouter.ai/keys)
|
- **OpenRouter** - Get from [OpenRouter Keys](https://openrouter.ai/keys)
|
||||||
- **Google** - Get from [Google AI Studio](https://makersuite.google.com/app/apikey)
|
|
||||||
- **Ollama** - Base URL (default: http://localhost:11434)
|
- **Ollama** - Base URL (default: http://localhost:11434)
|
||||||
|
- **Google** - API key used for Google Custom Search (web search) and Google embeddings (semantic search) — not a chat provider
|
||||||
|
|
||||||
### Essential Settings
|
### Essential Settings
|
||||||
|
|
||||||
|
|||||||
93
oAI/Services/UpdateCheckService.swift
Normal file
93
oAI/Services/UpdateCheckService.swift
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// UpdateCheckService.swift
|
||||||
|
// oAI
|
||||||
|
//
|
||||||
|
// Checks for new releases on GitLab and surfaces an update badge in the footer
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Rune Olsen
|
||||||
|
//
|
||||||
|
// This file is part of oAI.
|
||||||
|
//
|
||||||
|
// oAI is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// oAI is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
|
||||||
|
// Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public
|
||||||
|
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
final class UpdateCheckService {
|
||||||
|
static let shared = UpdateCheckService()
|
||||||
|
|
||||||
|
var updateAvailable: Bool = false
|
||||||
|
var latestVersion: String? = nil
|
||||||
|
|
||||||
|
private let apiURL = "https://gitlab.pm/api/v4/projects/rune%2Foai-swift/releases"
|
||||||
|
private let releasesURL = URL(string: "https://gitlab.pm/rune/oai-swift/releases")!
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
/// Kick off a background update check. Silently does nothing on failure.
|
||||||
|
func checkForUpdates() {
|
||||||
|
Task {
|
||||||
|
await performCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func performCheck() async {
|
||||||
|
guard let url = URL(string: apiURL) else { return }
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.timeoutInterval = 10
|
||||||
|
|
||||||
|
guard let (data, _) = try? await URLSession.shared.data(for: request) else { return }
|
||||||
|
guard let releases = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]],
|
||||||
|
let latest = releases.first,
|
||||||
|
let tagName = latest["tag_name"] as? String else { return }
|
||||||
|
|
||||||
|
// Strip leading "v" from tag (e.g. "v2.3.1" → "2.3.1")
|
||||||
|
let latestVer = tagName.hasPrefix("v") ? String(tagName.dropFirst()) : tagName
|
||||||
|
let currentVer = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
||||||
|
|
||||||
|
if isNewer(latestVer, than: currentVer) {
|
||||||
|
self.latestVersion = latestVer
|
||||||
|
self.updateAvailable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Semantic version comparison — returns true if `version` is newer than `current`.
|
||||||
|
private func isNewer(_ version: String, than current: String) -> Bool {
|
||||||
|
let lhs = version.split(separator: ".").compactMap { Int($0) }
|
||||||
|
let rhs = current.split(separator: ".").compactMap { Int($0) }
|
||||||
|
let count = max(lhs.count, rhs.count)
|
||||||
|
for i in 0..<count {
|
||||||
|
let l = i < lhs.count ? lhs[i] : 0
|
||||||
|
let r = i < rhs.count ? rhs[i] : 0
|
||||||
|
if l > r { return true }
|
||||||
|
if l < r { return false }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the GitLab releases page in the default browser.
|
||||||
|
func openReleasesPage() {
|
||||||
|
#if os(macOS)
|
||||||
|
NSWorkspace.shared.open(releasesURL)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,11 @@ struct FooterView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update available badge
|
||||||
|
#if os(macOS)
|
||||||
|
UpdateBadge()
|
||||||
|
#endif
|
||||||
|
|
||||||
// Shortcuts hint
|
// Shortcuts hint
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Text("⌘N New • ⌘M Model • ⌘⇧S Save")
|
Text("⌘N New • ⌘M Model • ⌘⇧S Save")
|
||||||
@@ -254,6 +259,32 @@ struct SyncStatusFooter: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct UpdateBadge: View {
|
||||||
|
private let updater = UpdateCheckService.shared
|
||||||
|
private let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if updater.updateAvailable {
|
||||||
|
Button(action: { updater.openReleasesPage() }) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
Text("Update Available\(updater.latestVersion.map { " (v\($0))" } ?? "")")
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
|
.foregroundColor(.green)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("A new version is available — click to open the releases page")
|
||||||
|
} else {
|
||||||
|
Text("v\(currentVersion)")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.oaiSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
let stats = SessionStats(
|
let stats = SessionStats(
|
||||||
totalInputTokens: 1250,
|
totalInputTokens: 1250,
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ struct oAIApp: App {
|
|||||||
Task {
|
Task {
|
||||||
await GitSyncService.shared.syncOnStartup()
|
await GitSyncService.shared.syncOnStartup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for updates in the background
|
||||||
|
UpdateCheckService.shared.checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
#!/usr/bin/swift
|
|
||||||
//
|
|
||||||
// Git Sync Phase 1 Validation Script
|
|
||||||
// Tests integration without requiring git repository
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
print("🧪 Git Sync Phase 1 Validation")
|
|
||||||
print("================================\n")
|
|
||||||
|
|
||||||
var passCount = 0
|
|
||||||
var failCount = 0
|
|
||||||
|
|
||||||
func test(_ name: String, _ block: () throws -> Bool) {
|
|
||||||
do {
|
|
||||||
if try block() {
|
|
||||||
print("✅ \(name)")
|
|
||||||
passCount += 1
|
|
||||||
} else {
|
|
||||||
print("❌ \(name)")
|
|
||||||
failCount += 1
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("❌ \(name) - Error: \(error)")
|
|
||||||
failCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 1: SyncModels.swift exists and has correct structure
|
|
||||||
test("SyncModels.swift exists") {
|
|
||||||
let path = "oAI/Models/SyncModels.swift"
|
|
||||||
return FileManager.default.fileExists(atPath: path)
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SyncModels.swift contains SyncAuthMethod enum") {
|
|
||||||
let path = "oAI/Models/SyncModels.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("enum SyncAuthMethod") &&
|
|
||||||
content.contains("case ssh") &&
|
|
||||||
content.contains("case password") &&
|
|
||||||
content.contains("case token")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SyncModels.swift contains SyncError enum") {
|
|
||||||
let path = "oAI/Models/SyncModels.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("enum SyncError") &&
|
|
||||||
content.contains("case notConfigured") &&
|
|
||||||
content.contains("case secretsDetected")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SyncModels.swift contains SyncStatus struct") {
|
|
||||||
let path = "oAI/Models/SyncModels.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("struct SyncStatus") &&
|
|
||||||
content.contains("var lastSyncTime") &&
|
|
||||||
content.contains("var isCloned")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SyncModels.swift contains ConversationExport struct") {
|
|
||||||
let path = "oAI/Models/SyncModels.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("struct ConversationExport") &&
|
|
||||||
content.contains("func toMarkdown()")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: GitSyncService.swift exists and has correct structure
|
|
||||||
test("GitSyncService.swift exists") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
return FileManager.default.fileExists(atPath: path)
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService.swift has singleton pattern") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("static let shared") &&
|
|
||||||
content.contains("@Observable")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService.swift has core git operations") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("func testConnection()") &&
|
|
||||||
content.contains("func cloneRepository()") &&
|
|
||||||
content.contains("func pull()") &&
|
|
||||||
content.contains("func push(")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService.swift has export/import operations") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("func exportAllConversations()") &&
|
|
||||||
content.contains("func importAllConversations()")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService.swift has secret scanning") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("func scanForSecrets") &&
|
|
||||||
content.contains("OpenAI Key") &&
|
|
||||||
content.contains("Anthropic Key")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService.swift has status management") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("func updateStatus()") &&
|
|
||||||
content.contains("syncStatus")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: SettingsService.swift has sync properties
|
|
||||||
test("SettingsService.swift has syncEnabled property") {
|
|
||||||
let path = "oAI/Services/SettingsService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("var syncEnabled: Bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsService.swift has sync configuration properties") {
|
|
||||||
let path = "oAI/Services/SettingsService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("var syncRepoURL") &&
|
|
||||||
content.contains("var syncLocalPath") &&
|
|
||||||
content.contains("var syncAuthMethod")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsService.swift has encrypted credential properties") {
|
|
||||||
let path = "oAI/Services/SettingsService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("var syncUsername: String?") &&
|
|
||||||
content.contains("var syncPassword: String?") &&
|
|
||||||
content.contains("var syncAccessToken: String?") &&
|
|
||||||
content.contains("getEncryptedSetting") &&
|
|
||||||
content.contains("setEncryptedSetting")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsService.swift has auto-sync toggles") {
|
|
||||||
let path = "oAI/Services/SettingsService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("var syncAutoExport") &&
|
|
||||||
content.contains("var syncAutoPull")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsService.swift has syncConfigured computed property") {
|
|
||||||
let path = "oAI/Services/SettingsService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("var syncConfigured: Bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 4: SettingsView.swift has Sync tab
|
|
||||||
test("SettingsView.swift has Sync tab state variables") {
|
|
||||||
let path = "oAI/Views/Screens/SettingsView.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("@State private var syncRepoURL") &&
|
|
||||||
content.contains("@State private var syncLocalPath") &&
|
|
||||||
content.contains("@State private var syncUsername") &&
|
|
||||||
content.contains("@State private var isTestingSync")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsView.swift has syncTab view") {
|
|
||||||
let path = "oAI/Views/Screens/SettingsView.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("private var syncTab: some View")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsView.swift has sync helper methods") {
|
|
||||||
let path = "oAI/Views/Screens/SettingsView.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("private func testSyncConnection()") &&
|
|
||||||
content.contains("private func cloneRepo()") &&
|
|
||||||
content.contains("private func exportConversations()") &&
|
|
||||||
content.contains("private func pushToGit()") &&
|
|
||||||
content.contains("private func pullFromGit()")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsView.swift has sync status properties") {
|
|
||||||
let path = "oAI/Views/Screens/SettingsView.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("private var syncStatusIcon") &&
|
|
||||||
content.contains("private var syncStatusColor") &&
|
|
||||||
content.contains("private var syncStatusText")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("SettingsView.swift has Sync tab in picker") {
|
|
||||||
let path = "oAI/Views/Screens/SettingsView.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("Text(\"Sync\")") && content.contains(".tag(4)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 5: Build succeeded
|
|
||||||
test("Project builds successfully") {
|
|
||||||
return true // Already verified in previous build
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 6: File structure validation
|
|
||||||
test("All sync files are in correct locations") {
|
|
||||||
let models = FileManager.default.fileExists(atPath: "oAI/Models/SyncModels.swift")
|
|
||||||
let service = FileManager.default.fileExists(atPath: "oAI/Services/GitSyncService.swift")
|
|
||||||
return models && service
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService uses correct DatabaseService methods") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("listConversations()") &&
|
|
||||||
content.contains("loadConversation(id:")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GitSyncService handles async operations correctly") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("async throws") &&
|
|
||||||
content.contains("await runGit")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("Secret scanning patterns are comprehensive") {
|
|
||||||
let path = "oAI/Services/GitSyncService.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
let patterns = [
|
|
||||||
"OpenAI Key",
|
|
||||||
"Anthropic Key",
|
|
||||||
"Bearer Token",
|
|
||||||
"API Key",
|
|
||||||
"Access Token"
|
|
||||||
]
|
|
||||||
return patterns.allSatisfy { content.contains($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
test("ConversationExport markdown format includes metadata") {
|
|
||||||
let path = "oAI/Models/SyncModels.swift"
|
|
||||||
guard let content = try? String(contentsOfFile: path) else { return false }
|
|
||||||
return content.contains("func toMarkdown()") &&
|
|
||||||
content.contains("Created") &&
|
|
||||||
content.contains("Updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print summary
|
|
||||||
print("\n================================")
|
|
||||||
print("📊 Test Results")
|
|
||||||
print("================================")
|
|
||||||
print("✅ Passed: \(passCount)")
|
|
||||||
print("❌ Failed: \(failCount)")
|
|
||||||
print("📈 Total: \(passCount + failCount)")
|
|
||||||
print("🎯 Success Rate: \(passCount * 100 / (passCount + failCount))%")
|
|
||||||
|
|
||||||
if failCount == 0 {
|
|
||||||
print("\n🎉 All tests passed! Phase 1 integration is complete.")
|
|
||||||
exit(0)
|
|
||||||
} else {
|
|
||||||
print("\n⚠️ Some tests failed. Review the results above.")
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user