// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import AgentKit import Foundation import TermindApplication import TermindPresentation import Testing @MainActor @Suite struct AssistantTranscriptPresentationTests { @Test func toolBatchAndFirstToolWithTheSameSourceIDHaveIndependentDisclosures() { let id = UUID() let state = AssistantTranscriptPresentationState() let batch = AssistantTranscriptDisclosureID.toolBatch(id) let tool = AssistantTranscriptDisclosureID.tool(id) #expect(!state.isExpanded(batch)) #expect(!state.isExpanded(tool)) state.toggleDisclosure(batch) #expect(state.isExpanded(batch)) #expect(!state.isExpanded(tool)) state.toggleDisclosure(tool) #expect(state.isExpanded(batch)) #expect(state.isExpanded(tool)) state.toggleDisclosure(tool) #expect(state.isExpanded(batch)) #expect(!state.isExpanded(tool)) state.toggleDisclosure(batch) state.toggleDisclosure(batch) #expect(state.isExpanded(batch)) #expect(!state.isExpanded(tool)) #expect(state.layoutRevision(for: [id]) > 0) } @Test func aSingleToolDefaultsClosedWithoutBecomingStoredBatchState() { let id = UUID() let state = AssistantTranscriptPresentationState() #expect(!state.isExpanded(.tool(id))) #expect(!state.isExpanded(.toolBatch(id))) state.toggleDisclosure(.tool(id)) #expect(state.isExpanded(.tool(id))) #expect(!state.isExpanded(.toolBatch(id))) } @Test func changingFromOneToolToABatchUsesBatchDefaultsAndKeepsExplicitToolChoices() { let id = UUID() let untouched = AssistantTranscriptPresentationState() #expect(!untouched.isExpanded(.tool(id))) #expect(!untouched.isExpanded(.toolBatch(id))) let explicitlyExpanded = AssistantTranscriptPresentationState() explicitlyExpanded.toggleDisclosure(.tool(id)) #expect(explicitlyExpanded.isExpanded(.tool(id))) #expect(!explicitlyExpanded.isExpanded(.toolBatch(id))) } @Test func completionKeepsVisibleWorkAndPruningDropsOldConversationState() { let id = UUID() let state = AssistantTranscriptPresentationState() let old = work(id: id, state: .running) let completed = work(id: id, state: .completed) state.prepareWorkCompletionTransitions(from: [old], to: [completed], visibleIDs: [id]) #expect(state.isExpanded(.work(id))) #expect(state.collapseWorkOutsideViewport(visibleIDs: [id]).isEmpty) #expect(state.collapseWorkOutsideViewport(visibleIDs: []) == [id]) #expect(!state.isExpanded(.work(id))) state.toggleDisclosure(.work(id)) state.prune(validTimelineIDs: []) #expect(!state.readerDidToggle(.work(id))) #expect(state.layoutRevision(for: [id]) == 0) } @Test func historicalAndManuallyFoldedWorkDoNotAcquirePendingCollapses() { let id = UUID() let state = AssistantTranscriptPresentationState() let old = work(id: id, state: .running) let completed = work(id: id, state: .completed) state.prepareWorkCompletionTransitions(from: [], to: [completed], visibleIDs: [id]) #expect(!state.isExpanded(.work(id))) state.toggleDisclosure(.work(id), default: true) state.prepareWorkCompletionTransitions(from: [old], to: [completed], visibleIDs: [id]) #expect(!state.isExpanded(.work(id))) #expect(!state.hasPendingAutoCollapse(id)) let afterAnswer = work(id: id, state: .completed, afterAnswer: true) let fresh = AssistantTranscriptPresentationState() fresh.prepareWorkCompletionTransitions(from: [old], to: [afterAnswer], visibleIDs: []) #expect(fresh.isExpanded(.work(id), default: true)) } @Test func durationFloorsSecondsAndDoesNotDependOnTheRenderingClock() { let start = Date(timeIntervalSince1970: 10_000) let locale = Locale(identifier: "en") for (elapsed, expected) in [(59.99, "59s"), (60.0, "1m"), (61.9, "1m 1s"), (3_600.0, "1h"), (-1.0, "0s")] { #expect( AgentWorkPresentation.duration(from: start, to: start.addingTimeInterval(elapsed), locale: locale) == expected) } #expect(!AgentWorkPresentation.isExpandedByDefault(for: .completed)) for state: AgentRunState in [.running, .waitingForUser, .waitingForApproval, .failed, .cancelled, .interrupted] { #expect(AgentWorkPresentation.isExpandedByDefault(for: state)) } } @Test func toolStateAndMixedBatchPreserveFailureSemantics() { let states: [(AgentToolCard.State, AgentToolPresentation.BatchTint)] = [ (.proposed, .secondary), (.running, .secondary), (.reviewing, .warning), (.waitingForUser, .warning), (.waitingForApproval, .warning), (.completed, .success), (.failed, .failure), (.denied, .failure), (.interrupted, .secondary), ] for (state, tint) in states { #expect(TermindToolPresentationProfiles.renderer.tint(for: state) == tint) } let completed = card(state: .completed) let failed = card(state: .failed) #expect(TermindToolPresentationProfiles.renderer.batchTint(for: [completed, failed]) == .warning) let title = TermindToolPresentationProfiles.renderer.batchSummary( for: [completed, failed], locale: Locale(identifier: "en")) #expect(title.localizedCaseInsensitiveContains("failed")) #expect( TermindToolPresentationProfiles.renderer.batchTint(for: [completed, card(state: .waitingForApproval)]) == .warning) } @Test func aDirectDenialBeforeTheChannelDrainsKeepsOneDeniedCard() async throws { let value = card(state: .proposed) let conversation = ConversationModel() let request = AgentApprovalRequest( invocation: value.invocation, descriptor: value.descriptor, userIntent: "Inspect") let answer = Task { await conversation.requestApproval(request) } defer { conversation.respondToApproval(request.id, allow: false) } for _ in 0..<100 where conversation.pendingApprovals.isEmpty { await Task.yield() } _ = try #require(conversation.pendingApprovals.first) conversation.respondToApproval(request.id, allow: false) _ = await answer.value conversation.handle(.toolProposed(value.invocation, value.descriptor)) conversation.handle(.approvalRequested(request)) conversation.handle( .toolFinished( value.invocation, .init( callID: value.invocation.call.id, content: "Denied", isError: true ))) #expect(conversation.toolCards.count == 1) #expect(conversation.toolCards.first?.state == .denied) #expect(conversation.transcript.count == 1) } @Test func approvalPolicyDenialFinishesAsDeniedRatherThanFailed() { let value = card(state: .proposed) let conversation = ConversationModel() conversation.handle(.toolProposed(value.invocation, value.descriptor)) conversation.handle( .toolFinished( value.invocation, AgentToolResult( callID: value.invocation.call.id, content: "Denied", isError: true, metadata: [AgentToolResult.approvalPolicyDeniedKey: .bool(true)] ))) #expect(conversation.toolCards.first?.state == .denied) } @Test func resultProjectionUsesTheInjectedPresenter() throws { var value = card(state: .completed) value.result = AgentToolResult(callID: value.invocation.call.id, content: "{\"answer\":42}") let presenter = AgentToolDetailPresenter(id: "test.answer") { input in [.field(.init(label: "Answer", value: input.result.objectValue?["answer"]?.encodedString ?? "missing"))] } let detail = try #require(TermindToolPresentationProfiles.renderer.detail(for: value, presenter: presenter)) #expect(detail.items == [.field(.init(label: "Answer", value: "42"))]) } @Test func reasoningTitlesKeepEmphasisLinksAndImageAltText() { let title = TermindMarkdownParser.inlineTitle( "## **Inspect**\n\n`nginx` [logs](https://example.com) ![alt](https://example.com/p.png)") #expect(String(title.characters) == "Inspect nginx logs alt") #expect(title.runs.contains { $0.inlinePresentationIntent?.contains(.stronglyEmphasized) == true }) #expect(title.runs.contains { $0.inlinePresentationIntent?.contains(.code) == true }) let localLink = TermindMarkdownParser.inlineTitle("[command](file:///tmp/local)") #expect(String(localLink.characters) == "command") #expect(localLink.runs.contains { $0.link == URL(string: "file:///tmp/local") }) } private func work(id: UUID, state: AgentRunState, afterAnswer: Bool = false) -> AssistantTranscriptEntry { .init( id: id, content: .work( .init( run: .init(id: id, state: state, startedAt: .distantPast), items: [], orderedAt: .distantPast, appearsAfterAnswer: afterAnswer ))) } private func card(state: AgentToolCard.State) -> AgentToolCard { .init( invocation: .init( runID: UUID(), call: .init(id: UUID().uuidString, name: "inspect", arguments: .object([:]))), descriptor: .init( name: "inspect", summary: "Inspect", inputSchema: .object([:]), target: .host, approvalPolicy: .approve, concurrency: .parallel), state: state ) } @Test func aReaderToggleSurvivesDefaultStateTransitions() { let id = UUID() let state = AssistantTranscriptPresentationState() #expect(state.isExpanded(.work(id), default: true)) state.toggleDisclosure(.work(id), default: true) #expect(!state.isExpanded(.work(id), default: true)) #expect(state.readerDidToggle(.work(id))) state.followDefault(true, for: .work(id)) #expect(!state.isExpanded(.work(id), default: true)) } @Test func untouchedDisclosureFollowsItsRuntimeDefault() { let id = UUID() let state = AssistantTranscriptPresentationState() state.followDefault(true, for: .work(id)) #expect(state.isExpanded(.work(id))) #expect(!state.readerDidToggle(.work(id))) } @Test func deferredAutoCollapseWaitsForTheCoordinatorAndRespectsReaderOverride() { let id = UUID() let state = AssistantTranscriptPresentationState() state.deferAutoCollapse(id) #expect(state.isExpanded(.work(id))) #expect(state.hasPendingAutoCollapse(id)) #expect(state.completeAutoCollapse(id)) #expect(!state.isExpanded(.work(id))) state.deferAutoCollapse(id) state.toggleDisclosure(.work(id), default: true) #expect(!state.hasPendingAutoCollapse(id)) #expect(!state.completeAutoCollapse(id)) } @Test func reasoningDeadlineIsAbsoluteAndNewMarkdownResetsIt() { let id = UUID() let state = AssistantTranscriptPresentationState() let start = Date(timeIntervalSince1970: 1_000) let first = state.reasoningTitleDeadline( for: id, revision: "first", now: start ) #expect(first == start.addingTimeInterval(3)) #expect( state.reasoningTitleDeadline( for: id, revision: "first", now: start.addingTimeInterval(2) ) == first) #expect( state.reasoningTitleDeadline( for: id, revision: "second", now: start.addingTimeInterval(2) ) == start.addingTimeInterval(5)) } }