Files
hapi/ios/Packages/HapiKit/Tests/HapiProtocolTests/ToolGroupIdentityTests.swift
T
weishu c2e3d16b7e feat(native): improve anchored chat scrolling and history loading
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention.

Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main.

Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance.
2026-09-10 16:22:36 +08:00

74 lines
3.4 KiB
Swift

import Testing
@testable import HapiProtocol
@Suite("Native tool group identity")
struct ToolGroupIdentityTests {
private func tool(_ id: String) -> ChatBlock {
.toolCall(ToolCallBlock(
id: id, localId: nil, createdAt: 0, invokedAt: nil,
durationMs: nil, usage: nil, model: nil,
tool: ChatToolCall(id: id, name: "Read", state: .completed, createdAt: 0),
children: [], meta: nil
))
}
private var separator: ChatBlock {
.agentText(AgentTextBlock(
id: "text", localId: nil, createdAt: 0, invokedAt: nil,
durationMs: nil, usage: nil, model: nil, text: "Between tool runs", meta: nil
))
}
private func groups(_ blocks: [ChatBlock], more: Bool = false,
previous: [ToolGroupBlock] = []) -> [ToolGroupBlock] {
buildVisibleChatBlocks(blocks, options: .init(hasMoreMessages: more, previousGroups: previous))
.compactMap { if case .toolGroup(let group) = $0 { group } else { nil } }
}
@Test(arguments: [false, true])
func splittingAGroupCannotReuseItsIdentityTwice(_ more: Bool) {
let tools = ["a", "b", "c", "d"].map(tool)
let previous = groups(tools, more: more)
let split = [tools[0], tools[1], separator, tools[2], tools[3]]
let next = groups(split, more: more, previous: previous)
#expect(next.count == 2)
#expect(Set(next.map(\.id)).count == 2)
#expect(next.filter { $0.id == previous[0].id }.count == 1)
#expect(next.flatMap(\.tools).map(\.id) == ["a", "b", "c", "d"])
#expect(groups(split, more: more, previous: next).map(\.id) == next.map(\.id))
}
@Test func aFallbackCannotCollideWithAnAlreadyClaimedHistoricalId() {
let original = groups([tool("c"), tool("d")]) // tool-group:c
let grown = groups(["a", "b", "c", "d"].map(tool), previous: original)
let split = [tool("a"), tool("b"), separator, tool("c"), tool("d")]
let next = groups(split, previous: grown)
#expect(next[0].id == original[0].id)
#expect(next[1].id != next[0].id)
#expect(groups(split, previous: next).map(\.id) == next.map(\.id))
}
@Test func prependingANewGroupCannotStealALaterGroupsHistoricalId() {
let original = groups(["c", "d", "e"].map(tool)) // tool-group:c
let trimmed = groups([tool("d"), tool("e")], previous: original)
let prepended = [tool("a"), tool("b"), tool("c"), separator, tool("d"), tool("e")]
let next = groups(prepended, more: true, previous: trimmed)
#expect(next.count == 2)
#expect(next[0].id != next[1].id)
#expect(next[1].id == original[0].id)
#expect(groups(prepended, more: true, previous: next).map(\.id) == next.map(\.id))
}
@Test func bothBoundaryCollisionsUseAStableSuffixWithoutDroppingBlocks() {
guard case .agentText(var left) = separator, case .agentText(var right) = separator else { return }
left.id = "tool-group:a"
right.id = "tool-group:b"
let blocks: [ChatBlock] = [.agentText(left), tool("a"), tool("b"), .agentText(right)]
let visible = buildVisibleChatBlocks(blocks, options: .init(hasMoreMessages: false))
let next = groups(blocks)
#expect(visible.count == 3)
#expect(next[0].id == "tool-group:a#2")
#expect(groups(blocks, previous: next).map(\.id) == next.map(\.id))
}
}