mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-07 19:06:11 +00:00
93 lines
4.7 KiB
Swift
93 lines
4.7 KiB
Swift
import HapiUI
|
|
import SwiftUI
|
|
import UIKit
|
|
import XCTest
|
|
@testable import Hapi
|
|
@testable import HapiProtocol
|
|
|
|
@MainActor
|
|
final class ToolCallLayoutTests: XCTestCase {
|
|
private let prefix = "cat <<'EOF'\nx"
|
|
private var script: String { prefix + String(repeating: "\nx", count: 100) + "\nEOF" }
|
|
|
|
private func block(name: String, command: String) -> ToolCallBlock {
|
|
// Cover both string commands and Codex's array representation.
|
|
let input: JSONValue = name == "CodexBash"
|
|
? .array([.string("cat"), .string(String(command.dropFirst(4)))])
|
|
: .string(command)
|
|
return ToolCallBlock(
|
|
id: "tool", localId: nil, createdAt: 0, invokedAt: nil,
|
|
durationMs: nil, usage: nil, model: nil,
|
|
tool: ChatToolCall(id: "tool", name: name, state: .completed,
|
|
input: .object(["command": input]), createdAt: 0),
|
|
children: [], meta: nil
|
|
)
|
|
}
|
|
|
|
private func height(of block: ToolCallBlock, size: DynamicTypeSize, details: Bool = false) -> CGFloat {
|
|
let content = details
|
|
? AnyView(ToolCallBody(tool: block.tool, basePath: nil))
|
|
: AnyView(ToolCallBlockView(block: block, basePath: nil))
|
|
let host = UIHostingController(rootView: content
|
|
.hapiTypography()
|
|
.environment(\.dynamicTypeSize, size))
|
|
return host.sizeThatFits(in: CGSize(width: 320, height: CGFloat.greatestFiniteMagnitude)).height
|
|
}
|
|
|
|
func testSummaryPreviewIsOneLineAndRemainsBoundedAtAccessibilitySizes() {
|
|
for name in ["Bash", "CodexBash", "exec_command", "functions.exec_command"] {
|
|
for size in [DynamicTypeSize.large, .accessibility5] {
|
|
let oneLine = height(of: block(name: name, command: "cat <<'EOF'"), size: size)
|
|
let longScript = height(of: block(name: name, command: script), size: size)
|
|
let longerScript = height(of: block(name: name, command: script + script), size: size)
|
|
XCTAssertEqual(longScript, longerScript, accuracy: 1, "\(name), \(size): summary must not grow with output")
|
|
if size == .large {
|
|
XCTAssertEqual(longScript, oneLine, accuracy: 1)
|
|
XCTAssertLessThanOrEqual(longScript, 60)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testFileAndImageSummariesKeepTheFilenameWithoutChangingInspectorInput() {
|
|
for (name, key, action) in [("view_image", "path", "View image"), ("Read", "file_path", "Read file"),
|
|
("Edit", "file_path", "Edit file"), ("mcp__hapi__display_image", "path", "Show image")] {
|
|
var value = block(name: name, command: "")
|
|
let path = "/tmp/review/a-very-long-parent-directory/light-summary.png"
|
|
value.tool.input = .object([key: .string(path)])
|
|
let summary = toolSummaryPresentation(value.tool, basePath: nil)
|
|
XCTAssertEqual(summary.title, "\(action) · light-summary.png")
|
|
XCTAssertNil(summary.subtitle)
|
|
XCTAssertEqual(value.tool.input?[chatKey: key]?.chatString, path)
|
|
XCTAssertEqual(height(of: value, size: .large), 44, accuracy: 1)
|
|
}
|
|
}
|
|
|
|
func testGroupSummaryDiffIgnoresMemberOutputButKeepsVisibleChanges() {
|
|
let first = block(name: "Read", command: "")
|
|
var second = first
|
|
second.id = "second"
|
|
second.tool.id = "second"
|
|
let blocks = buildVisibleChatBlocks([.toolCall(first), .toolCall(second)], options: .init(hasMoreMessages: false))
|
|
guard case .toolGroup(var group) = blocks[0] else { return XCTFail("Expected group") }
|
|
let original = TranscriptRow.group(ToolGroupPresentation(group))
|
|
group.tools[1].tool.result = .string(String(repeating: "streamed output", count: 1000))
|
|
let streamed = TranscriptRow.group(ToolGroupPresentation(group))
|
|
XCTAssertEqual(original, streamed)
|
|
XCTAssertEqual(streamed.spacing(after: original), 8)
|
|
group.summary.errorCount += 1
|
|
XCTAssertNotEqual(original, .group(ToolGroupPresentation(group)))
|
|
}
|
|
|
|
func testInspectorCommandKeepsTheCompleteScript() {
|
|
for name in ["Bash", "CodexBash", "exec_command", "functions.exec_command"] {
|
|
let longBlock = block(name: name, command: script)
|
|
XCTAssertEqual(toolCardPresentation(longBlock.tool, basePath: nil).subtitle, script)
|
|
XCTAssertEqual(chatTerminalCommand(longBlock.tool.input), script)
|
|
let shortHeight = height(of: block(name: name, command: prefix + "\nEOF"), size: .large, details: true)
|
|
let fullHeight = height(of: longBlock, size: .large, details: true)
|
|
XCTAssertGreaterThan(fullHeight, shortHeight + 1000, "\(name): inspector body must retain all 100 extra lines")
|
|
}
|
|
}
|
|
}
|