Skip to content
Merged
33 changes: 33 additions & 0 deletions Tests/AnyLanguageModelTests/CharacterExtensionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Testing

@testable import AnyLanguageModel

@Suite("Character Extensions")
struct CharacterExtensionsTests {
private func character(_ string: String) -> Character {
string.first!
}

@Test func containsEmojiScalarDetectsEmoji() {
#expect(character("😀").containsEmojiScalar)
#expect(!character("A").containsEmojiScalar)
}

@Test func isValidJSONStringCharacterAcceptsExpectedCharacters() {
#expect(character("A").isValidJSONStringCharacter)
#expect(character("7").isValidJSONStringCharacter)
#expect(character(" ").isValidJSONStringCharacter)
#expect(character("😀").isValidJSONStringCharacter)
#expect(character("é").isValidJSONStringCharacter)
}

@Test func isValidJSONStringCharacterRejectsDisallowedCharacters() {
let control = Character(UnicodeScalar(0x1F)!)

#expect(!character("\\").isValidJSONStringCharacter)
#expect(!character("\"").isValidJSONStringCharacter)
#expect(!character("”").isValidJSONStringCharacter)
#expect(!control.isValidJSONStringCharacter)
#expect(!character("】").isValidJSONStringCharacter)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation
import JSONSchema
import Testing

@testable import AnyLanguageModel

@Suite("ConvertibleToGeneratedContent")
struct ConvertibleToGeneratedContentTests {
@Test func optionalNoneMapsToNullGeneratedContent() {
let value: GeneratedContent? = nil
#expect(value.generatedContent.kind == .null)
}

@Test func optionalSomeMapsToWrappedGeneratedContent() {
let wrapped = GeneratedContent("hello")
let value: GeneratedContent? = wrapped

#expect(value.generatedContent == wrapped)
}

@Test func arrayMapsToArrayGeneratedContent() {
let first = GeneratedContent("a")
let second = GeneratedContent(2)
let array = [first, second]

#expect(array.generatedContent.kind == .array([first, second]))
}

@Test func defaultInstructionsAndPromptRepresentationsUseJSONString() throws {
let content = GeneratedContent(properties: [
"name": "AnyLanguageModel",
"stars": 5,
])
let decoder = JSONDecoder()
let expectedValue = try decoder.decode(JSONValue.self, from: Data(content.jsonString.utf8))
let instructionsValue = try decoder.decode(
JSONValue.self,
from: Data(content.instructionsRepresentation.description.utf8)
)
let promptValue = try decoder.decode(
JSONValue.self,
from: Data(content.promptRepresentation.description.utf8)
)

#expect(instructionsValue == expectedValue)
#expect(promptValue == expectedValue)
}
}
112 changes: 112 additions & 0 deletions Tests/AnyLanguageModelTests/DynamicGenerationSchemaTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Foundation
import Testing

@testable import AnyLanguageModel

@Suite("DynamicGenerationSchema")
struct DynamicGenerationSchemaTests {
@Test func objectSchemaConvertsToGenerationSchema() throws {
let person = DynamicGenerationSchema(
name: "Person",
description: "A person object",
properties: [
.init(name: "name", description: "Full name", schema: .init(type: String.self)),
.init(name: "age", schema: .init(type: Int.self), isOptional: true),
]
)

let schema = try GenerationSchema(root: person, dependencies: [])

#expect(schema.root == .ref("Person"))
#expect(schema.defs["Person"] != nil)
}

@Test func anyOfSchemaAndStringEnumSchemaConvert() throws {
let text = DynamicGenerationSchema(type: String.self)
let integer = DynamicGenerationSchema(type: Int.self)
let payload = DynamicGenerationSchema(name: "Payload", anyOf: [text, integer])
let color = DynamicGenerationSchema(name: "Color", anyOf: ["red", "green", "blue"])

let payloadSchema = try GenerationSchema(root: payload, dependencies: [])
let colorSchema = try GenerationSchema(root: color, dependencies: [])

#expect(payloadSchema.root == .ref("Payload"))
#expect(colorSchema.root == .ref("Color"))
#expect(payloadSchema.debugDescription.contains("anyOf"))
#expect(colorSchema.debugDescription.contains("string(enum"))
}

@Test func arraySchemaConvertsWithMinAndMax() throws {
let tags = DynamicGenerationSchema(
arrayOf: .init(type: String.self),
minimumElements: 1,
maximumElements: 3
)
let container = DynamicGenerationSchema(
name: "Container",
properties: [.init(name: "tags", schema: tags)]
)

let schema = try GenerationSchema(root: container, dependencies: [])
guard case .object(let objectNode) = schema.defs["Container"] else {
Issue.record("Expected Container definition to be an object")
return
}
guard case .array(let arrayNode) = objectNode.properties["tags"] else {
Issue.record("Expected tags property to be an array")
return
}
#expect(arrayNode.minItems == 1)
#expect(arrayNode.maxItems == 3)
}

@Test func typeInitializerMapsScalarAndReferenceBodies() {
let boolSchema = DynamicGenerationSchema(type: Bool.self)
let stringSchema = DynamicGenerationSchema(type: String.self)
let intSchema = DynamicGenerationSchema(type: Int.self)
let floatSchema = DynamicGenerationSchema(type: Float.self)
let doubleSchema = DynamicGenerationSchema(type: Double.self)
let decimalSchema = DynamicGenerationSchema(type: Decimal.self)
let referenceSchema = DynamicGenerationSchema(type: GeneratedContent.self)

if case .scalar(.bool) = boolSchema.body {} else { Issue.record("Expected bool scalar mapping") }
if case .scalar(.string) = stringSchema.body {} else { Issue.record("Expected string scalar mapping") }
if case .scalar(.integer) = intSchema.body {} else { Issue.record("Expected integer scalar mapping") }
if case .scalar(.number) = floatSchema.body {} else { Issue.record("Expected float number mapping") }
if case .scalar(.number) = doubleSchema.body {} else { Issue.record("Expected double number mapping") }
if case .scalar(.decimal) = decimalSchema.body {} else { Issue.record("Expected decimal mapping") }

if case .reference(let name) = referenceSchema.body {
#expect(name.contains("GeneratedContent"))
} else {
Issue.record("Expected reference mapping for non-scalar Generable type")
}
}

@Test func referenceInitializerCreatesReferenceBody() {
let reference = DynamicGenerationSchema(referenceTo: "Address")
if case .reference(let name) = reference.body {
#expect(name == "Address")
} else {
Issue.record("Expected reference body")
}
}

@Test func duplicateDependencyNamesThrow() {
let dep1 = DynamicGenerationSchema(name: "Shared", properties: [])
let dep2 = DynamicGenerationSchema(name: "Shared", properties: [])
let root = DynamicGenerationSchema(referenceTo: "Shared")

#expect(throws: GenerationSchema.SchemaError.self) {
_ = try GenerationSchema(root: root, dependencies: [dep1, dep2])
}
}

@Test func undefinedReferenceThrows() {
let root = DynamicGenerationSchema(referenceTo: "MissingType")

#expect(throws: GenerationSchema.SchemaError.self) {
_ = try GenerationSchema(root: root, dependencies: [])
}
}
}
76 changes: 76 additions & 0 deletions Tests/AnyLanguageModelTests/GenerationGuideTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Foundation
import Testing

