2.4.3 #9

Merged
rune merged 14 commits from 2.4.3 into main 2026-07-27 08:13:22 +02:00
Owner
No description provided.
rune added 12 commits 2026-07-27 08:13:11 +02:00
The old Tests/oAITests/oAITests.swift + root Package.swift/Sources/
were a swift package init stub that @testable imported a fake empty
oAI module, completely disconnected from the real ~31K-line app
(built only via oAI.xcodeproj). Running `swift test` there would
have silently "passed" while testing nothing.

oAITests is a proper Unit Testing Bundle target added via Xcode's
target editor, wired into the app's scheme (TestAction references
oAITests.xctest). Verified with `xcodebuild test` before and after
removing the dead scaffold.

First phase of the test-suite rollout plan (peaceful-baking-kurzweil).
27 tests across 4 files, all targeting code that was already testable
the moment the target existed -- no visibility bumps, no refactors:

- GitignoreParserTests: MCPService.GitignoreParser's glob-to-regex
  matching (wildcards, **, directory anchors, negation, comments).
- OpenRouterModelsTests: the string-vs-content-block-array decoders
  on both the request side (APIMessage.MessageContent/ContentItem)
  and response side (Choice.MessageContent, StreamChoice.Delta),
  including image extraction from content blocks.
- AIProviderTests: ChatResponse/Usage decoding, with an explicit
  regression guard that Usage.rawCostUSD always decodes to nil (it's
  only ever set programmatically, never from API JSON).
- MessageCodableTests: confirms Message's transient fields
  (isStreaming, isStarred, generatedImages, toolCalls,
  thinkingContent) don't survive an encode/decode round-trip, and
  documents that its custom == deliberately ignores role/timestamp/
  attachments/modelId -- a real but non-obvious behavior worth
  locking in with a test.

Removed the placeholder oAITests.swift example test now that there's
real coverage. Phase 1 of the test-suite rollout plan
(peaceful-baking-kurzweil).
Every change here is either dropping `private` (still invisible
outside the module, @testable import just needs internal-or-wider)
or converting a self-independent instance method to `static func`
(ChatViewModel.inferProvider/calculateCost/detectGoodbyePhrase --
none of the three ever touched `self`, and constructing a real
ChatViewModel triggers a real network call in init, so static-ifying
them sidesteps that entirely rather than fighting it). Call sites
updated to `Self.foo(...)` where the static conversion required it.

Touches: GitSyncService's URL/secret-scanning helpers,
EmbeddingService's embedding (de)serialization, ContextSelectionService's
importance scoring, and the request-building/response-parsing helpers
on all four providers (OpenRouter, Ollama, OpenAI, Anthropic) -- the
core AI request/response layer, previously 100% untested.

Verified: full existing test suite (27 tests) still green, no
regressions. Tests for these functions land in the next commit.
Phase 2 of the test-suite rollout plan (peaceful-baking-kurzweil).
66 new tests across 8 files (93 total in the suite now):

- GitSyncServiceTests: convertToSSH/injectCredentials/sanitizeFilename/
  detectSecretsInText, including a below-threshold false-positive check
  on the secret regex.
- ChatViewModelPureLogicTests: detectGoodbyePhrase, inferProvider,
  calculateCost -- including the cache-read (0.1x) / cache-write (1.25x)
  pricing multipliers, which is real billing-affecting logic.
- EmbeddingServiceTests / ContextSelectionServiceTests: embedding
  (de)serialization round-trip, importance-score weighting, and the
  token-estimate fallback (content.count / 4) when no real count exists.
- OpenRouterProviderTests / OllamaProviderTests / OpenAIProviderTests /
  AnthropicProviderTests: request-building (attachments, online mode,
  cache_control breakpoints, o1/o3 temperature omission, tool schema
  conversion) and response-parsing (text, tool_use blocks, empty-choices
  fallback behavior) for all four providers, with no network involved.

Also marks calculateCost/inferProvider/detectGoodbyePhrase (ChatViewModel)
and serializeEmbedding/deserializeEmbedding (EmbeddingService) as
`nonisolated` -- discovered via the actual test failures, not
speculation: the project's `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`
setting isolates the classes that carry an explicit `@MainActor`
(ChatViewModel), so calling their static members from a plain
synchronous @Test needs the pure ones marked `nonisolated`. Matches
the existing convention already used elsewhere in EmbeddingService
(cosineSimilarity was already nonisolated before this change).

Phase 2 of the test-suite rollout plan (peaceful-baking-kurzweil).
The oAITests PBXNativeTarget (PBXContainerItemProxy, PBXTargetDependency,
XCBuildConfiguration with TEST_HOST/BUNDLE_LOADER, the "recommended
settings" changes accepted when the target was created -- DEAD_CODE_STRIPPING,
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED, STRING_CATALOG_GENERATE_SYMBOLS,
DEVELOPMENT_TEAM moved to project-level inheritance) was somehow never
actually staged in the very first "Add real oAITests target" commit
(8c7fb59) despite every xcodebuild test run since then depending on it
being present on disk. Every subsequent commit this session only staged
specific file paths (never oAI.xcodeproj again), so the gap went
unnoticed until a full `git status` review here.

