// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import AgentKit import Foundation import TermindDomain import Testing @testable import TermindApplication @Suite(.timeLimit(.minutes(1))) @MainActor struct AssistantChatCollaborationReviewTests { @Test func modelCatalogUsesOnlyEnabledChatConfigurationAndReportsReadiness() async throws { let fixture = ChatReviewFixture() let disabled = ModelProvider(name: "Disabled", isEnabled: false, models: [AIModel(id: "hidden")]) fixture.ai.upsert(disabled) var provider = fixture.provider provider.models.append(AIModel(id: "embedding", kind: .embedding)) fixture.ai.upsert(provider) let unready = try await fixture.coordinator.configuredChatModels() #expect(Set(unready.map(\.name)) == ["source-model", "target-model"]) #expect(unready.allSatisfy { !$0.isReady }) fixture.ai.setSecret("test-only-secret", for: provider.credentialRef) let ready = try await fixture.coordinator.configuredChatModels() #expect(ready.allSatisfy { $0.isReady }) #expect(ready.allSatisfy { !$0.reference.contains("test-only-secret") }) } @Test func creationInheritsCapturedPolicyWithoutChangingTheManualModelOrCopyingContext() async throws { let fixture = ChatReviewFixture() fixture.source.composer.draft = "Unsent human draft" fixture.source.attach(ContextChip(kind: .file, label: "private.txt", content: "private attachment")) fixture.source.composer.armedSkillName = "source-only-skill" fixture.source.permissionMode = .fullAccess fixture.source.isPlanning = false let captured = fixture.sourceContext(mode: .planning, permission: .askForApproval) let runID = UUID() let result = try await fixture.coordinator.createChat( prompt: "Explicit task summary", model: nil, title: "Independent task", source: captured, sourceRunID: runID, sourceToolCallID: "create-once") let target = try fixture.target(from: result) try await waitUntil { !target.isPreparingAgentRun } #expect(target !== fixture.source) #expect(target.model == fixture.sourceModel) #expect(target.permissionMode == .askForApproval) #expect(target.isPlanning) #expect(fixture.ai.lastSelectedModel == fixture.targetModel) #expect(target.composer.draft.isEmpty) #expect(target.composer.attachments.isEmpty) #expect(target.composer.armedSkillName == nil) let prompt = try #require(target.transcript.first) #expect(prompt.text == "Explicit task summary") #expect(prompt.images.isEmpty && prompt.contextAttachments.isEmpty) #expect(prompt.authoredText == nil) #expect(prompt.origin?.sourceConversationID == fixture.source.id) #expect(prompt.origin?.sourceRunID == runID) #expect(prompt.origin?.sourceToolCallID == "create-once") #expect(prompt.contextSnapshot?.sessionContext == nil) #expect(!target.canEditUserMessage(prompt.id)) #expect(fixture.source.composer.draft == "Unsent human draft") #expect(fixture.source.composer.attachments.count == 1) let restored = try #require(await fixture.repository.loadConversations().first { $0.id == target.id }) #expect(restored.metadata.customTitle == "Independent task") #expect(restored.messages.first?.origin == prompt.origin) let retried = try await fixture.coordinator.createChat( prompt: "Do not send this twice", model: nil, title: nil, source: captured, sourceRunID: runID, sourceToolCallID: "create-once") #expect(try fixture.target(from: retried).id == target.id) #expect(fixture.store.conversations.count == 2) #expect(target.messages.filter { $0.role == .user }.count == 1) } @Test func explicitUnavailableModelFailsWithoutFallbackOrCreation() async throws { let fixture = ChatReviewFixture() let unavailable = AIModelRef(modelProviderID: fixture.provider.id, modelID: "removed") await #expect(throws: AgentToolError.self) { try await fixture.coordinator.createChat( prompt: "task", model: AssistantChatCoordinator.reference(unavailable), title: nil, source: fixture.sourceContext(), sourceRunID: UUID(), sourceToolCallID: "unavailable") } #expect(fixture.store.conversations.map(\.id) == [fixture.source.id]) #expect(await fixture.repository.loadConversations().isEmpty) #expect(fixture.ai.lastSelectedModel == fixture.targetModel) } @Test func idleDeliveryPreservesTargetComposerAndSettingsAndDeduplicatesRetries() async throws { let fixture = ChatReviewFixture() let target = fixture.addTarget() target.permissionMode = .fullAccess target.reasoningEffort = .high target.composer.draft = "Keep writing this" let attachment = ContextChip(kind: .pastedText, label: "Unsent attachment", content: "private draft") target.attach(attachment) target.composer.armedSkillName = "unsent-skill" let runID = UUID() let result = try await fixture.coordinator.sendToChat( id: target.id, message: "Incoming task", source: fixture.sourceContext(), sourceRunID: runID, sourceToolCallID: "send-once") try await waitUntil { !target.isPreparingAgentRun } #expect(result.objectValue?["status"]?.stringValue == "delivered") #expect(target.model == fixture.targetModel) #expect(target.permissionMode == .fullAccess) #expect(target.reasoningEffort == .high) #expect(target.composer.draft == "Keep writing this") #expect(target.composer.attachments.map(\.id) == [attachment.id]) #expect(target.composer.armedSkillName == "unsent-skill") let incoming = try #require(target.messages.first { $0.role == .user }) #expect(incoming.text == "Incoming task") #expect(incoming.contextChips.isEmpty && incoming.images.isEmpty) #expect(incoming.origin?.sourceConversationID == fixture.source.id) #expect(!target.canEditUserMessage(incoming.id)) _ = try await fixture.coordinator.sendToChat( id: target.id, message: "Retry body must not be delivered", source: fixture.sourceContext(), sourceRunID: runID, sourceToolCallID: "send-once") #expect(target.messages.filter { $0.role == .user }.count == 1) } @Test func receiptWriteFailureReleasesPreparationAndPreservesComposer() async throws { let fixture = ChatReviewFixture(receipts: RejectingChatReviewReceipts()) let target = fixture.addTarget() target.composer.draft = "keep" await #expect(throws: ChatReviewWriteFailure.self) { try await fixture.coordinator.sendToChat( id: target.id, message: "Not accepted", source: fixture.sourceContext(), sourceRunID: UUID(), sourceToolCallID: "failed-receipt") } #expect(!target.isPreparingAgentRun) #expect(target.messages.isEmpty) #expect(target.composer.draft == "keep") #expect(await fixture.repository.loadConversations().isEmpty) } @Test func retryAfterTargetDeletionPreservesTheHistoricalDeliveryWithoutRecreatingTheChat() async throws { let fixture = ChatReviewFixture() let runID = UUID() let result = try await fixture.coordinator.createChat( prompt: "Delivered task", model: nil, title: "Deleted target", source: fixture.sourceContext(), sourceRunID: runID, sourceToolCallID: "deleted-target") let target = try fixture.target(from: result) try await waitUntil { !target.isPreparingAgentRun } fixture.store.remove(target.id) let retried = try await fixture.coordinator.createChat( prompt: "Do not recreate", model: nil, title: nil, source: fixture.sourceContext(), sourceRunID: runID, sourceToolCallID: "deleted-target") #expect(retried.objectValue?["status"]?.stringValue == "delivered") #expect(fixture.store.conversations.map(\.id) == [fixture.source.id]) let receipt = try #require( try fixture.receipts.receipt(sourceRunID: runID, sourceToolCallID: "deleted-target")) #expect(receipt.state == .delivered) } @Test func planSenderCannotSteerAnActingRunEvenWhenItsUIToggleChanges() async throws { let fixture = ChatReviewFixture() let target = fixture.addTarget() fixture.startSuspendedRun(target, planning: false) target.isPlanning = true await #expect(throws: AgentToolError.self) { try await fixture.coordinator.sendToChat( id: target.id, message: "Plan only", source: fixture.sourceContext(mode: .planning), sourceRunID: UUID(), sourceToolCallID: "plan-to-acting") } #expect(target.pendingSteering.isEmpty) await target.stopAndWait() } @Test func queuedAgentDeliveryCancelledBeforeTheBoundaryNeverBecomesAHumanDraft() async throws { let fixture = ChatReviewFixture() let target = fixture.addTarget() fixture.startSuspendedRun(target, planning: true) target.isPlanning = false target.composer.draft = "Human draft" let runID = UUID() _ = try await fixture.coordinator.sendToChat( id: target.id, message: "Agent-only message", source: fixture.sourceContext(mode: .planning), sourceRunID: runID, sourceToolCallID: "queued") let pending = try #require(target.pendingSteering.first) #expect(pending.message.origin != nil) target.stop() await target.stopAndWait() #expect(target.composer.draft == "Human draft") #expect(target.pendingSteering.isEmpty) let receipt = try #require(try fixture.receipts.receipt(sourceRunID: runID, sourceToolCallID: "queued")) #expect(receipt.state == .undelivered) } @Test func waitDoesNotReportAnOldCompletedRunWhileTheNextRunIsPreparing() async throws { let fixture = ChatReviewFixture() let target = fixture.addTarget() target.handle(.runState(.completed)) let preparationID = UUID() let didPrepare = target.beginAgentPreparation(id: preparationID, cancellation: { Task {} }) #expect(didPrepare) let waiting = try await fixture.coordinator.waitForChats( ids: [target.id], timeoutMilliseconds: 0, source: fixture.sourceContext(), untilUserInterjects: {}) #expect(waiting.objectValue?["timed_out"]?.boolValue == true) #expect(waiting.objectValue?["chats"]?.arrayValue?.first?.objectValue?["status"]?.stringValue == "preparing") target.finishAgentPreparation(id: preparationID) let completed = try await fixture.coordinator.waitForChats( ids: [target.id], timeoutMilliseconds: 0, source: fixture.sourceContext(), untilUserInterjects: {}) #expect(completed.objectValue?["timed_out"]?.boolValue == false) } @Test func chatListingExcludesArchivesAndReadReturnsBoundedVisibleText() async throws { let fixture = ChatReviewFixture() for index in 0..<24 { let target = fixture.addTarget() target.customTitle = "Task \(index)" } let archived = fixture.addTarget() archived.archivedAt = .now let result = try await fixture.coordinator.listChats(cursor: nil, includeArchived: false) let rows = try #require(result.objectValue?["chats"]?.arrayValue) #expect(rows.count == 20) #expect(rows.allSatisfy { $0.objectValue?["archived"]?.boolValue == false }) #expect(rows.allSatisfy { $0.objectValue?["messages"] == nil }) let next = try #require(result.objectValue?["next_cursor"]?.stringValue) let second = try await fixture.coordinator.listChats(cursor: next, includeArchived: false) #expect(second.objectValue?["chats"]?.arrayValue?.count == 5) let target = fixture.addTarget() target.messages = [ChatMessage(role: .assistant, text: String(repeating: "x", count: 20_000))] let read = try await fixture.coordinator.readChat(id: target.id, cursor: nil) #expect(read.objectValue?["truncated"]?.boolValue == false) let text = try #require(read.objectValue?["messages"]?.arrayValue?.first?.objectValue?["text"]?.stringValue) #expect(text.count == 20_000) } @Test func readChatContinuesAtTheFirstUnreturnedWholeLineWithoutCapturingPages() async throws { let fixture = ChatReviewFixture() let target = fixture.addTarget() let original = String(repeating: String(repeating: "\"", count: 400) + "\n", count: 200) + "last>" target.messages = [ChatMessage(role: .assistant, text: original)] var cursor: String? var received = "" var pages = 0 repeat { let page = try await fixture.coordinator.readChat(id: target.id, cursor: cursor) var raw = AgentToolResult(callID: "call", content: page.encodedString) raw.hasBoundedModelContent = true let projected = await AgentToolOutputProjection().project( raw, invocation: .init(runID: UUID(), call: .init(id: "call", name: "read_chat", arguments: .object([:])))) let model = try #require(projected.modelContent) #expect(model.utf8.count <= 65_536) #expect(projected.outputCapture == nil) let framed = AgentToolResult(callID: "call", content: model, metadata: ["untrusted": .bool(true)]) let payload = try #require(framed.untrustedPayload) let fields = try #require(AgentJSONValue.decode(Data(payload.utf8)).objectValue) received += fields["messages"]?.arrayValue?.compactMap { $0.objectValue?["text"]?.stringValue }.joined() ?? "" let next = fields["next_cursor"]?.stringValue #expect(next == nil || next != cursor) cursor = next pages += 1 } while cursor != nil && pages < 10 #expect(cursor == nil) #expect(pages > 1) #expect(received == original) } @Test func listingBatchesMixedCreatorRelationsOncePerPage() async throws { let collaborationStore = CountingChatReviewStore() let fixture = ChatReviewFixture(receipts: collaborationStore) fixture.source.updatedAt = Date(timeIntervalSince1970: 0) var targets: [ConversationModel] = [] for index in 0..<21 { let target = fixture.addTarget() target.customTitle = "Target \(index)" target.updatedAt = Date(timeIntervalSince1970: Double(10_000 - index)) targets.append(target) } let firstRelation = AssistantChatCreationRelation( conversationID: targets[0].id, sourceConversationID: fixture.source.id, sourceRunID: UUID(), sourceToolCallID: "first-page") let secondRelation = AssistantChatCreationRelation( conversationID: targets[20].id, sourceConversationID: fixture.source.id, sourceRunID: UUID(), sourceToolCallID: "second-page") try collaborationStore.save(firstRelation) try collaborationStore.save(secondRelation) let firstPageIDs = Set(targets.prefix(20).map(\.id)) let secondPageIDs = Set([targets[20].id, fixture.source.id]) let first = try await fixture.coordinator.listChats(cursor: nil, includeArchived: false) let firstRows = try #require(first.objectValue?["chats"]?.arrayValue) #expect(collaborationStore.relationReads == [firstPageIDs]) #expect(try creator(in: firstRows, conversationID: targets[0].id)["kind"] == .string("agent")) #expect(try creator(in: firstRows, conversationID: targets[1].id) == ["kind": .string("user")]) let cursor = try #require(first.objectValue?["next_cursor"]?.stringValue) let second = try await fixture.coordinator.listChats(cursor: cursor, includeArchived: false) let secondRows = try #require(second.objectValue?["chats"]?.arrayValue) #expect(collaborationStore.relationReads == [firstPageIDs, secondPageIDs]) let secondCreator = try creator(in: secondRows, conversationID: targets[20].id) #expect(secondCreator["kind"] == .string("agent")) #expect(secondCreator["chat_id"] == .string(fixture.source.id.uuidString)) #expect(secondCreator["title"] == .string("Source")) #expect(try creator(in: secondRows, conversationID: fixture.source.id) == ["kind": .string("user")]) } @Test func creatorKeepsCreationAuthorityThroughLaterDeliveryRenameAndSourceDeletion() async throws { let fixture = ChatReviewFixture() let creationRunID = UUID() let created = try await fixture.coordinator.createChat( prompt: "Created task", model: nil, title: "Created target", source: fixture.sourceContext(), sourceRunID: creationRunID, sourceToolCallID: "create-target") #expect(created.objectValue?["creator"] == nil) let target = try fixture.target(from: created) try await waitUntil { !target.isPreparingAgentRun } let laterSourceID = UUID() let later = try await fixture.coordinator.sendToChat( id: target.id, message: "Later agent message", source: AgentChatSourceContext( conversationID: laterSourceID, title: "Later source", modelReference: AssistantChatCoordinator.reference(fixture.sourceModel), permissionMode: .askForApproval, mode: .acting), sourceRunID: UUID(), sourceToolCallID: "later-send") #expect(later.objectValue?["creator"] == nil) try await waitUntil { !target.isPreparingAgentRun } #expect(target.messages.compactMap(\.origin).count == 2) var rows = try await listRows(fixture) var creatorInfo = try creator(in: rows, conversationID: target.id) #expect(creatorInfo["chat_id"] == .string(fixture.source.id.uuidString)) #expect(creatorInfo["title"] == .string("Source")) fixture.source.customTitle = "Renamed source" rows = try await listRows(fixture) creatorInfo = try creator(in: rows, conversationID: target.id) #expect(creatorInfo["title"] == .string("Renamed source")) fixture.store.remove(fixture.source.id) rows = try await listRows(fixture) creatorInfo = try creator(in: rows, conversationID: target.id) #expect(creatorInfo["chat_id"] == .string(fixture.source.id.uuidString)) #expect(creatorInfo["title"] == .string("Source")) } @Test func creatorUsesNullWithoutAnExactSavedTitleAndManualForkRemainsUserCreated() async throws { let fixture = ChatReviewFixture() let missingSourceID = UUID() let creationRunID = UUID() let target = fixture.addTarget() target.messages = [ ChatMessage( role: .user, text: "Wrong run", origin: AgentMessageOrigin( sourceConversationID: missingSourceID, sourceRunID: UUID(), sourceToolCallID: "create-without-title", sourceTitle: "Wrong run title")), ChatMessage( role: .user, text: "Wrong tool call", origin: AgentMessageOrigin( sourceConversationID: missingSourceID, sourceRunID: creationRunID, sourceToolCallID: "wrong-tool-call", sourceTitle: "Wrong tool title")), ChatMessage( role: .user, text: "Created without a saved title", origin: AgentMessageOrigin( sourceConversationID: missingSourceID, sourceRunID: creationRunID, sourceToolCallID: "create-without-title")), ] try fixture.receipts.save( AssistantChatCreationRelation( conversationID: target.id, sourceConversationID: missingSourceID, sourceRunID: creationRunID, sourceToolCallID: "create-without-title")) let compactedSourceID = UUID() let compactedRunID = UUID() let displayOnlyTarget = fixture.addTarget() displayOnlyTarget.messages = [ ChatMessage( role: .user, text: "Created before compaction", origin: AgentMessageOrigin( sourceConversationID: compactedSourceID, sourceRunID: compactedRunID, sourceToolCallID: "compacted-creation", sourceTitle: "Compacted source")) ] #expect(displayOnlyTarget.transcript.isEmpty) try fixture.receipts.save( AssistantChatCreationRelation( conversationID: displayOnlyTarget.id, sourceConversationID: compactedSourceID, sourceRunID: compactedRunID, sourceToolCallID: "compacted-creation")) let copiedOrigin = AgentMessageOrigin( sourceConversationID: UUID(), sourceRunID: UUID(), sourceToolCallID: "copied", sourceTitle: "Copied source") let manualSource = ConversationModel() let incoming = manualSource.composeIncomingAgentTurn("Copied task", origin: copiedOrigin) _ = manualSource.appendIncomingAgentTurnContent(incoming) let answer = AgentTranscriptMessage(role: .assistant, text: "Answer") manualSource.handle(.messageFinished(answer)) let fork = try #require(manualSource.forked(throughAssistantMessage: answer.id)) fixture.store.insert(fork) let rows = try await listRows(fixture) let targetCreator = try creator(in: rows, conversationID: target.id) #expect(targetCreator["kind"] == .string("agent")) #expect(targetCreator["chat_id"] == .string(missingSourceID.uuidString)) #expect(targetCreator["title"] == .null) #expect( try creator(in: rows, conversationID: displayOnlyTarget.id)["title"] == .string("Compacted source")) #expect(try creator(in: rows, conversationID: fork.id) == ["kind": .string("user")]) #expect(fork.messages.first?.origin == copiedOrigin) } @Test func relationReadFailurePropagatesFromListChats() async { let fixture = ChatReviewFixture(receipts: RejectingChatReviewRelationReads()) await #expect(throws: ChatReviewReadFailure.self) { try await fixture.coordinator.listChats(cursor: nil, includeArchived: false) } } private func listRows(_ fixture: ChatReviewFixture) async throws -> [AgentJSONValue] { let result = try await fixture.coordinator.listChats( cursor: nil, includeArchived: false) return try #require(result.objectValue?["chats"]?.arrayValue) } private func creator( in rows: [AgentJSONValue], conversationID: UUID ) throws -> [String: AgentJSONValue] { let row = try #require( rows.first { $0.objectValue?["chat_id"] == .string(conversationID.uuidString) }) return try #require(row.objectValue?["creator"]?.objectValue) } private func waitUntil(_ condition: @escaping @MainActor () -> Bool) async throws { let clock = ContinuousClock() let deadline = clock.now.advanced(by: .seconds(2)) while !condition() { guard clock.now < deadline else { throw ChatReviewTimeout() } try await Task.sleep(for: .milliseconds(1)) } } } @MainActor private final class ChatReviewFixture { let provider: ModelProvider let ai: AIConfigurationModel let source = ConversationModel() let store: AssistantConversationStore let repository = InMemoryAgentRunRepository() let receipts: any AssistantChatCollaborationStoring let coordinator: AssistantChatCoordinator let sourceModel: AIModelRef let targetModel: AIModelRef init(receipts: any AssistantChatCollaborationStoring = InMemoryAssistantChatCollaborationStore()) { let provider = ModelProvider( name: "Configured", inferenceURL: "https://example.invalid/v1/chat/completions", credentialRef: "chat-review-key", models: [AIModel(id: "source-model"), AIModel(id: "target-model")]) self.provider = provider sourceModel = AIModelRef(modelProviderID: provider.id, modelID: "source-model") targetModel = AIModelRef(modelProviderID: provider.id, modelID: "target-model") ai = AIConfigurationModel(modelProviders: [provider], lastSelectedModel: targetModel, persistsSecrets: false) let selections = AssistantModelSelectionController(ai: ai, store: InMemoryAssistantModelSelectionStore()) source.model = sourceModel source.customTitle = "Source" store = AssistantConversationStore(conversations: [source]) self.receipts = receipts let controller = AssistantExecutionController( ai: ai, skills: SkillLibraryModel(), repository: repository, modelSelections: selections, authorize: { true }, makeRegistry: { _, _, _, _, _, _, _ in AgentToolRegistry([]) }, title: { _ in "Generated" }) coordinator = AssistantChatCoordinator( conversations: store, ai: ai, modelSelections: selections, collaborationStore: receipts, prepareDelivery: { text, origin, target in controller.prepareIncomingAgentMessage(text, origin: origin, in: target) }, persistDelivery: { prepared, target in try await controller.persistPreparedAgentDelivery(prepared, in: target) }, startDelivery: { prepared, target in controller.startPreparedAgentDelivery(prepared, in: target) }, cancelDelivery: { prepared, target in await controller.cancelPreparedAgentDelivery(prepared, in: target) }) } func sourceContext( mode: AgentRunMode = .acting, permission: AgentPermissionMode = .askForApproval ) -> AgentChatSourceContext { AgentChatSourceContext( conversationID: source.id, title: source.derivedTitle, modelReference: AssistantChatCoordinator.reference(sourceModel), permissionMode: permission, mode: mode) } func addTarget() -> ConversationModel { let target = ConversationModel() target.model = targetModel store.insert(target) return target } func target(from result: AgentJSONValue) throws -> ConversationModel { let raw = try #require(result.objectValue?["chat_id"]?.stringValue) let id = try #require(UUID(uuidString: raw)) return try #require(store.conversation(id: id)) } func startSuspendedRun(_ target: ConversationModel, planning: Bool) { target.start( runtime: AgentRuntime( model: SuspendedChatReviewModel(), tools: [], approval: AgentApprovalBroker(reviewer: nil, manualApproval: { _ in .allow }), repository: repository), request: AgentRunRequest( conversationID: target.id, prompt: "Existing run", permissionMode: .askForApproval, isPlanning: planning), repository: repository) } } private struct SuspendedChatReviewModel: AgentModelStreaming { func stream(_ request: AgentModelRequest) -> AsyncThrowingStream { AsyncThrowingStream { _ in } } } private struct ChatReviewWriteFailure: Error {} private struct ChatReviewReadFailure: Error {} private struct ChatReviewTimeout: Error {} @MainActor private final class CountingChatReviewStore: AssistantChatCollaborationStoring { private let base = InMemoryAssistantChatCollaborationStore() private(set) var relationReads: [Set] = [] var confirmsDeliveryDuringMessagePersistence: Bool { base.confirmsDeliveryDuringMessagePersistence } func receipt(sourceRunID: UUID, sourceToolCallID: String) throws -> AssistantChatDeliveryReceipt? { try base.receipt(sourceRunID: sourceRunID, sourceToolCallID: sourceToolCallID) } func save(_ receipt: AssistantChatDeliveryReceipt) throws { try base.save(receipt) } func updateReceipt( messageID: UUID, state: AgentMessageDeliveryState, failure: String? ) throws { try base.updateReceipt(messageID: messageID, state: state, failure: failure) } func receipts(targetConversationID: UUID) throws -> [AssistantChatDeliveryReceipt] { try base.receipts(targetConversationID: targetConversationID) } func save(_ relation: AssistantChatCreationRelation) throws { try base.save(relation) } func creationRelations( conversationIDs: Set ) throws -> [UUID: AssistantChatCreationRelation] { relationReads.append(conversationIDs) return try base.creationRelations(conversationIDs: conversationIDs) } func deleteRelation(conversationID: UUID) throws { try base.deleteRelation(conversationID: conversationID) } } @MainActor private struct RejectingChatReviewReceipts: AssistantChatCollaborationStoring { let confirmsDeliveryDuringMessagePersistence = false func receipt(sourceRunID: UUID, sourceToolCallID: String) throws -> AssistantChatDeliveryReceipt? { nil } func save(_ receipt: AssistantChatDeliveryReceipt) throws { throw ChatReviewWriteFailure() } func updateReceipt(messageID: UUID, state: AgentMessageDeliveryState, failure: String?) throws {} func receipts(targetConversationID: UUID) throws -> [AssistantChatDeliveryReceipt] { [] } func save(_ relation: AssistantChatCreationRelation) throws { throw ChatReviewWriteFailure() } func creationRelations( conversationIDs: Set ) throws -> [UUID: AssistantChatCreationRelation] { [:] } func deleteRelation(conversationID: UUID) throws {} } @MainActor private struct RejectingChatReviewRelationReads: AssistantChatCollaborationStoring { let confirmsDeliveryDuringMessagePersistence = false func receipt(sourceRunID: UUID, sourceToolCallID: String) throws -> AssistantChatDeliveryReceipt? { nil } func save(_ receipt: AssistantChatDeliveryReceipt) throws {} func updateReceipt(messageID: UUID, state: AgentMessageDeliveryState, failure: String?) throws {} func receipts(targetConversationID: UUID) throws -> [AssistantChatDeliveryReceipt] { [] } func save(_ relation: AssistantChatCreationRelation) throws {} func creationRelations( conversationIDs: Set ) throws -> [UUID: AssistantChatCreationRelation] { throw ChatReviewReadFailure() } func deleteRelation(conversationID: UUID) throws {} }