Files
scarf/scarf/scarfTests/CredentialPoolsGatingTests.swift
T
Alan Wizemann da721fa276 feat(hermes-v12): provider catalog + auxiliary swap (Phase B)
Adds the five v0.12 inference providers to ModelCatalogService.overlayOnlyProviders
so the model picker reaches them. IDs match HERMES_OVERLAYS verbatim:

- gmi → GMI Cloud (api_key)
- azure-foundry → Azure AI Foundry (api_key)
- lmstudio → LM Studio (api_key, promoted from custom-endpoint alias)
- minimax-oauth → MiniMax (OAuth, oauth_external)
- tencent-tokenhub → Tencent TokenHub (api_key)

Auxiliary tasks: drop the `flush_memories` row (Hermes removed it
entirely in v0.12) and add `auxiliary.curator` so users can configure
the model the autonomous curator's review fork uses. The Curator row is
gated on HermesCapabilities.hasCuratorAux, so v0.11 hosts don't see a
control that writes a key Hermes ignores. AuxiliarySettings, the YAML
parser, and HealthViewModel's Tool Gateway breakdown are all updated.

Side fixes:

- CredentialPoolsGatingTests was missing `import ScarfCore` after
  ModelCatalogService moved to the package (broke the test target's
  compile against pure-Mac scarf).
- Promoted `ModelCatalogService.overlayOnlyProviders` to public so the
  new `v012OverlayProvidersCarryCorrectAuthTypes` lock-in test can
  reach it.

Tests: 14 ToolGateway tests pass; 209 ScarfCore tests pass; both Mac
and iOS schemes build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:16:37 +02:00

86 lines
3.8 KiB
Swift

import Testing
import Foundation
import ScarfCore
@testable import scarf
/// Tests that ``CredentialPoolsOAuthGate`` steers each known provider to
/// the right OAuth flow. The regression this prevents: a user hitting the
/// "Start OAuth" button for nous / openai-codex / qwen-oauth /
/// google-gemini-cli / copilot-acp and watching the UI stall silently.
@Suite struct CredentialPoolsGatingTests {
/// Synthesize a ModelCatalogService over a minimal fixture cache so
/// tests don't depend on the live `~/.hermes/models_dev_cache.json`.
private func makeCatalog() throws -> ModelCatalogService {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("scarf-cpgate-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let path = dir.appendingPathComponent("models_dev_cache.json").path
// Include anthropic so the .ok path has a recognizable provider.
let json = """
{
"anthropic": {
"name": "Anthropic",
"models": { "claude-sonnet-4-5": { "name": "Claude Sonnet 4.5" } }
}
}
"""
try json.write(toFile: path, atomically: true, encoding: .utf8)
return ModelCatalogService(path: path)
}
@Test func nousRoutesToDedicatedSignInFlow() throws {
let catalog = try makeCatalog()
#expect(CredentialPoolsOAuthGate.resolve(providerID: "nous", catalog: catalog) == .useNousSignIn)
// Whitespace + case insensitivity should also work users who type
// "Nous " shouldn't fall through to the generic flow.
#expect(CredentialPoolsOAuthGate.resolve(providerID: " Nous ", catalog: catalog) == .useNousSignIn)
}
@Test func deviceCodeAndExternalProvidersRouteToCLI() throws {
let catalog = try makeCatalog()
// `openai-codex` is .oauthExternal in the overlay table.
if case .useCLI(let provider) = CredentialPoolsOAuthGate.resolve(providerID: "openai-codex", catalog: catalog) {
#expect(provider == "openai-codex")
} else {
Issue.record("openai-codex should route to .useCLI")
}
// `qwen-oauth` is .oauthExternal.
if case .useCLI = CredentialPoolsOAuthGate.resolve(providerID: "qwen-oauth", catalog: catalog) {
// ok
} else {
Issue.record("qwen-oauth should route to .useCLI")
}
// `google-gemini-cli` is .oauthExternal.
if case .useCLI = CredentialPoolsOAuthGate.resolve(providerID: "google-gemini-cli", catalog: catalog) {
// ok
} else {
Issue.record("google-gemini-cli should route to .useCLI")
}
// `copilot-acp` is .externalProcess.
if case .useCLI = CredentialPoolsOAuthGate.resolve(providerID: "copilot-acp", catalog: catalog) {
// ok
} else {
Issue.record("copilot-acp should route to .useCLI")
}
}
@Test func pkceProvidersPassThroughAsOK() throws {
let catalog = try makeCatalog()
// Anthropic is a standard PKCE provider in Hermes must not be gated.
#expect(CredentialPoolsOAuthGate.resolve(providerID: "anthropic", catalog: catalog) == .ok)
}
@Test func unknownProvidersDefaultToOK() throws {
let catalog = try makeCatalog()
// Providers we don't know about shouldn't be blocked users with
// custom setups need the escape hatch.
#expect(CredentialPoolsOAuthGate.resolve(providerID: "custom-provider-xyz", catalog: catalog) == .ok)
}
@Test func emptyProviderReturnsProviderEmpty() throws {
let catalog = try makeCatalog()
#expect(CredentialPoolsOAuthGate.resolve(providerID: "", catalog: catalog) == .providerEmpty)
#expect(CredentialPoolsOAuthGate.resolve(providerID: " ", catalog: catalog) == .providerEmpty)
}
}