mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-09 19:29:41 +00:00
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.
74 lines
3.4 KiB
Swift
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))
|
|
}
|
|
}
|