Rename app from oAI to Confab
"oAI" reads as easily confused with OpenAI, both visually and in casual conversation. Renamed to "Confab" throughout: Xcode target/scheme/bundle ID (com.oai.Confab), Info.plist and Help Book identity, all user-facing UI text, internal Log subsystem and color identifiers, localization catalogs (6 languages, including a proper reworded/retranslated Intel-deprecation notice), Help Book HTML content, and docs (README/DEVELOPMENT/PRIVACY/SECURITY). Deliberately cosmetic-only: the on-disk data folder (~/Library/Application Support/oAI/), database/backup filenames, Keychain service identifiers, and EncryptionService's key-derivation inputs are all left untouched so existing conversations, settings, and stored API keys survive the update with zero migration and no re-entering credentials. Verified live: a real signed build successfully decrypted a stored API key and loaded an existing conversation database after the bundle ID change. Also includes a small already-completed, previously uncommitted model-release-date feature (ModelInfo/OpenRouterModels/ OpenRouterProvider/ModelInfoView) that happened to share several files with this rename. Gitignored on this branch and updated on disk but not part of this commit: CLAUDE.md, RELEASE_NOTES.md, and the build*.sh scripts.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ChatResponse / Usage decoding")
|
||||
struct AIProviderTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("AnthropicProvider request/response conversion")
|
||||
struct AnthropicProviderTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("BackupService.decideFavoritesSync")
|
||||
struct BackupServiceFavoritesSyncTests {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ChatViewModel pure static helpers")
|
||||
struct ChatViewModelPureLogicTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ContextSelectionService DB-coupled paths, against a throwaway in-memory queue")
|
||||
struct ContextSelectionServiceDBTests {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ContextSelectionService pure scoring helpers")
|
||||
struct ContextSelectionServiceTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ConversationExportService")
|
||||
struct ConversationExportServiceTests {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// ConversationListViewPureLogicTests.swift
|
||||
// oAITests
|
||||
//
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ConversationListView.idsInRange")
|
||||
struct ConversationListViewPureLogicTests {
|
||||
|
||||
private let ids = (0..<6).map { _ in UUID() }
|
||||
|
||||
@Test("Forward range includes anchor and target inclusive")
|
||||
func forwardRange() {
|
||||
let result = ConversationListView.idsInRange(orderedIds: ids, anchorId: ids[1], targetId: ids[4])
|
||||
#expect(result == Set(ids[1...4]))
|
||||
}
|
||||
|
||||
@Test("Backward range (target above anchor) still selects the span between them")
|
||||
func backwardRange() {
|
||||
let result = ConversationListView.idsInRange(orderedIds: ids, anchorId: ids[4], targetId: ids[1])
|
||||
#expect(result == Set(ids[1...4]))
|
||||
}
|
||||
|
||||
@Test("Anchor equal to target selects just that one id")
|
||||
func sameAnchorAndTarget() {
|
||||
let result = ConversationListView.idsInRange(orderedIds: ids, anchorId: ids[2], targetId: ids[2])
|
||||
#expect(result == [ids[2]])
|
||||
}
|
||||
|
||||
@Test("Nil anchor (first Shift-click) falls back to just the target")
|
||||
func nilAnchorFallsBackToTarget() {
|
||||
let result = ConversationListView.idsInRange(orderedIds: ids, anchorId: nil, targetId: ids[3])
|
||||
#expect(result == [ids[3]])
|
||||
}
|
||||
|
||||
@Test("Anchor no longer present in the ordered list falls back to just the target")
|
||||
func missingAnchorFallsBackToTarget() {
|
||||
let staleAnchor = UUID()
|
||||
let result = ConversationListView.idsInRange(orderedIds: ids, anchorId: staleAnchor, targetId: ids[3])
|
||||
#expect(result == [ids[3]])
|
||||
}
|
||||
|
||||
@Test("Single-element list selects that element")
|
||||
func singleElementList() {
|
||||
let solo = [ids[0]]
|
||||
let result = ConversationListView.idsInRange(orderedIds: solo, anchorId: ids[0], targetId: ids[0])
|
||||
#expect(result == [ids[0]])
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("ConversationMergeService AI response parsing")
|
||||
struct ConversationMergeServiceParsingTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("DatabaseService migrations, against a throwaway in-memory queue")
|
||||
struct DatabaseServiceMigrationTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("DraftRecoveryService")
|
||||
struct DraftRecoveryServiceTests {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("EmbeddingService serialization")
|
||||
struct EmbeddingServiceTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("GitSyncService pure helpers")
|
||||
struct GitSyncServiceTests {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("GitignoreParser")
|
||||
struct GitignoreParserTests {
|
||||
|
||||
@@ -9,7 +9,7 @@ import Testing
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import AppKit
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("MCPService PDF text extraction")
|
||||
struct MCPServicePDFTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("Message Codable + Equatable")
|
||||
struct MessageCodableTests {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// NativeTextEditorPureLogicTests.swift
|
||||
// oAITests
|
||||
//
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("NativeTextEditor.inlineCodeRanges")
|
||||
struct NativeTextEditorPureLogicTests {
|
||||
|
||||
@Test("No backticks yields no ranges")
|
||||
func noBackticks() {
|
||||
#expect(NativeTextEditor.inlineCodeRanges(in: "hello world").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A single closed backtick pair is matched, backticks included")
|
||||
func singleClosedPair() {
|
||||
let text = "Hello `code` world"
|
||||
let ranges = NativeTextEditor.inlineCodeRanges(in: text)
|
||||
#expect(ranges.count == 1)
|
||||
let matched = (text as NSString).substring(with: ranges[0])
|
||||
#expect(matched == "`code`")
|
||||
}
|
||||
|
||||
@Test("An unterminated single backtick is not matched")
|
||||
func unterminatedBacktick() {
|
||||
let text = "Hello `code world"
|
||||
#expect(NativeTextEditor.inlineCodeRanges(in: text).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Multiple closed pairs are all matched")
|
||||
func multiplePairs() {
|
||||
let text = "`one` and `two` and `three`"
|
||||
let ranges = NativeTextEditor.inlineCodeRanges(in: text)
|
||||
#expect(ranges.count == 3)
|
||||
let nsText = text as NSString
|
||||
#expect(ranges.map { nsText.substring(with: $0) } == ["`one`", "`two`", "`three`"])
|
||||
}
|
||||
|
||||
@Test("Backtick pair cannot span a newline")
|
||||
func doesNotSpanNewline() {
|
||||
let text = "`code\nblock`"
|
||||
#expect(NativeTextEditor.inlineCodeRanges(in: text).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Empty string yields no ranges")
|
||||
func emptyString() {
|
||||
#expect(NativeTextEditor.inlineCodeRanges(in: "").isEmpty)
|
||||
}
|
||||
|
||||
@Test("Adjacent backtick pairs on the same line are each matched separately")
|
||||
func adjacentPairs() {
|
||||
let text = "`a``b`"
|
||||
let ranges = NativeTextEditor.inlineCodeRanges(in: text)
|
||||
let nsText = text as NSString
|
||||
#expect(ranges.map { nsText.substring(with: $0) } == ["`a`", "`b`"])
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("NativeTextEditor.fencedCodeBlockRanges")
|
||||
struct NativeTextEditorFencedBlockTests {
|
||||
|
||||
@Test("No fences yields no ranges")
|
||||
func noFences() {
|
||||
#expect(NativeTextEditor.fencedCodeBlockRanges(in: "hello world").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A closed multi-line fence is matched in full, including the language tag")
|
||||
func closedMultilineFence() {
|
||||
let text = "```python\nprint(\"hi\")\n```"
|
||||
let ranges = NativeTextEditor.fencedCodeBlockRanges(in: text)
|
||||
#expect(ranges.count == 1)
|
||||
#expect((text as NSString).substring(with: ranges[0]) == text)
|
||||
}
|
||||
|
||||
@Test("An unterminated fence (no closing triple) is not matched")
|
||||
func unterminatedFence() {
|
||||
let text = "```python\nprint(\"hi\")"
|
||||
#expect(NativeTextEditor.fencedCodeBlockRanges(in: text).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Text before and after a fence is excluded from the match")
|
||||
func surroundingTextExcluded() {
|
||||
let text = "before\n```\ncode\n```\nafter"
|
||||
let ranges = NativeTextEditor.fencedCodeBlockRanges(in: text)
|
||||
#expect(ranges.count == 1)
|
||||
#expect((text as NSString).substring(with: ranges[0]) == "```\ncode\n```")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("NativeTextEditor.codeStyledRanges")
|
||||
struct NativeTextEditorCodeStyledRangesTests {
|
||||
|
||||
@Test("Combines a fenced block and a separate inline span")
|
||||
func combinesFencedAndInline() {
|
||||
let text = "See `foo` then:\n```\nbar\n```"
|
||||
let ranges = NativeTextEditor.codeStyledRanges(in: text)
|
||||
let nsText = text as NSString
|
||||
#expect(ranges.map { nsText.substring(with: $0) } == ["`foo`", "```\nbar\n```"])
|
||||
}
|
||||
|
||||
@Test("A backtick inside a fenced block's own content is not separately re-matched as inline")
|
||||
func backtickInsideFenceNotDoubleMatched() {
|
||||
let text = "```\nuse `code` here\n```"
|
||||
let ranges = NativeTextEditor.codeStyledRanges(in: text)
|
||||
#expect(ranges.count == 1)
|
||||
#expect((text as NSString).substring(with: ranges[0]) == text)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("NativeTextEditor.fencedCodeBlocks (language + code-content extraction)")
|
||||
struct NativeTextEditorFencedCodeBlocksTests {
|
||||
|
||||
@Test("Extracts the language tag and the code content, excluding fences and the tag line")
|
||||
func extractsLanguageAndCodeContent() {
|
||||
let text = "```python\nprint(\"hi\")\n```"
|
||||
let blocks = NativeTextEditor.fencedCodeBlocks(in: text)
|
||||
#expect(blocks.count == 1)
|
||||
#expect(blocks[0].language == "python")
|
||||
let nsText = text as NSString
|
||||
#expect(nsText.substring(with: blocks[0].codeRange) == "print(\"hi\")\n")
|
||||
}
|
||||
|
||||
@Test("No language tag yields a nil language")
|
||||
func noLanguageTagYieldsNil() {
|
||||
let text = "```\ncode\n```"
|
||||
let blocks = NativeTextEditor.fencedCodeBlocks(in: text)
|
||||
#expect(blocks.count == 1)
|
||||
#expect(blocks[0].language == nil)
|
||||
}
|
||||
|
||||
@Test("Language alias is passed through as typed, unresolved (SyntaxHighlighter resolves aliases itself)")
|
||||
func languageAliasPassthrough() {
|
||||
let text = "```py\nprint(1)\n```"
|
||||
let blocks = NativeTextEditor.fencedCodeBlocks(in: text)
|
||||
#expect(blocks[0].language == "py")
|
||||
}
|
||||
|
||||
@Test("Unterminated fence yields no blocks")
|
||||
func unterminatedFenceYieldsNoBlocks() {
|
||||
#expect(NativeTextEditor.fencedCodeBlocks(in: "```python\nprint(1)").isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("SyntaxHighlighter runs → NSColor")
|
||||
struct SyntaxHighlighterBridgingTests {
|
||||
|
||||
// NB: bridging via `NSAttributedString(highlighted)` does NOT work — that path stores
|
||||
// SwiftUI's `.foregroundColor` under a private `SwiftUI.ForegroundColor` key, not the
|
||||
// standard Cocoa `.foregroundColor` key (confirmed empirically before landing on this
|
||||
// `.runs`-based approach, which is what `NativeTextEditor` actually uses in production).
|
||||
|
||||
@Test("A Python keyword's foreground color is readable via AttributedString.runs and converts to NSColor")
|
||||
func keywordColorReadableViaRuns() {
|
||||
let highlighted = SyntaxHighlighter.highlight(code: "def foo():", language: "python")
|
||||
#expect(!highlighted.runs.isEmpty)
|
||||
|
||||
var foundKeywordColor = false
|
||||
let expected = NSColor(SyntaxHighlighter.keywordColor)
|
||||
for run in highlighted.runs {
|
||||
guard let color = run.foregroundColor else { continue }
|
||||
if NSColor(color).usingColorSpace(.deviceRGB) == expected.usingColorSpace(.deviceRGB) {
|
||||
foundKeywordColor = true
|
||||
}
|
||||
}
|
||||
#expect(foundKeywordColor)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("OllamaProvider request/response conversion")
|
||||
struct OllamaProviderTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("OpenAIProvider request/response conversion")
|
||||
struct OpenAIProviderTests {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("OpenRouterModels decoding")
|
||||
struct OpenRouterModelsTests {
|
||||
@@ -129,4 +129,37 @@ struct OpenRouterModelsTests {
|
||||
#expect(delta.contentBlockImages.count == 1)
|
||||
#expect(delta.contentBlockImages.first?.imageUrl.url == "https://example.com/stream.png")
|
||||
}
|
||||
|
||||
// MARK: - Model list: ModelData.created (release date)
|
||||
|
||||
@Test("ModelData decodes a created timestamp when present")
|
||||
func modelDataDecodesCreated() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude Sonnet 4.5",
|
||||
"description": null,
|
||||
"context_length": 200000,
|
||||
"pricing": {"prompt": "0.000003", "completion": "0.000015"},
|
||||
"created": 1735689600
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let model = try JSONDecoder().decode(OpenRouterModelsResponse.ModelData.self, from: json)
|
||||
#expect(model.created == 1735689600)
|
||||
}
|
||||
|
||||
@Test("ModelData decodes to nil created when the field is absent")
|
||||
func modelDataMissingCreated() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": "some/model",
|
||||
"name": "Some Model",
|
||||
"description": null,
|
||||
"context_length": 4096,
|
||||
"pricing": {"prompt": "0", "completion": "0"}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let model = try JSONDecoder().decode(OpenRouterModelsResponse.ModelData.self, from: json)
|
||||
#expect(model.created == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("OpenRouterProvider request/response conversion")
|
||||
struct OpenRouterProviderTests {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
|
||||
import Testing
|
||||
@testable import oAI
|
||||
@testable import Confab
|
||||
|
||||
@Suite("Settings.Provider display metadata")
|
||||
struct SettingsProviderTests {
|
||||
|
||||
Reference in New Issue
Block a user