119 lines
5.9 KiB
Swift
119 lines
5.9 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AgentKit
|
|
import AppKit
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindPresentation
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite @MainActor
|
|
struct RemoteOutputToolPresentationTests {
|
|
@Test(arguments: ["ssh_exec_run", "ssh_exec_output", "sftp_read"], [380.0, 720.0])
|
|
func longOutputExpandsCompletelyWithoutCopyControls(_ name: String, _ width: Double) throws {
|
|
let output =
|
|
(0..<180).map { "line \($0): " + String(repeating: "output ", count: 8) }.joined(separator: "\n")
|
|
+ (name == "sftp_read" ? "" : "\ncombined-output-last-line")
|
|
let value = card(name, output: output)
|
|
let presenter = try #require(AppDependencies.toolPresenter(for: value.descriptor))
|
|
for language in [AppLanguage.english, .simplifiedChinese] {
|
|
let detail = try #require(
|
|
TermindToolPresentationProfiles.renderer.detail(
|
|
for: value, presenter: presenter, locale: language.locale))
|
|
let preview = AgentToolDetailPreview(detail)
|
|
#expect(preview.isShortened)
|
|
#expect(preview.fullText.contains(output))
|
|
#expect(preview.fullItems == detail.items)
|
|
if name != "sftp_read" {
|
|
#expect(preview.fullText.contains("combined-output-last-line"))
|
|
}
|
|
|
|
let state = TranscriptPresentationState()
|
|
state.disclosures.followDefault(true, for: .tool(value.id))
|
|
let row = TranscriptNativeRow.make(for: .tool)
|
|
func configure() {
|
|
row.configure(
|
|
entry: .init(id: value.id, content: .tool(value)), proposedWidth: width,
|
|
configuration: .init(
|
|
isCompact: true, showsTerminalActions: false, isConversationStreaming: false,
|
|
locale: language.locale, reduceMotion: true, colorScheme: .light),
|
|
actions: .init(), presentation: state, markdownCache: TranscriptMarkdownCache(limit: 4),
|
|
measurement: false, invalidate: { _, _ in })
|
|
}
|
|
func button(_ identifier: String) throws -> NSButton {
|
|
try #require(
|
|
descendants(row).first { $0.identifier?.rawValue == identifier } as? NSButton)
|
|
}
|
|
func expectOnlyExpansionControl() throws {
|
|
let body = try #require(
|
|
descendants(row).first { $0.identifier?.rawValue == "AssistantTool.CompactDetails" })
|
|
let buttons = descendants(body).compactMap { $0 as? NSButton }
|
|
#expect(buttons.map { $0.identifier?.rawValue } == ["AssistantTool.More"])
|
|
}
|
|
configure()
|
|
let compactHeight = row.measuredHeight(for: width)
|
|
#expect(!descendants(row).compactMap { $0 as? NSTextField }.contains { $0.stringValue == output })
|
|
try expectOnlyExpansionControl()
|
|
try button("AssistantTool.More").performClick(nil)
|
|
configure()
|
|
#expect(state.disclosures.isExpanded(.toolDetail(value.id)))
|
|
#expect(row.measuredHeight(for: width) > compactHeight)
|
|
#expect(descendants(row).compactMap { $0 as? NSTextField }.contains { $0.stringValue == output })
|
|
try expectOnlyExpansionControl()
|
|
try button("AssistantTool.More").performClick(nil)
|
|
configure()
|
|
#expect(!state.disclosures.isExpanded(.toolDetail(value.id)))
|
|
#expect(abs(row.measuredHeight(for: width) - compactHeight) < 0.5)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func displayFoldingNeverCreatesAMarkerAndActualMarkersAreReusedVerbatim() throws {
|
|
var value = card("ssh_exec_output", output: String(repeating: "line\n", count: 100))
|
|
let presenter = try #require(AppDependencies.toolPresenter(for: value.descriptor))
|
|
let detail = try #require(
|
|
TermindToolPresentationProfiles.renderer.detail(
|
|
for: value, presenter: presenter, locale: Locale(identifier: "zh-Hans")))
|
|
#expect(AgentToolDetailPreview(detail).isShortened)
|
|
#expect(TermindToolPresentationProfiles.renderer.truncationNotice(for: value) == nil)
|
|
value.result?.isTruncated = true
|
|
#expect(TermindToolPresentationProfiles.renderer.truncationNotice(for: value) == nil)
|
|
let marker = AgentTruncateMarker.render(
|
|
ranges: [10...20], totalLines: 20,
|
|
capture: .init(path: "chat/outputs/ssh-exec-output-test.txt", bytes: 100, lines: 20))
|
|
value.result?.truncateMarker = marker
|
|
#expect(TermindToolPresentationProfiles.renderer.truncationNotice(for: value) == marker)
|
|
}
|
|
|
|
private func card(_ name: String, output: String) -> AgentToolCard {
|
|
let result: AgentJSONValue =
|
|
name == "sftp_read"
|
|
? .object(["path": .string("/tmp/output.log"), "content": .string(output)])
|
|
: .object([
|
|
"handle": .string(UUID().uuidString), "status": .string("exited"), "exit_code": .number(0),
|
|
"output": .string(output), "stdin_closed": .bool(true),
|
|
])
|
|
return AgentToolCard(
|
|
invocation: .init(
|
|
runID: UUID(),
|
|
call: .init(
|
|
id: "call", name: name,
|
|
arguments: .object(
|
|
name == "ssh_exec_run" ? ["command": .string("cat /tmp/output.log")] : [:]))),
|
|
descriptor: .init(
|
|
name: name, summary: name, inputSchema: .object([:]), target: .host, approvalPolicy: .ask,
|
|
presentation: .init(
|
|
symbol: "terminal", activity: .semanticLabel(.commandOutput), presenterID: "builtin.\(name)",
|
|
actionKind: .read)),
|
|
state: .completed,
|
|
result: .init(callID: "call", content: result.encodedString))
|
|
}
|
|
|
|
private func descendants(_ view: NSView) -> [NSView] {
|
|
view.subviews.flatMap { [$0] + descendants($0) }
|
|
}
|
|
}
|