Without this, anyone else pulling the branch (or a truly clean checkout
on this machine) would have all the .swift test files but no target to
compile them into -- xcodebuild test would fail to find oAITests at all.
Confirmed the diff is exactly the expected target-wiring content, nothing
unrelated or corrupted, before committing.
DatabaseService gains a testable init(dbQueue:) plus a makeInMemory()
convenience and schema-introspection helpers (tableExists/columnNames),
so migration tests don't need the test target to import GRDB directly.
ContextSelectionService now takes an injected DatabaseService (defaults
to .shared) and its DB-coupled methods (smartSelection,
getSummariesForExcludedRange, isMessageStarred) are bumped to internal.
No logic changes to existing call sites.
17 tests against throwaway in-memory DatabaseService instances: all
v1-v8 tables/columns exist post-migration, settings CRUD, conversation
save/load round-trip, instance isolation between separate in-memory
queues, and ContextSelectionService's DB-coupled paths (starring,
excluded-range summaries, smartSelection end to end) that were
previously untestable.
Apple Intelligence / Foundation Models is now genuinely available on
this machine (macOS 27 beta 4) — it was reverted back in June
(f63226b) when it wasn't. Ports the reverted AppleFoundationProvider
forward onto 2.4.3, adapted for everything that's changed since:
PolyForm license headers, current AIProvider protocol shape, current
Settings.Provider/ProviderRegistry/CreditsView/SettingsView structure.

Fixes a real bug found via live testing: LanguageModelSession.
GenerationError was deprecated in macOS 27.0 in favor of a new
LanguageModelError type. On a macOS 27+ runtime, generation failures
now throw LanguageModelError, not GenerationError, so the original
error-mapping catch never matched and Apple's raw error text leaked
to the user instead of oAI's friendly message. Now dispatches to
whichever type the runtime actually throws, gated with
@available(macOS 27.0, *), keeping the old GenerationError path as
a fallback for macOS 26.x (the app's actual deployment target).

Confirmed end-to-end in the live app: provider selectable, Settings
shows a live "Available" badge, chat header shows correct branding,
and — a real, expected Phase 1 limitation — oAI's default system
prompt (active Agent Skills + MCP tool guidance, ~16K tokens on this
machine) exceeds the on-device model's 4K context window on the
very first message. The friendly error message now correctly reports
this instead of Apple's raw string. Tool calling remains out of scope
until Phase 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
effectiveSystemPrompt now gates tool-usage guidelines, the user's
custom system prompt, and active Agent Skills behind whether the
selected model actually supports tools (ModelInfo.capabilities.tools).
All three assume tool/file/web access and can easily blow past small
context windows.

Confirmed live against Apple Intelligence: the default system prompt
dropped from 16,377 to ~250 tokens (well under the 4K on-device
limit), fully resolving the context-exceeded error from the initial
Phase 1 rollout. Verified end-to-end with a real successful generation
in the app after the fix.

Also: .gitignore now excludes *.profraw (stray code-coverage artifact
picked up while testing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Apple's Foundation Models framework is still under active development
(built against macOS 27 beta) and likely to stay rough for a while —
small 4K context window, occasional generation errors, no tool
support yet. Surface that clearly wherever it appears:
- Model name: "Apple On-Device (Beta)" (shows in header/model picker)
- Model description: notes the beta status and what to expect
- Settings -> General: "⚠️ Beta — ..." disclaimer under the Apple
  Intelligence section, same style as the existing Paperless-NGX beta
  note
- Credits panel: same disclaimer for the Apple Intelligence entry

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MCP's read_file previously hard-required UTF-8 text decoding, so any
PDF in an allowed folder failed with "Cannot read file as UTF-8
text" — chat attachments already supported PDFs (raw bytes to
vision-capable models), but the AI couldn't read one on its own
during agentic file-tool use. search_files' content_search had the
same gap, silently skipping PDFs.

Adds MCPService.extractPDFText(atPath:) using PDFKit (built into
macOS, no new dependency) to pull text from a PDF's text layer.
Wired into both read_file and search_files' content search. Returns
a clear error for scanned/image-only PDFs with no text layer.
Automatically covers the Research Agents sub-agent tool loop too,
since it shares the same executeTool dispatcher.

Verified live: asked the AI to read a real PDF containing "The
secret code is PINEAPPLE-42." via read_file — it extracted the text
correctly through the MCP tool-call path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rune merged commit ef26ae1887 into main 2026-07-27 08:13:22 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Reference: rune/oai-swift#9