Files
Termind/TFMTests/WebSearchTests.swift

147 lines
5.4 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AgentKit
import AgentKitScrubber
import Foundation
import TermindApplication
import TermindDomain
import Testing
@Suite
struct WebSearchTests {
@Test
func scrubberSearchToolIsLocallyReadOnlyAndRequiresAQuery() async throws {
let tool = WebSearchTool(web: stubClient())
#expect(tool.descriptor.name == "web_search")
#expect(tool.descriptor.target == .network)
#expect(tool.descriptor.approvalPolicy == .approve)
#expect(try await tool.preflight(invocation(query: "swift")).approvalPolicy == .approve)
await #expect(throws: AgentToolError.self) {
try await tool.preflight(
AgentToolInvocation(
runID: UUID(),
call: AgentToolCall(id: "1", name: "web_search", arguments: .object([:]))
))
}
}
@Test
func localSearchToolIsRegisteredOnlyWhenSelected() {
let without = AgentToolCatalog.registry(builtIn: .init(web: stubClient()))
.descriptors
let with = AgentToolCatalog.registry(
builtIn: .init(web: stubClient(), search: stubClient())
).descriptors
#expect(!without.contains { $0.name == "web_search" })
#expect(with.contains { $0.name == "web_search" })
}
@Test
func protocolSupportIsSeparateFromTheModelGuess() {
var provider = ModelProvider(name: "Gateway")
#expect(!provider.acceptsNativeWebSearchTool)
provider.apiFormat = .responses
#expect(provider.acceptsNativeWebSearchTool)
var anthropic = ModelProvider(name: "Anthropic")
anthropic.apiFormat = .messages
#expect(anthropic.acceptsNativeWebSearchTool)
var google = ModelProvider(name: "Google")
google.apiFormat = .generateContent
#expect(google.acceptsNativeWebSearchTool)
}
@Test
func recognisedProviderModelsStillSupportNativeSearch() {
var model = AIModel(id: "gpt-5.4")
model.abilities = [.toolCall, .webSearch]
var provider = ModelProvider(name: "OpenAI")
provider.apiFormat = .responses
#expect(provider.supportsNativeWebSearch(model: model))
}
@Test @MainActor
func providerNativeSearchMakesACardButNotATranscriptTurn() {
let conversation = ConversationModel()
let invocation = invocation(query: "swift 6")
let descriptor = AgentToolDescriptor(
name: "web_search", summary: "Web search run by the model provider.",
inputSchema: .object(["type": .string("object")]),
target: .network, approvalPolicy: .approve
)
conversation.handle(.toolProposed(invocation, descriptor))
conversation.handle(
.toolFinished(
invocation,
AgentToolResult(
callID: invocation.call.id, content: "[]",
metadata: [AgentToolResult.providerNativeKey: .bool(true)]
)))
#expect(conversation.toolCards.count == 1)
#expect(conversation.transcript.isEmpty)
#expect(conversation.pendingPriorMessages.isEmpty)
}
@Test @MainActor
func ordinarySearchResultsStillEnterTheTranscript() {
let conversation = ConversationModel()
let invocation = invocation(query: "swift 6")
let descriptor = AgentToolDescriptor(
name: "web_search", summary: "Search the public web.",
inputSchema: .object(["type": .string("object")]),
target: .network, approvalPolicy: .approve
)
conversation.handle(.toolProposed(invocation, descriptor))
conversation.handle(
.toolFinished(
invocation,
AgentToolResult(
callID: invocation.call.id, content: "<untrusted-data>\n[]\n</untrusted-data>"
)))
#expect(conversation.transcript.count == 1)
#expect(conversation.transcript[0].role == .tool)
}
@Test
func orphanedProviderSearchResultsAreDroppedFromHistory() {
let history: [AgentTranscriptMessage] = [
AgentTranscriptMessage(role: .user, text: "look it up"),
AgentTranscriptMessage(
role: .assistant, text: "",
toolCalls: [AgentToolCall(id: "call-1", name: "ssh_exec", arguments: .object([:]))]
),
AgentTranscriptMessage(
role: .tool, text: "ok", toolCallID: "call-1", toolName: "ssh_exec"
),
AgentTranscriptMessage(
role: .tool, text: "[]",
toolCallID: "provider-web-search-11328F4A", toolName: "web_search"
),
]
let repaired = AgentOrphanedToolResultRepair.apply(history)
#expect(repaired.count == 3)
#expect(!repaired.contains { $0.toolCallID?.hasPrefix("provider-web-search") == true })
}
private func stubClient() -> ScrubberWebClient {
ScrubberWebClient(
document: { _ in throw AgentWebError.emptyDocument },
search: { _, _ in [] }
)
}
private func invocation(query: String) -> AgentToolInvocation {
AgentToolInvocation(
runID: UUID(),
call: AgentToolCall(
id: "call-1", name: "web_search",
arguments: .object(["query": .string(query)])
)
)
}
}