// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #if compiler(>=6.4) import Foundation import FoundationModelsFake import Testing #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif @Suite("DataEntry and DataAttachment") struct DataEntryAndAttachmentTests { // MARK: - UTType Tests @Test func utTypeBuiltInConstantsAndLookup() { #expect(UTType.data.identifier == "public.data") #expect(UTType.json.identifier == "public.json") #expect(UTType.plainText.identifier == "public.plain-text") #expect(UTType.utf8PlainText.identifier == "public.utf8-plain-text") #expect(UTType.png.identifier == "public.png") #expect(UTType.jpeg.identifier == "public.jpeg") #expect(UTType.pdf.identifier == "com.adobe.pdf") let jsonByID = UTType("public.json") #expect(jsonByID == .json) #expect(jsonByID?.identifier == "public.json") #expect(UTType("") == nil) #if !canImport(UniformTypeIdentifiers) let custom = UTType("com.example.custom-type") #expect(custom?.identifier == "com.example.custom-type") let imported = UTType(importedAs: "com.example.telemetry", conformingTo: .json) #expect(imported.conforms(to: .json)) #expect(imported.conforms(to: .data)) let unknownExtension = UTType(filenameExtension: "zzfake", conformingTo: .text) #expect(unknownExtension?.conforms(to: .text) == true) #expect(UTType(filenameExtension: "") == nil) #endif let fromMime = UTType(mimeType: "application/json") #expect(fromMime?.conforms(to: .json) == true) let fromExt = UTType(filenameExtension: "png") #expect(fromExt?.conforms(to: .png) == true) } #if compiler(>=6.4.2) // MARK: - Transcript.DataEntry Tests @Test func dataEntryInitializationAndDescription() { let payload = Data([0x01, 0x02, 0x03, 0x04]) let metadata = GeneratedContent(properties: ["source": "sensor"]) let entry = Transcript.DataEntry( id: "data-entry-1", contentType: .json, content: payload, metadata: metadata ) #expect(entry.id == "data-entry-1") #expect(entry.contentType == .json) #expect(entry.content == payload) #expect(entry.metadata == metadata) #expect(entry.description.contains("public.json")) #expect(entry.description.contains("4 bytes")) #expect(Transcript.Entry.data(entry).description == entry.description) #expect(Transcript.Entry.data(entry).id == "data-entry-1") } @Test func dataEntryTranscriptJSONRoundTrip() throws { let payload = Data("hello world".utf8) let metadata = GeneratedContent(properties: ["key": "value", "count": 42]) let entry = Transcript.DataEntry( id: "entry-42", contentType: .plainText, content: payload, metadata: metadata ) let transcript = Transcript(entries: [.data(entry)]) let encodedTranscript = try JSONEncoder().encode(transcript) let decodedTranscript = try JSONDecoder().decode(Transcript.self, from: encodedTranscript) #expect(decodedTranscript == transcript) } @Test func dataWithAnUndeclaredContentTypeRoundTrips() throws { let contentType = UTType(importedAs: "com.example.telemetry") let entry = Transcript.DataEntry( id: "entry-43", contentType: contentType, content: Data([0x2A])) let attachment = Transcript.DataAttachment(contentType: contentType, content: Data([0x2B])) let transcript = Transcript(entries: [ .data(entry), .prompt( Transcript.Prompt( id: "prompt-1", segments: [ .attachment(Transcript.AttachmentSegment(id: "seg-1", content: .data(attachment))) ] ) ), ]) let encoded = try JSONEncoder().encode(transcript) let decoded = try JSONDecoder().decode(Transcript.self, from: encoded) guard case .data(let decodedEntry) = try #require(decoded.first) else { Issue.record("Expected a data entry, got \(String(describing: decoded.first))") return } #expect(decodedEntry.contentType.identifier == "com.example.telemetry") guard case .prompt(let prompt) = decoded.last, case .attachment(let segment) = prompt.segments.first, case .data(let decodedAttachment) = segment.content else { Issue.record("Expected a data attachment, got \(String(describing: decoded.last))") return } #expect(decodedAttachment.contentType.identifier == "com.example.telemetry") } // MARK: - DataEntryRepresentable Tests private struct CustomTelemetryRecord: DataEntryRepresentable, Equatable { var id: String var bytes: Data init(id: String, bytes: Data) { self.id = id self.bytes = bytes } init(_ entry: Transcript.DataEntry) throws { self.id = entry.id self.bytes = entry.content } var transcriptRepresentation: Transcript.DataEntry { Transcript.DataEntry(id: id, contentType: .data, content: bytes) } } @Test func dataEntryRepresentableRoundTrip() throws { let original = CustomTelemetryRecord(id: "telemetry-1", bytes: Data([0xAA, 0xBB])) let dataEntry = original.transcriptRepresentation let restored = try CustomTelemetryRecord(dataEntry) #expect(dataEntry.id == "telemetry-1") #expect(dataEntry.contentType == .data) #expect(restored == original) } // MARK: - Transcript.DataAttachment & DataAttachmentRepresentable Tests private struct CustomPDFPayload: DataAttachmentRepresentable, Equatable { var data: Data init(data: Data) { self.data = data } init(_ attachment: Transcript.DataAttachment) throws { self.data = attachment.content } var transcriptRepresentation: Transcript.DataAttachment { Transcript.DataAttachment( contentType: .pdf, content: data, metadata: GeneratedContent(properties: ["format": "pdf"]) ) } } @Test func dataAttachmentInitializationDescriptionAndTranscriptJSONRoundTrip() throws { let payload = Data([0x25, 0x50, 0x44, 0x46]) let metadata = GeneratedContent(properties: ["pageCount": 1]) let dataAttachment = Transcript.DataAttachment( contentType: .pdf, content: payload, metadata: metadata ) let segment = Transcript.AttachmentSegment( id: "seg-pdf-1", content: .data(dataAttachment), label: "invoice" ) let prompt = Transcript.Prompt(id: "p-1", segments: [.attachment(segment)]) let transcript = Transcript(entries: [.prompt(prompt)]) let encodedTranscript = try JSONEncoder().encode(transcript) let decodedTranscript = try JSONDecoder().decode(Transcript.self, from: encodedTranscript) #expect(segment.description.contains("com.adobe.pdf")) #expect(decodedTranscript == transcript) } /// Wraps a `Transcript.DataAttachment`, which doesn't conform to `DataAttachmentRepresentable`. private struct RawDataPayload: DataAttachmentRepresentable { var attachment: Transcript.DataAttachment init(_ attachment: Transcript.DataAttachment) { self.attachment = attachment } var transcriptRepresentation: Transcript.DataAttachment { attachment } } @Test func attachmentBuilderWithDataAttachmentRepresentableInPromptAndInstructions() async throws { let pdf = CustomPDFPayload(data: Data([0x01, 0x02, 0x03])) let directAttachment = Transcript.DataAttachment( contentType: .json, content: Data("{}".utf8) ) let prompt = Prompt { "Analyze this PDF:" Attachment(pdf).label("doc-1") Attachment(RawDataPayload(directAttachment)).label("json-1") } let instructions = Instructions { "Reference attachment:" Attachment(pdf).label("ref-doc") } let model = MockDataModel(supportedAttachments: [.pdf, .json]) let session = LanguageModelSession(model: model, instructions: instructions) _ = try await session.respond(to: prompt) #expect(session.transcript.count == 3) if case .instructions(let instEntry) = session.transcript[0] { #expect(instEntry.segments.count == 2) if case .attachment(let attSeg) = instEntry.segments[1] { #expect(attSeg.label == "ref-doc") #expect(attSeg.content == .data(pdf.transcriptRepresentation)) let roundTripped = try CustomPDFPayload(pdf.transcriptRepresentation) #expect(roundTripped == pdf) } else { Issue.record("Expected attachment segment in instructions") } } else { Issue.record("Expected instructions entry") } if case .prompt(let promptEntry) = session.transcript[1] { #expect(promptEntry.segments.count == 3) if case .attachment(let firstAtt) = promptEntry.segments[1], case .attachment(let secondAtt) = promptEntry.segments[2] { #expect(firstAtt.label == "doc-1") #expect(firstAtt.content == .data(pdf.transcriptRepresentation)) #expect(secondAtt.label == "json-1") #expect(secondAtt.content == .data(directAttachment)) } else { Issue.record("Expected two attachment segments in prompt") } } else { Issue.record("Expected prompt entry") } } // MARK: - Capability Validation & Channel Event Tests private struct MockDataModel: LanguageModel { struct Executor: LanguageModelExecutor { typealias Model = MockDataModel struct Configuration: Hashable, Sendable {} init(configuration: Configuration) throws {} func respond( to request: LanguageModelExecutorGenerationRequest, model: MockDataModel, streamingInto channel: LanguageModelExecutorGenerationChannel ) async throws { if model.emitsDataEntries { await channel.send( .data( entryID: "emitted-data-1", action: .update( contentType: .plainText, content: Data("first".utf8), metadata: GeneratedContent(properties: ["step": 1]) ) ) ) await channel.send( .data( entryID: "emitted-data-1", action: .update( contentType: .json, content: Data("{\"updated\":true}".utf8), metadata: GeneratedContent(properties: ["step": 2]) ) ) ) await channel.send( .data( action: .update( contentType: .data, content: Data([0xFF]) ) ) ) } await channel.send(.response(action: .appendText("Done", tokenCount: 1))) } } var supportedAttachments: Set = [] var supportedEntries: Set = [] var emitsDataEntries: Bool = false var capabilities: LanguageModelCapabilities { LanguageModelCapabilities([]) } var executorConfiguration: Executor.Configuration { Executor.Configuration() } func supportsDataAttachmentType(_ type: UTType) async throws -> Bool { supportedAttachments.contains(type) } func supportsDataEntryType(_ type: UTType) async throws -> Bool { supportedEntries.contains(type) } } @Test func sessionRejectsUnsupportedDataAttachmentInPrompt() async throws { let model = MockDataModel(supportedAttachments: [.png]) let session = LanguageModelSession(model: model) let jsonAttachment = Transcript.DataAttachment( contentType: .json, content: Data("{}".utf8) ) await #expect(throws: LanguageModelError.self) { try await session.respond { "Read JSON:" Attachment(RawDataPayload(jsonAttachment)) } } let supportedSession = LanguageModelSession( model: MockDataModel(supportedAttachments: [.json]) ) let response = try await supportedSession.respond { "Read JSON:" Attachment(RawDataPayload(jsonAttachment)) } #expect(response.content == "Done") } @Test func sessionRejectsUnsupportedDataEntryInTranscriptForRespondAndStream() async throws { let dataEntry = Transcript.DataEntry( id: "d-1", contentType: .pdf, content: Data([0x01, 0x02]) ) let transcript = Transcript(entries: [.data(dataEntry)]) let unsupportedSession = LanguageModelSession( model: MockDataModel(supportedEntries: []), transcript: transcript ) await #expect(throws: LanguageModelError.self) { try await unsupportedSession.respond(to: "Hello") } let stream = unsupportedSession.streamResponse(to: "Hello") await #expect(throws: LanguageModelError.self) { _ = try await stream.collect() } let supportedSession = LanguageModelSession( model: MockDataModel(supportedEntries: [.pdf]), transcript: transcript ) let response = try await supportedSession.respond(to: "Hello") #expect(response.content == "Done") } @Test func channelDataEventCreatesAndUpdatesDataEntryInPlace() async throws { let model = MockDataModel(emitsDataEntries: true) let session = LanguageModelSession(model: model) let response = try await session.respond(to: "Emit data") #expect(response.content == "Done") let dataEntries = session.transcript.compactMap { entry -> Transcript.DataEntry? in if case .data(let d) = entry { return d } return nil } #expect(dataEntries.count == 2) #expect(dataEntries[0].id == "emitted-data-1") #expect(dataEntries[0].contentType == .json) #expect(dataEntries[0].content == Data("{\"updated\":true}".utf8)) #expect(dataEntries[0].metadata == GeneratedContent(properties: ["step": 2])) #expect(dataEntries[1].contentType == .data) #expect(dataEntries[1].content == Data([0xFF])) } #endif // compiler(>=6.4.2) } #endif // compiler(>=6.4)