Files
Termind/TFMTests/AgentKnowledgeToolPresentationTests.swift

188 lines
8.9 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AgentKit
import Foundation
import TermindApplication
import TermindPresentation
import TermindRemoteInfrastructure
import Testing
@Suite struct AgentKnowledgeToolPresentationTests {
private let english = Locale(identifier: "en")
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func oldMemoryMetadataIsRepairedInEveryState(_ language: String) {
let locale = Locale(identifier: language)
for state: AgentToolCard.State in [
.proposed, .waitingForApproval, .waitingForUser, .reviewing, .running,
.completed, .failed, .denied, .interrupted,
] {
var value = card(
"memory",
arguments: [
"operations": .array([
.object([
"action": .string("add"), "target": .string("user"), "content": .string("private-body"),
])
])
])
value.state = state
let title = TermindToolPresentationProfiles.renderer.summary(for: value, locale: locale)
#expect(!title.contains("skill"))
#expect(!title.contains("技能"))
#expect(!title.contains("private-body"))
#expect(title.contains(language == "en" ? "memory" : language == "zh-Hans" ? "记忆" : "記憶"))
}
var value = card("memory", arguments: ["operations": .array([])])
value.state = .completed
if language == "en" {
#expect(TermindToolPresentationProfiles.renderer.summary(for: value, locale: locale) == "Read memory")
}
}
@Test func eachKnowledgeModeHasTheCorrectActionAndTarget() {
let cases: [(String, [String: AgentJSONValue], String)] = [
("skills_list", [:], "Listed skills"),
("skill_read_file", ["name": .string("guide")], "Read guide / SKILL.md"),
("skill_read_file", ["name": .string("guide"), "path": .string(".")], "Listed guide"),
("skill_install", ["name": .string("guide")], "Installed guide"),
(
"skill_manage",
["operations": .array([.object(["action": .string("delete"), "name": .string("guide")])])],
"Updated guide"
),
("memory", ["operations": .array([.object(["action": .string("add")])])], "Updated memory"),
("session_search", [:], "Listed saved conversations"),
("session_search", ["query": .string("deploy")], "Searched deploy in chats"),
("session_search", ["conversation_id": .string("hidden-id")], "Read saved conversations"),
]
for (name, args, expected) in cases {
let value = card(name, arguments: args)
#expect(TermindToolPresentationProfiles.renderer.summary(for: value, locale: english) == expected)
}
}
@Test func mixedMemoryReadAndWriteBatchesKeepTheirActionsAndFailuresSeparate() {
let read = card("memory", arguments: ["operations": .array([])])
var write = card("memory", arguments: ["operations": .array([.object(["action": .string("add")])])])
write.state = .failed
let title = TermindToolPresentationProfiles.renderer.batchSummary(for: [read, read, write], locale: english)
#expect(title.contains("Read memory"))
#expect(title.contains("memory (2 times)"))
#expect(!title.contains("tool calls"))
#expect(title.contains("failed to update memory"))
#expect(!title.contains("skill"))
}
@Test func unknownPresenterOrRemoteIdentityCannotSelectKnowledgePolicy() {
var value = card("memory")
value.descriptor.presentation?.presenterID = "remote.memory"
#expect(TermindAgentTools.presenter(for: value.descriptor) == nil)
value.descriptor.presentation?.presenterID = "builtin.memory"
value.descriptor.target = .mcp
#expect(TermindAgentTools.presenter(for: value.descriptor) == nil)
}
@Test func approvalKeepsBodyOutOfItsTitleAndProjectsTheCompleteArguments() {
let body = String(repeating: "approval-only-content\n", count: 1_000)
let value = card(
"skill_manage",
arguments: [
"operations": .array([
.object([
"action": .string("create"), "name": .string("example"), "content": .string(body),
])
])
])
let request = AgentApprovalRequest(
invocation: value.invocation, descriptor: value.descriptor, userIntent: "Create")
let approval = TermindToolPresentationProfiles.renderer.approval(
for: request, presenter: SkillManageTool.presenter, locale: english)
#expect(approval.title == "Update skills")
#expect(approval.detail.items.contains(.text(.init(title: "Content", text: body, style: .monospaced))))
}
@Test func failedKnowledgeResultsCannotDrawSuccessMessages() throws {
var value = card("skill_install")
value.state = .failed
value.result = AgentToolResult(callID: "call", content: "{\"saved\":true}", isError: true)
let detail = try #require(
TermindToolPresentationProfiles.renderer.detail(
for: value, presenter: SkillInstallTool.presenter, locale: english))
#expect(
!detail.items.contains {
if case .message(_, .success) = $0 { return true }
return false
})
}
@Test func titleLimitsDoNotChangeOriginalArguments() {
let query = "开始\n" + String(repeating: "🧭", count: 160)
let value = card("session_search", arguments: ["query": .string(query)])
let title = TermindToolPresentationProfiles.renderer.summary(for: value, locale: english)
#expect(title.count <= 80)
#expect(!title.contains("\n"))
#expect(title.contains("…"))
#expect(title.hasSuffix(" in chats"))
#expect(value.invocation.call.arguments.objectValue?["query"]?.stringValue == query)
}
@Test func wholeCardPreviewSharesItsBudgetAndPreservesFullContent() {
let text = String(repeating: "中🧭", count: 900)
let detail = AgentToolDetail(
items: (0..<32).map { .text(.init(title: "Block \($0)", text: text, style: .plain)) })
let preview = AgentToolDetailPreview(detail)
#expect(preview.isShortened)
#expect(preview.fullItems == detail.items)
let rendered = AgentToolDetailPreview(AgentToolDetail(items: preview.items)).fullText
// Five complete logical lines share the card body budget; headings are metadata.
#expect(rendered.utf8.count <= AgentToolDetailPreview.byteLimit + 100)
#expect(preview.items.count == 5)
#expect(rendered.hasSuffix(text))
#expect(preview.fullText.contains("Block 31"))
#expect(preview.fullText.hasSuffix(text))
}
@Test func listsShareFiveRowsAcrossSectionsAndShortDetailsRemainUnchanged() {
let rows = (0..<4).map { AgentToolDetail.ListRow(title: "Skill \($0)") }
let detail = AgentToolDetail(items: [
.list(.init(title: "First", rows: rows)), .list(.init(title: "Second", rows: rows)),
])
let preview = AgentToolDetailPreview(detail)
let count = preview.items.reduce(0) { count, item in
if case .list(let list) = item { return count + list.rows.count }
return count
}
#expect(count == 5)
#expect(preview.isShortened)
let short = AgentToolDetail(items: [.list(.init(rows: [.init(title: "One")], emptyMessage: "None"))])
#expect(!AgentToolDetailPreview(short).isShortened)
}
@Test @MainActor func detailExpansionIsIndependentAndSurvivesReconfiguration() {
let state = AssistantTranscriptPresentationState()
let id = UUID()
state.toggleDisclosure(.toolDetail(id))
let revision = state.layoutRevision(for: [id])
state.followDefault(false, for: .tool(id))
state.prune(validTimelineIDs: [id])
#expect(state.isExpanded(.toolDetail(id)))
#expect(!state.isExpanded(.tool(id)))
#expect(state.layoutRevision(for: [id]) >= revision)
state.prune(validTimelineIDs: [])
#expect(!state.isExpanded(.toolDetail(id)))
}
private func card(_ name: String, arguments: [String: AgentJSONValue] = [:]) -> AgentToolCard {
AgentToolCard(
invocation: .init(runID: UUID(), call: .init(id: "call", name: name, arguments: .object(arguments))),
descriptor: .init(
name: name, summary: name, inputSchema: .object([:]), target: .local,
approvalPolicy: .ask,
presentation: .init(
symbol: "brain", activity: .semanticArgument(key: "target", fallback: .skill),
presenterID: "builtin.\(name)", actionKind: .write)), state: .completed)
}
}