Skip to content

Commit 7e1d0e0

Browse files
authored
Expand test coverage (#133)
* Expand test coverage for ToolExecutionDelegate * Add tests for Character extensions * Add tests for generated content convertibility * Add tests for dynamic schema generation * Add tests for instructions * Add tests for generation guides * Add tests for feedback * Make defaultInstructionsAndPromptRepresentationsUseJSONString resilient to guardrail violations * Add tests for JSONDecoder extension * Add tests for prompt * Add tests for transcript * Add tests for URLSession extension * Make defaultInstructionsAndPromptRepresentationsUseJSONString resilient to key reordering
1 parent 531eb67 commit 7e1d0e0

12 files changed

Lines changed: 718 additions & 16 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Testing
2+
3+
@testable import AnyLanguageModel
4+
5+
@Suite("Character Extensions")
6+
struct CharacterExtensionsTests {
7+
private func character(_ string: String) -> Character {
8+
string.first!
9+
}
10+
11+
@Test func containsEmojiScalarDetectsEmoji() {
12+
#expect(character("😀").containsEmojiScalar)
13+
#expect(!character("A").containsEmojiScalar)
14+
}
15+
16+
@Test func isValidJSONStringCharacterAcceptsExpectedCharacters() {
17+
#expect(character("A").isValidJSONStringCharacter)
18+
#expect(character("7").isValidJSONStringCharacter)
19+
#expect(character(" ").isValidJSONStringCharacter)
20+
#expect(character("😀").isValidJSONStringCharacter)
21+
#expect(character("é").isValidJSONStringCharacter)
22+
}
23+
24+
@Test func isValidJSONStringCharacterRejectsDisallowedCharacters() {
25+
let control = Character(UnicodeScalar(0x1F)!)
26+
27+
#expect(!character("\\").isValidJSONStringCharacter)
28+
#expect(!character("\"").isValidJSONStringCharacter)
29+
#expect(!character("").isValidJSONStringCharacter)
30+
#expect(!control.isValidJSONStringCharacter)
31+
#expect(!character("").isValidJSONStringCharacter)
32+
}
33+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
import JSONSchema
3+
import Testing
4+
5+
@testable import AnyLanguageModel
6+
7+
@Suite("ConvertibleToGeneratedContent")
8+
struct ConvertibleToGeneratedContentTests {
9+
@Test func optionalNoneMapsToNullGeneratedContent() {
10+
let value: GeneratedContent? = nil
11+
#expect(value.generatedContent.kind == .null)
12+
}
13+
14+
@Test func optionalSomeMapsToWrappedGeneratedContent() {
15+
let wrapped = GeneratedContent("hello")
16+
let value: GeneratedContent? = wrapped
17+
18+
#expect(value.generatedContent == wrapped)
19+
}
20+
21+
@Test func arrayMapsToArrayGeneratedContent() {
22+
let first = GeneratedContent("a")
23+
let second = GeneratedContent(2)
24+
let array = [first, second]
25+
26+
#expect(array.generatedContent.kind == .array([first, second]))
27+
}
28+
29+
@Test func defaultInstructionsAndPromptRepresentationsUseJSONString() throws {
30+
let content = GeneratedContent(properties: [
31+
"name": "AnyLanguageModel",
32+
"stars": 5,
33+
])
34+
let decoder = JSONDecoder()
35+
let expectedValue = try decoder.decode(JSONValue.self, from: Data(content.jsonString.utf8))
36+
let instructionsValue = try decoder.decode(
37+
JSONValue.self,
38+
from: Data(content.instructionsRepresentation.description.utf8)
39+
)
40+
let promptValue = try decoder.decode(
41+
JSONValue.self,
42+
from: Data(content.promptRepresentation.description.utf8)
43+
)
44+
45+
#expect(instructionsValue == expectedValue)
46+
#expect(promptValue == expectedValue)
47+
}
48+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import AnyLanguageModel
5+
6+
@Suite("DynamicGenerationSchema")
7+
struct DynamicGenerationSchemaTests {
8+
@Test func objectSchemaConvertsToGenerationSchema() throws {
9+
let person = DynamicGenerationSchema(
10+
name: "Person",
11+
description: "A person object",
12+
properties: [
13+
.init(name: "name", description: "Full name", schema: .init(type: String.self)),
14+
.init(name: "age", schema: .init(type: Int.self), isOptional: true),
15+
]
16+
)
17+
18+
let schema = try GenerationSchema(root: person, dependencies: [])
19+
20+
#expect(schema.root == .ref("Person"))
21+
#expect(schema.defs["Person"] != nil)
22+
}
23+
24+
@Test func anyOfSchemaAndStringEnumSchemaConvert() throws {
25+
let text = DynamicGenerationSchema(type: String.self)
26+
let integer = DynamicGenerationSchema(type: Int.self)
27+
let payload = DynamicGenerationSchema(name: "Payload", anyOf: [text, integer])
28+
let color = DynamicGenerationSchema(name: "Color", anyOf: ["red", "green", "blue"])
29+
30+
let payloadSchema = try GenerationSchema(root: payload, dependencies: [])
31+
let colorSchema = try GenerationSchema(root: color, dependencies: [])
32+
33+
#expect(payloadSchema.root == .ref("Payload"))
34+
#expect(colorSchema.root == .ref("Color"))
35+
#expect(payloadSchema.debugDescription.contains("anyOf"))
36+
#expect(colorSchema.debugDescription.contains("string(enum"))
37+
}
38+
39+
@Test func arraySchemaConvertsWithMinAndMax() throws {
40+
let tags = DynamicGenerationSchema(
41+
arrayOf: .init(type: String.self),
42+
minimumElements: 1,
43+
maximumElements: 3
44+
)
45+
let container = DynamicGenerationSchema(
46+
name: "Container",
47+
properties: [.init(name: "tags", schema: tags)]
48+
)
49+
50+
let schema = try GenerationSchema(root: container, dependencies: [])
51+
guard case .object(let objectNode) = schema.defs["Container"] else {
52+
Issue.record("Expected Container definition to be an object")
53+
return
54+
}
55+
guard case .array(let arrayNode) = objectNode.properties["tags"] else {
56+
Issue.record("Expected tags property to be an array")
57+
return
58+
}
59+
#expect(arrayNode.minItems == 1)
60+
#expect(arrayNode.maxItems == 3)
61+
}
62+
63+
@Test func typeInitializerMapsScalarAndReferenceBodies() {
64+
let boolSchema = DynamicGenerationSchema(type: Bool.self)
65+
let stringSchema = DynamicGenerationSchema(type: String.self)
66+
let intSchema = DynamicGenerationSchema(type: Int.self)
67+
let floatSchema = DynamicGenerationSchema(type: Float.self)
68+
let doubleSchema = DynamicGenerationSchema(type: Double.self)
69+
let decimalSchema = DynamicGenerationSchema(type: Decimal.self)
70+
let referenceSchema = DynamicGenerationSchema(type: GeneratedContent.self)
71+
72+
if case .scalar(.bool) = boolSchema.body {} else { Issue.record("Expected bool scalar mapping") }
73+
if case .scalar(.string) = stringSchema.body {} else { Issue.record("Expected string scalar mapping") }
74+
if case .scalar(.integer) = intSchema.body {} else { Issue.record("Expected integer scalar mapping") }
75+
if case .scalar(.number) = floatSchema.body {} else { Issue.record("Expected float number mapping") }
76+
if case .scalar(.number) = doubleSchema.body {} else { Issue.record("Expected double number mapping") }
77+
if case .scalar(.decimal) = decimalSchema.body {} else { Issue.record("Expected decimal mapping") }
78+
79+
if case .reference(let name) = referenceSchema.body {
80+
#expect(name.contains("GeneratedContent"))
81+
} else {
82+
Issue.record("Expected reference mapping for non-scalar Generable type")
83+
}
84+
}
85+
86+
@Test func referenceInitializerCreatesReferenceBody() {
87+
let reference = DynamicGenerationSchema(referenceTo: "Address")
88+
if case .reference(let name) = reference.body {
89+
#expect(name == "Address")
90+
} else {
91+
Issue.record("Expected reference body")
92+
}
93+
}
94+
95+
@Test func duplicateDependencyNamesThrow() {
96+
let dep1 = DynamicGenerationSchema(name: "Shared", properties: [])
97+
let dep2 = DynamicGenerationSchema(name: "Shared", properties: [])
98+
let root = DynamicGenerationSchema(referenceTo: "Shared")
99+
100+
#expect(throws: GenerationSchema.SchemaError.self) {
101+
_ = try GenerationSchema(root: root, dependencies: [dep1, dep2])
102+
}
103+
}
104+
105+
@Test func undefinedReferenceThrows() {
106+
let root = DynamicGenerationSchema(referenceTo: "MissingType")
107+
108+
#expect(throws: GenerationSchema.SchemaError.self) {
109+
_ = try GenerationSchema(root: root, dependencies: [])
110+
}
111+
}
112+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import AnyLanguageModel
5+
6+
@Suite("GenerationGuide")
7+
struct GenerationGuideTests {
8+
@Test func stringFactoriesAreCallable() {
9+
_ = GenerationGuide<String>.constant("fixed")
10+
_ = GenerationGuide<String>.anyOf(["red", "green", "blue"])
11+
_ = GenerationGuide<String>.pattern(#/^[a-z]+$/#)
12+
}
13+
14+
@Test func intFactoriesSetBounds() {
15+
let minimum = GenerationGuide<Int>.minimum(1)
16+
let maximum = GenerationGuide<Int>.maximum(10)
17+
let range = GenerationGuide<Int>.range(2 ... 8)
18+
19+
#expect(minimum.minimum == 1)
20+
#expect(minimum.maximum == nil)
21+
#expect(maximum.minimum == nil)
22+
#expect(maximum.maximum == 10)
23+
#expect(range.minimum == 2)
24+
#expect(range.maximum == 8)
25+
}
26+
27+
@Test func floatFactoriesSetBounds() {
28+
let minimum = GenerationGuide<Float>.minimum(1.25)
29+
let maximum = GenerationGuide<Float>.maximum(9.75)
30+
let range = GenerationGuide<Float>.range(2.5 ... 8.5)
31+
32+
#expect(minimum.minimum == 1.25)
33+
#expect(maximum.maximum == 9.75)
34+
#expect(range.minimum == 2.5)
35+
#expect(range.maximum == 8.5)
36+
}
37+
38+
@Test func doubleFactoriesSetBounds() {
39+
let minimum = GenerationGuide<Double>.minimum(0.1)
40+
let maximum = GenerationGuide<Double>.maximum(0.9)
41+
let range = GenerationGuide<Double>.range(0.2 ... 0.8)
42+
43+
#expect(minimum.minimum == 0.1)
44+
#expect(maximum.maximum == 0.9)
45+
#expect(range.minimum == 0.2)
46+
#expect(range.maximum == 0.8)
47+
}
48+
49+
@Test func decimalFactoriesSetBounds() {
50+
let minimum = GenerationGuide<Decimal>.minimum(Decimal(string: "1.5")!)
51+
let maximum = GenerationGuide<Decimal>.maximum(Decimal(string: "9.5")!)
52+
let range = GenerationGuide<Decimal>.range(Decimal(string: "2.5")! ... Decimal(string: "8.5")!)
53+
54+
#expect(minimum.minimum == 1.5)
55+
#expect(maximum.maximum == 9.5)
56+
#expect(range.minimum == 2.5)
57+
#expect(range.maximum == 8.5)
58+
}
59+
60+
@Test func arrayFactoriesSetCountBounds() {
61+
let minimum = GenerationGuide<[String]>.minimumCount(1)
62+
let maximum = GenerationGuide<[String]>.maximumCount(5)
63+
let range = GenerationGuide<[String]>.count(2 ... 4)
64+
let exact = GenerationGuide<[String]>.count(3)
65+
let element = GenerationGuide<[String]>.element(.constant("x"))
66+
67+
#expect(minimum.minimumCount == 1)
68+
#expect(maximum.maximumCount == 5)
69+
#expect(range.minimumCount == 2)
70+
#expect(range.maximumCount == 4)
71+
#expect(exact.minimumCount == 3)
72+
#expect(exact.maximumCount == 3)
73+
#expect(element.minimumCount == nil)
74+
#expect(element.maximumCount == nil)
75+
}
76+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Testing
2+
3+
@testable import AnyLanguageModel
4+
5+
@Suite("Instructions")
6+
struct InstructionsTests {
7+
@Test func initializesFromStringRepresentable() {
8+
let instructions = Instructions("Be concise.")
9+
#expect(instructions.description == "Be concise.")
10+
}
11+
12+
@Test func initializesFromInstructionsRepresentable() {
13+
let existing = Instructions("Base instructions")
14+
let wrapped = Instructions(existing)
15+
#expect(wrapped.description == "Base instructions")
16+
}
17+
18+
@Test func builderCombinesLines() throws {
19+
let instructions = try Instructions {
20+
"First line"
21+
"Second line"
22+
}
23+
24+
#expect(instructions.description == "First line\nSecond line")
25+
}
26+
27+
@Test func builderSupportsConditionalsAndOptionals() throws {
28+
let includeConditional = true
29+
let includeOptional = false
30+
31+
let instructions = try Instructions {
32+
"Always"
33+
if includeConditional {
34+
"Conditional"
35+
} else {
36+
"Other"
37+
}
38+
if includeOptional {
39+
"Optional"
40+
}
41+
}
42+
43+
#expect(instructions.description == "Always\nConditional")
44+
}
45+
46+
@Test func arrayRepresentationJoinsByNewline() {
47+
let array = ["One", "Two", "Three"]
48+
#expect(array.instructionsRepresentation.description == "One\nTwo\nThree")
49+
}
50+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import AnyLanguageModel
5+
6+
@Suite("JSONDecoder Extensions")
7+
struct JSONDecoderExtensionsTests {
8+
private struct Payload: Decodable {
9+
let date: Date
10+
}
11+
12+
@Test func iso8601WithFractionalSecondsDecodesFractionalDates() throws {
13+
let json = #"{"date":"2026-02-17T12:34:56.789Z"}"#.data(using: .utf8)!
14+
let decoder = JSONDecoder()
15+
decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds
16+
17+
let payload = try decoder.decode(Payload.self, from: json)
18+
19+
let formatter = ISO8601DateFormatter()
20+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
21+
let expected = formatter.date(from: "2026-02-17T12:34:56.789Z")!
22+
#expect(payload.date == expected)
23+
}
24+
25+
@Test func iso8601WithFractionalSecondsFallsBackToNonFractionalDates() throws {
26+
let json = #"{"date":"2026-02-17T12:34:56Z"}"#.data(using: .utf8)!
27+
let decoder = JSONDecoder()
28+
decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds
29+
30+
let payload = try decoder.decode(Payload.self, from: json)
31+
32+
let formatter = ISO8601DateFormatter()
33+
formatter.formatOptions = [.withInternetDateTime]
34+
let expected = formatter.date(from: "2026-02-17T12:34:56Z")!
35+
#expect(payload.date == expected)
36+
}
37+
38+
@Test func iso8601WithFractionalSecondsThrowsDataCorruptedForInvalidDates() {
39+
let json = #"{"date":"not-a-date"}"#.data(using: .utf8)!
40+
let decoder = JSONDecoder()
41+
decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds
42+
43+
do {
44+
_ = try decoder.decode(Payload.self, from: json)
45+
Issue.record("Expected decode to fail for invalid date")
46+
} catch let error as DecodingError {
47+
if case .dataCorrupted = error {
48+
#expect(Bool(true))
49+
} else {
50+
Issue.record("Expected dataCorrupted, got \(error)")
51+
}
52+
} catch {
53+
Issue.record("Expected DecodingError, got \(error)")
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)