@testable import AnyLanguageModel

@Suite("GenerationGuide")
struct GenerationGuideTests {
@Test func stringFactoriesAreCallable() {
_ = GenerationGuide<String>.constant("fixed")
_ = GenerationGuide<String>.anyOf(["red", "green", "blue"])
_ = GenerationGuide<String>.pattern(#/^[a-z]+$/#)
}

@Test func intFactoriesSetBounds() {
let minimum = GenerationGuide<Int>.minimum(1)
let maximum = GenerationGuide<Int>.maximum(10)
let range = GenerationGuide<Int>.range(2 ... 8)

#expect(minimum.minimum == 1)
#expect(minimum.maximum == nil)
#expect(maximum.minimum == nil)
#expect(maximum.maximum == 10)
#expect(range.minimum == 2)
#expect(range.maximum == 8)
}

@Test func floatFactoriesSetBounds() {
let minimum = GenerationGuide<Float>.minimum(1.25)
let maximum = GenerationGuide<Float>.maximum(9.75)
let range = GenerationGuide<Float>.range(2.5 ... 8.5)

#expect(minimum.minimum == 1.25)
#expect(maximum.maximum == 9.75)
#expect(range.minimum == 2.5)
#expect(range.maximum == 8.5)
}

@Test func doubleFactoriesSetBounds() {
let minimum = GenerationGuide<Double>.minimum(0.1)
let maximum = GenerationGuide<Double>.maximum(0.9)
let range = GenerationGuide<Double>.range(0.2 ... 0.8)

#expect(minimum.minimum == 0.1)
#expect(maximum.maximum == 0.9)
#expect(range.minimum == 0.2)
#expect(range.maximum == 0.8)
}

@Test func decimalFactoriesSetBounds() {
let minimum = GenerationGuide<Decimal>.minimum(Decimal(string: "1.5")!)
let maximum = GenerationGuide<Decimal>.maximum(Decimal(string: "9.5")!)
let range = GenerationGuide<Decimal>.range(Decimal(string: "2.5")! ... Decimal(string: "8.5")!)

#expect(minimum.minimum == 1.5)
#expect(maximum.maximum == 9.5)
#expect(range.minimum == 2.5)
#expect(range.maximum == 8.5)
}

@Test func arrayFactoriesSetCountBounds() {
let minimum = GenerationGuide<[String]>.minimumCount(1)
let maximum = GenerationGuide<[String]>.maximumCount(5)
let range = GenerationGuide<[String]>.count(2 ... 4)
let exact = GenerationGuide<[String]>.count(3)
let element = GenerationGuide<[String]>.element(.constant("x"))

#expect(minimum.minimumCount == 1)
#expect(maximum.maximumCount == 5)
#expect(range.minimumCount == 2)
#expect(range.maximumCount == 4)
#expect(exact.minimumCount == 3)
#expect(exact.maximumCount == 3)
#expect(element.minimumCount == nil)
#expect(element.maximumCount == nil)
}
}
50 changes: 50 additions & 0 deletions Tests/AnyLanguageModelTests/InstructionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Testing

@testable import AnyLanguageModel

@Suite("Instructions")
struct InstructionsTests {
@Test func initializesFromStringRepresentable() {
let instructions = Instructions("Be concise.")
#expect(instructions.description == "Be concise.")
}

@Test func initializesFromInstructionsRepresentable() {
let existing = Instructions("Base instructions")
let wrapped = Instructions(existing)
#expect(wrapped.description == "Base instructions")
}

@Test func builderCombinesLines() throws {
let instructions = try Instructions {
"First line"
"Second line"
}

#expect(instructions.description == "First line\nSecond line")
}

@Test func builderSupportsConditionalsAndOptionals() throws {
let includeConditional = true
let includeOptional = false

let instructions = try Instructions {
"Always"
if includeConditional {
"Conditional"
} else {
"Other"
}
if includeOptional {
"Optional"
}
}

#expect(instructions.description == "Always\nConditional")
}

@Test func arrayRepresentationJoinsByNewline() {
let array = ["One", "Two", "Three"]
#expect(array.instructionsRepresentation.description == "One\nTwo\nThree")
}
}
56 changes: 56 additions & 0 deletions Tests/AnyLanguageModelTests/JSONDecoderExtensionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import Testing

@testable import AnyLanguageModel

@Suite("JSONDecoder Extensions")
struct JSONDecoderExtensionsTests {
private struct Payload: Decodable {
let date: Date
}

@Test func iso8601WithFractionalSecondsDecodesFractionalDates() throws {
let json = #"{"date":"2026-02-17T12:34:56.789Z"}"#.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds

let payload = try decoder.decode(Payload.self, from: json)

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let expected = formatter.date(from: "2026-02-17T12:34:56.789Z")!
#expect(payload.date == expected)
}

@Test func iso8601WithFractionalSecondsFallsBackToNonFractionalDates() throws {
let json = #"{"date":"2026-02-17T12:34:56Z"}"#.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds

let payload = try decoder.decode(Payload.self, from: json)

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
let expected = formatter.date(from: "2026-02-17T12:34:56Z")!
#expect(payload.date == expected)
}

@Test func iso8601WithFractionalSecondsThrowsDataCorruptedForInvalidDates() {
let json = #"{"date":"not-a-date"}"#.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds

do {
_ = try decoder.decode(Payload.self, from: json)
Issue.record("Expected decode to fail for invalid date")
} catch let error as DecodingError {
if case .dataCorrupted = error {
#expect(Bool(true))
} else {
Issue.record("Expected dataCorrupted, got \(error)")
}
} catch {
Issue.record("Expected DecodingError, got \(error)")
}
}
}
Loading