@struktur/sdk 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +4111 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers.js +492 -0
- package/dist/parsers.js.map +1 -0
- package/dist/strategies.js +2435 -0
- package/dist/strategies.js.map +1 -0
- package/package.json +24 -12
- package/src/agent-cli-integration.test.ts +0 -47
- package/src/agent-export.test.ts +0 -17
- package/src/agent-tool-labels.test.ts +0 -50
- package/src/artifacts/AGENTS.md +0 -16
- package/src/artifacts/fileToArtifact.test.ts +0 -37
- package/src/artifacts/fileToArtifact.ts +0 -44
- package/src/artifacts/input.test.ts +0 -243
- package/src/artifacts/input.ts +0 -360
- package/src/artifacts/providers.test.ts +0 -19
- package/src/artifacts/providers.ts +0 -7
- package/src/artifacts/urlToArtifact.test.ts +0 -23
- package/src/artifacts/urlToArtifact.ts +0 -19
- package/src/auth/AGENTS.md +0 -11
- package/src/auth/config.test.ts +0 -132
- package/src/auth/config.ts +0 -186
- package/src/auth/tokens.test.ts +0 -58
- package/src/auth/tokens.ts +0 -229
- package/src/chunking/AGENTS.md +0 -11
- package/src/chunking/ArtifactBatcher.test.ts +0 -22
- package/src/chunking/ArtifactBatcher.ts +0 -110
- package/src/chunking/ArtifactSplitter.test.ts +0 -38
- package/src/chunking/ArtifactSplitter.ts +0 -151
- package/src/debug/AGENTS.md +0 -79
- package/src/debug/logger.test.ts +0 -244
- package/src/debug/logger.ts +0 -211
- package/src/extract.test.ts +0 -22
- package/src/extract.ts +0 -150
- package/src/fields.test.ts +0 -681
- package/src/fields.ts +0 -246
- package/src/index.test.ts +0 -20
- package/src/index.ts +0 -110
- package/src/llm/AGENTS.md +0 -9
- package/src/llm/LLMClient.test.ts +0 -394
- package/src/llm/LLMClient.ts +0 -264
- package/src/llm/RetryingRunner.test.ts +0 -174
- package/src/llm/RetryingRunner.ts +0 -270
- package/src/llm/message.test.ts +0 -42
- package/src/llm/message.ts +0 -47
- package/src/llm/models.test.ts +0 -82
- package/src/llm/models.ts +0 -190
- package/src/llm/resolveModel.ts +0 -86
- package/src/merge/AGENTS.md +0 -6
- package/src/merge/Deduplicator.test.ts +0 -108
- package/src/merge/Deduplicator.ts +0 -45
- package/src/merge/SmartDataMerger.test.ts +0 -177
- package/src/merge/SmartDataMerger.ts +0 -56
- package/src/parsers/AGENTS.md +0 -58
- package/src/parsers/collect.test.ts +0 -56
- package/src/parsers/collect.ts +0 -31
- package/src/parsers/index.ts +0 -6
- package/src/parsers/mime.test.ts +0 -91
- package/src/parsers/mime.ts +0 -137
- package/src/parsers/npm.ts +0 -26
- package/src/parsers/pdf.test.ts +0 -394
- package/src/parsers/pdf.ts +0 -194
- package/src/parsers/runner.test.ts +0 -95
- package/src/parsers/runner.ts +0 -177
- package/src/parsers/types.ts +0 -29
- package/src/prompts/AGENTS.md +0 -8
- package/src/prompts/DeduplicationPrompt.test.ts +0 -41
- package/src/prompts/DeduplicationPrompt.ts +0 -37
- package/src/prompts/ExtractorPrompt.test.ts +0 -21
- package/src/prompts/ExtractorPrompt.ts +0 -72
- package/src/prompts/ParallelMergerPrompt.test.ts +0 -8
- package/src/prompts/ParallelMergerPrompt.ts +0 -37
- package/src/prompts/SequentialExtractorPrompt.test.ts +0 -24
- package/src/prompts/SequentialExtractorPrompt.ts +0 -82
- package/src/prompts/formatArtifacts.test.ts +0 -39
- package/src/prompts/formatArtifacts.ts +0 -46
- package/src/strategies/AGENTS.md +0 -6
- package/src/strategies/DoublePassAutoMergeStrategy.test.ts +0 -53
- package/src/strategies/DoublePassAutoMergeStrategy.ts +0 -410
- package/src/strategies/DoublePassStrategy.test.ts +0 -48
- package/src/strategies/DoublePassStrategy.ts +0 -266
- package/src/strategies/ParallelAutoMergeStrategy.test.ts +0 -152
- package/src/strategies/ParallelAutoMergeStrategy.ts +0 -345
- package/src/strategies/ParallelStrategy.test.ts +0 -61
- package/src/strategies/ParallelStrategy.ts +0 -208
- package/src/strategies/SequentialAutoMergeStrategy.test.ts +0 -66
- package/src/strategies/SequentialAutoMergeStrategy.ts +0 -325
- package/src/strategies/SequentialStrategy.test.ts +0 -53
- package/src/strategies/SequentialStrategy.ts +0 -142
- package/src/strategies/SimpleStrategy.test.ts +0 -46
- package/src/strategies/SimpleStrategy.ts +0 -94
- package/src/strategies/concurrency.test.ts +0 -16
- package/src/strategies/concurrency.ts +0 -14
- package/src/strategies/index.test.ts +0 -20
- package/src/strategies/index.ts +0 -7
- package/src/strategies/utils.test.ts +0 -76
- package/src/strategies/utils.ts +0 -95
- package/src/tokenization.test.ts +0 -119
- package/src/tokenization.ts +0 -71
- package/src/types.test.ts +0 -25
- package/src/types.ts +0 -174
- package/src/validation/AGENTS.md +0 -7
- package/src/validation/validator.test.ts +0 -204
- package/src/validation/validator.ts +0 -90
- package/tsconfig.json +0 -22
package/src/fields.test.ts
DELETED
|
@@ -1,681 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
parseFieldsString,
|
|
4
|
-
buildSchemaFromParsedFields,
|
|
5
|
-
buildSchemaFromFields,
|
|
6
|
-
} from "./fields";
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// parseFieldsString — scalars (positive)
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
describe("parseFieldsString — scalars", () => {
|
|
13
|
-
test("single field defaults to string", () => {
|
|
14
|
-
expect(parseFieldsString("title")).toEqual([
|
|
15
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
16
|
-
]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("two fields without types", () => {
|
|
20
|
-
expect(parseFieldsString("title,description")).toEqual([
|
|
21
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
22
|
-
{ name: "description", kind: "scalar", type: "string" },
|
|
23
|
-
]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("explicit :string is same as omitting type", () => {
|
|
27
|
-
expect(parseFieldsString("title:string")).toEqual([
|
|
28
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("spaces around comma — no types", () => {
|
|
33
|
-
expect(parseFieldsString("title, description")).toEqual([
|
|
34
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
35
|
-
{ name: "description", kind: "scalar", type: "string" },
|
|
36
|
-
]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("spaces around comma and colon", () => {
|
|
40
|
-
expect(parseFieldsString("title , price: number")).toEqual([
|
|
41
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
42
|
-
{ name: "price", kind: "scalar", type: "number" },
|
|
43
|
-
]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("spaces around colon only", () => {
|
|
47
|
-
expect(parseFieldsString("title, price:number")).toEqual([
|
|
48
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
49
|
-
{ name: "price", kind: "scalar", type: "number" },
|
|
50
|
-
]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("all four scalar types", () => {
|
|
54
|
-
expect(parseFieldsString("a:string,b:number,c:boolean,d:integer")).toEqual([
|
|
55
|
-
{ name: "a", kind: "scalar", type: "string" },
|
|
56
|
-
{ name: "b", kind: "scalar", type: "number" },
|
|
57
|
-
{ name: "c", kind: "scalar", type: "boolean" },
|
|
58
|
-
{ name: "d", kind: "scalar", type: "integer" },
|
|
59
|
-
]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("bool is an alias for boolean", () => {
|
|
63
|
-
expect(parseFieldsString("active:bool")).toEqual([
|
|
64
|
-
{ name: "active", kind: "scalar", type: "boolean" },
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("float is an alias for number", () => {
|
|
69
|
-
expect(parseFieldsString("ratio:float")).toEqual([
|
|
70
|
-
{ name: "ratio", kind: "scalar", type: "number" },
|
|
71
|
-
]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("int resolves to int (triggers multipleOf:1 in schema)", () => {
|
|
75
|
-
expect(parseFieldsString("count:int")).toEqual([
|
|
76
|
-
{ name: "count", kind: "scalar", type: "int" },
|
|
77
|
-
]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("all aliases together", () => {
|
|
81
|
-
expect(parseFieldsString("n:int,r:float,f:bool")).toEqual([
|
|
82
|
-
{ name: "n", kind: "scalar", type: "int" },
|
|
83
|
-
{ name: "r", kind: "scalar", type: "number" },
|
|
84
|
-
{ name: "f", kind: "scalar", type: "boolean" },
|
|
85
|
-
]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("many fields, all untyped", () => {
|
|
89
|
-
expect(parseFieldsString("id,name,email,phone,address")).toEqual([
|
|
90
|
-
{ name: "id", kind: "scalar", type: "string" },
|
|
91
|
-
{ name: "name", kind: "scalar", type: "string" },
|
|
92
|
-
{ name: "email", kind: "scalar", type: "string" },
|
|
93
|
-
{ name: "phone", kind: "scalar", type: "string" },
|
|
94
|
-
{ name: "address", kind: "scalar", type: "string" },
|
|
95
|
-
]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("leading/trailing whitespace in entire string is ignored", () => {
|
|
99
|
-
expect(parseFieldsString(" title ")).toEqual([
|
|
100
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
101
|
-
]);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// parseFieldsString — scalars (error cases)
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
describe("parseFieldsString — scalar errors", () => {
|
|
110
|
-
test("empty string throws with helpful message", () => {
|
|
111
|
-
expect(() => parseFieldsString("")).toThrow("Fields string must not be empty.");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("whitespace-only string throws", () => {
|
|
115
|
-
expect(() => parseFieldsString(" ")).toThrow("Fields string must not be empty.");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("unknown scalar type names the offending type and field", () => {
|
|
119
|
-
const err = () => parseFieldsString("title:object");
|
|
120
|
-
expect(err).toThrow(/Unknown type "object"/);
|
|
121
|
-
expect(err).toThrow(/field "title"/);
|
|
122
|
-
expect(err).toThrow(/bool, boolean, float, int, integer, number, string/);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("unknown type also hints at complex types", () => {
|
|
126
|
-
expect(() => parseFieldsString("x:map")).toThrow(/enum\{/);
|
|
127
|
-
expect(() => parseFieldsString("x:map")).toThrow(/array\{/);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("leading comma — empty field name before it", () => {
|
|
131
|
-
expect(() => parseFieldsString(",foo")).toThrow(/Empty field name/);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("trailing comma is silently ignored", () => {
|
|
135
|
-
// The brace-depth splitter drops empty trailing tokens — same as most CLIs.
|
|
136
|
-
expect(parseFieldsString("foo,")).toEqual([
|
|
137
|
-
{ name: "foo", kind: "scalar", type: "string" },
|
|
138
|
-
]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("consecutive commas — empty field name between them", () => {
|
|
142
|
-
expect(() => parseFieldsString("foo,,bar")).toThrow(/Empty field name/);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("colon with no type mentions the field name", () => {
|
|
146
|
-
const err = () => parseFieldsString("title:");
|
|
147
|
-
expect(err).toThrow(/Empty type/);
|
|
148
|
-
expect(err).toThrow(/"title"/);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("colon with whitespace-only type is treated as empty", () => {
|
|
152
|
-
expect(() => parseFieldsString("title: ")).toThrow(/Empty type/);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("field name with only a colon and no name", () => {
|
|
156
|
-
expect(() => parseFieldsString(":string")).toThrow(/Empty field name/);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// parseFieldsString — enums (positive)
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
describe("parseFieldsString — enums", () => {
|
|
165
|
-
test("two-value enum", () => {
|
|
166
|
-
expect(parseFieldsString("status:enum{draft|published}")).toEqual([
|
|
167
|
-
{ name: "status", kind: "enum", values: ["draft", "published"] },
|
|
168
|
-
]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("three-value enum with numbers (user example)", () => {
|
|
172
|
-
expect(parseFieldsString("wtf:enum{abc|def|123}")).toEqual([
|
|
173
|
-
{ name: "wtf", kind: "enum", values: ["abc", "def", "123"] },
|
|
174
|
-
]);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("spaces around pipe values are trimmed", () => {
|
|
178
|
-
expect(parseFieldsString("status:enum{ draft | published | archived }")).toEqual([
|
|
179
|
-
{ name: "status", kind: "enum", values: ["draft", "published", "archived"] },
|
|
180
|
-
]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("enum field preceded by plain field", () => {
|
|
184
|
-
expect(parseFieldsString("name, status:enum{active|inactive}")).toEqual([
|
|
185
|
-
{ name: "name", kind: "scalar", type: "string" },
|
|
186
|
-
{ name: "status", kind: "enum", values: ["active", "inactive"] },
|
|
187
|
-
]);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("enum field followed by plain field", () => {
|
|
191
|
-
expect(parseFieldsString("status:enum{a|b}, title")).toEqual([
|
|
192
|
-
{ name: "status", kind: "enum", values: ["a", "b"] },
|
|
193
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
194
|
-
]);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("enum sandwiched between other fields", () => {
|
|
198
|
-
expect(parseFieldsString("id, role:enum{admin|user|guest}, name")).toEqual([
|
|
199
|
-
{ name: "id", kind: "scalar", type: "string" },
|
|
200
|
-
{ name: "role", kind: "enum", values: ["admin", "user", "guest"] },
|
|
201
|
-
{ name: "name", kind: "scalar", type: "string" },
|
|
202
|
-
]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("enum values that look like numbers", () => {
|
|
206
|
-
expect(parseFieldsString("code:enum{1|2|3}")).toEqual([
|
|
207
|
-
{ name: "code", kind: "enum", values: ["1", "2", "3"] },
|
|
208
|
-
]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("enum values with hyphens and underscores", () => {
|
|
212
|
-
expect(parseFieldsString("type:enum{in-progress|not_started|done}")).toEqual([
|
|
213
|
-
{ name: "type", kind: "enum", values: ["in-progress", "not_started", "done"] },
|
|
214
|
-
]);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// parseFieldsString — enums (error cases)
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
|
|
222
|
-
describe("parseFieldsString — enum errors", () => {
|
|
223
|
-
test("single value enum throws and mentions field name", () => {
|
|
224
|
-
const err = () => parseFieldsString("x:enum{only}");
|
|
225
|
-
expect(err).toThrow(/at least two/);
|
|
226
|
-
expect(err).toThrow(/"x"/);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test("empty braces throws", () => {
|
|
230
|
-
const err = () => parseFieldsString("x:enum{}");
|
|
231
|
-
expect(err).toThrow(/at least two/);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("braces with only whitespace throws", () => {
|
|
235
|
-
expect(() => parseFieldsString("x:enum{ }")).toThrow(/at least two/);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("enum with only pipe separators and no values throws", () => {
|
|
239
|
-
expect(() => parseFieldsString("x:enum{|}")).toThrow(/at least two/);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("missing closing brace gives Unmatched braces error", () => {
|
|
243
|
-
expect(() => parseFieldsString("x:enum{a|b")).toThrow(/Unmatched braces/);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
// parseFieldsString — arrays (positive)
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
describe("parseFieldsString — arrays", () => {
|
|
252
|
-
test("array of string", () => {
|
|
253
|
-
expect(parseFieldsString("tags:array{string}")).toEqual([
|
|
254
|
-
{ name: "tags", kind: "array", items: "string" },
|
|
255
|
-
]);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("array shorthand defaults to string", () => {
|
|
259
|
-
expect(parseFieldsString("tags:array")).toEqual([
|
|
260
|
-
{ name: "tags", kind: "array", items: "string" },
|
|
261
|
-
]);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("array of number", () => {
|
|
265
|
-
expect(parseFieldsString("scores:array{number}")).toEqual([
|
|
266
|
-
{ name: "scores", kind: "array", items: "number" },
|
|
267
|
-
]);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("array of boolean", () => {
|
|
271
|
-
expect(parseFieldsString("flags:array{boolean}")).toEqual([
|
|
272
|
-
{ name: "flags", kind: "array", items: "boolean" },
|
|
273
|
-
]);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("array of integer", () => {
|
|
277
|
-
expect(parseFieldsString("ids:array{integer}")).toEqual([
|
|
278
|
-
{ name: "ids", kind: "array", items: "integer" },
|
|
279
|
-
]);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test("array of int", () => {
|
|
283
|
-
expect(parseFieldsString("ids:array{int}")).toEqual([
|
|
284
|
-
{ name: "ids", kind: "array", items: "int" },
|
|
285
|
-
]);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("array of bool", () => {
|
|
289
|
-
expect(parseFieldsString("flags:array{bool}")).toEqual([
|
|
290
|
-
{ name: "flags", kind: "array", items: "boolean" },
|
|
291
|
-
]);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("array of float", () => {
|
|
295
|
-
expect(parseFieldsString("scores:array{float}")).toEqual([
|
|
296
|
-
{ name: "scores", kind: "array", items: "number" },
|
|
297
|
-
]);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("array preceded by plain field (user example)", () => {
|
|
301
|
-
expect(parseFieldsString("name, addresses:array{string}")).toEqual([
|
|
302
|
-
{ name: "name", kind: "scalar", type: "string" },
|
|
303
|
-
{ name: "addresses", kind: "array", items: "string" },
|
|
304
|
-
]);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("array followed by plain field", () => {
|
|
308
|
-
expect(parseFieldsString("tags:array{string}, title")).toEqual([
|
|
309
|
-
{ name: "tags", kind: "array", items: "string" },
|
|
310
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
311
|
-
]);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test("whitespace inside braces is trimmed", () => {
|
|
315
|
-
expect(parseFieldsString("tags:array{ string }")).toEqual([
|
|
316
|
-
{ name: "tags", kind: "array", items: "string" },
|
|
317
|
-
]);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
// parseFieldsString — arrays (error cases)
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
|
|
325
|
-
describe("parseFieldsString — array errors", () => {
|
|
326
|
-
test("unknown item type names the offending type and field", () => {
|
|
327
|
-
const err = () => parseFieldsString("x:array{object}");
|
|
328
|
-
expect(err).toThrow(/Unknown type "object"/);
|
|
329
|
-
expect(err).toThrow(/field "x"/);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("empty braces throws with field name", () => {
|
|
333
|
-
const err = () => parseFieldsString("x:array{}");
|
|
334
|
-
expect(err).toThrow(/requires an item type/);
|
|
335
|
-
expect(err).toThrow(/"x"/);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("whitespace-only braces throws", () => {
|
|
339
|
-
expect(() => parseFieldsString("x:array{ }")).toThrow(/requires an item type/);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("missing closing brace gives Unmatched braces error", () => {
|
|
343
|
-
expect(() => parseFieldsString("x:array{string")).toThrow(/Unmatched braces/);
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// ---------------------------------------------------------------------------
|
|
348
|
-
// parseFieldsString — brace depth / structural errors
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
|
|
351
|
-
describe("parseFieldsString — structural errors", () => {
|
|
352
|
-
test("unmatched open brace throws", () => {
|
|
353
|
-
expect(() => parseFieldsString("x:array{string, y")).toThrow(/Unmatched braces/);
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
// buildSchemaFromParsedFields
|
|
359
|
-
// ---------------------------------------------------------------------------
|
|
360
|
-
|
|
361
|
-
describe("buildSchemaFromParsedFields", () => {
|
|
362
|
-
test("single string field", () => {
|
|
363
|
-
expect(buildSchemaFromParsedFields([
|
|
364
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
365
|
-
])).toEqual({
|
|
366
|
-
type: "object",
|
|
367
|
-
properties: { title: { type: "string" } },
|
|
368
|
-
required: ["title"],
|
|
369
|
-
additionalProperties: false,
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test("two scalar fields", () => {
|
|
374
|
-
expect(buildSchemaFromParsedFields([
|
|
375
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
376
|
-
{ name: "price", kind: "scalar", type: "number" },
|
|
377
|
-
])).toEqual({
|
|
378
|
-
type: "object",
|
|
379
|
-
properties: {
|
|
380
|
-
title: { type: "string" },
|
|
381
|
-
price: { type: "number" },
|
|
382
|
-
},
|
|
383
|
-
required: ["title", "price"],
|
|
384
|
-
additionalProperties: false,
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test("enum field produces string type with enum array", () => {
|
|
389
|
-
expect(buildSchemaFromParsedFields([
|
|
390
|
-
{ name: "status", kind: "enum", values: ["draft", "published"] },
|
|
391
|
-
])).toEqual({
|
|
392
|
-
type: "object",
|
|
393
|
-
properties: {
|
|
394
|
-
status: { type: "string", enum: ["draft", "published"] },
|
|
395
|
-
},
|
|
396
|
-
required: ["status"],
|
|
397
|
-
additionalProperties: false,
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test("array field produces array type with items", () => {
|
|
402
|
-
expect(buildSchemaFromParsedFields([
|
|
403
|
-
{ name: "tags", kind: "array", items: "string" },
|
|
404
|
-
])).toEqual({
|
|
405
|
-
type: "object",
|
|
406
|
-
properties: {
|
|
407
|
-
tags: { type: "array", items: { type: "string" } },
|
|
408
|
-
},
|
|
409
|
-
required: ["tags"],
|
|
410
|
-
additionalProperties: false,
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test("all three kinds together", () => {
|
|
415
|
-
expect(buildSchemaFromParsedFields([
|
|
416
|
-
{ name: "title", kind: "scalar", type: "string" },
|
|
417
|
-
{ name: "status", kind: "enum", values: ["a", "b"] },
|
|
418
|
-
{ name: "tags", kind: "array", items: "string" },
|
|
419
|
-
])).toEqual({
|
|
420
|
-
type: "object",
|
|
421
|
-
properties: {
|
|
422
|
-
title: { type: "string" },
|
|
423
|
-
status: { type: "string", enum: ["a", "b"] },
|
|
424
|
-
tags: { type: "array", items: { type: "string" } },
|
|
425
|
-
},
|
|
426
|
-
required: ["title", "status", "tags"],
|
|
427
|
-
additionalProperties: false,
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
test("int field produces integer type with multipleOf:1", () => {
|
|
432
|
-
expect(buildSchemaFromParsedFields([
|
|
433
|
-
{ name: "count", kind: "scalar", type: "int" },
|
|
434
|
-
])).toEqual({
|
|
435
|
-
type: "object",
|
|
436
|
-
properties: { count: { type: "integer", multipleOf: 1 } },
|
|
437
|
-
required: ["count"],
|
|
438
|
-
additionalProperties: false,
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test("float field produces plain number type (no multipleOf)", () => {
|
|
443
|
-
expect(buildSchemaFromParsedFields([
|
|
444
|
-
{ name: "ratio", kind: "scalar", type: "number" },
|
|
445
|
-
])).toEqual({
|
|
446
|
-
type: "object",
|
|
447
|
-
properties: { ratio: { type: "number" } },
|
|
448
|
-
required: ["ratio"],
|
|
449
|
-
additionalProperties: false,
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test("array of int items produces integer items with multipleOf:1", () => {
|
|
454
|
-
expect(buildSchemaFromParsedFields([
|
|
455
|
-
{ name: "ids", kind: "array", items: "int" },
|
|
456
|
-
])).toEqual({
|
|
457
|
-
type: "object",
|
|
458
|
-
properties: { ids: { type: "array", items: { type: "integer", multipleOf: 1 } } },
|
|
459
|
-
required: ["ids"],
|
|
460
|
-
additionalProperties: false,
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
test("throws on empty array with helpful message", () => {
|
|
464
|
-
expect(() => buildSchemaFromParsedFields([])).toThrow(
|
|
465
|
-
"Cannot build a schema from an empty fields list.",
|
|
466
|
-
);
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// ---------------------------------------------------------------------------
|
|
471
|
-
// buildSchemaFromFields (end-to-end convenience wrapper)
|
|
472
|
-
// ---------------------------------------------------------------------------
|
|
473
|
-
|
|
474
|
-
describe("buildSchemaFromFields", () => {
|
|
475
|
-
test("single untyped field", () => {
|
|
476
|
-
expect(buildSchemaFromFields("title")).toEqual({
|
|
477
|
-
type: "object",
|
|
478
|
-
properties: { title: { type: "string" } },
|
|
479
|
-
required: ["title"],
|
|
480
|
-
additionalProperties: false,
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
test("two untyped fields", () => {
|
|
485
|
-
expect(buildSchemaFromFields("title,description")).toEqual({
|
|
486
|
-
type: "object",
|
|
487
|
-
properties: {
|
|
488
|
-
title: { type: "string" },
|
|
489
|
-
description: { type: "string" },
|
|
490
|
-
},
|
|
491
|
-
required: ["title", "description"],
|
|
492
|
-
additionalProperties: false,
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test("mixed scalar types", () => {
|
|
497
|
-
expect(buildSchemaFromFields("title, price:number")).toEqual({
|
|
498
|
-
type: "object",
|
|
499
|
-
properties: {
|
|
500
|
-
title: { type: "string" },
|
|
501
|
-
price: { type: "number" },
|
|
502
|
-
},
|
|
503
|
-
required: ["title", "price"],
|
|
504
|
-
additionalProperties: false,
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
test("enum (user example: name,wtf:enum{abc|def|123})", () => {
|
|
509
|
-
expect(buildSchemaFromFields("name,wtf:enum{abc|def|123}")).toEqual({
|
|
510
|
-
type: "object",
|
|
511
|
-
properties: {
|
|
512
|
-
name: { type: "string" },
|
|
513
|
-
wtf: { type: "string", enum: ["abc", "def", "123"] },
|
|
514
|
-
},
|
|
515
|
-
required: ["name", "wtf"],
|
|
516
|
-
additionalProperties: false,
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
test("array (user example: name,addresses:array{string})", () => {
|
|
521
|
-
expect(buildSchemaFromFields("name,addresses:array{string}")).toEqual({
|
|
522
|
-
type: "object",
|
|
523
|
-
properties: {
|
|
524
|
-
name: { type: "string" },
|
|
525
|
-
addresses: { type: "array", items: { type: "string" } },
|
|
526
|
-
},
|
|
527
|
-
required: ["name", "addresses"],
|
|
528
|
-
additionalProperties: false,
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
test("array shorthand (user example: name,authors:array)", () => {
|
|
533
|
-
expect(buildSchemaFromFields("name,authors:array")).toEqual({
|
|
534
|
-
type: "object",
|
|
535
|
-
properties: {
|
|
536
|
-
name: { type: "string" },
|
|
537
|
-
authors: { type: "array", items: { type: "string" } },
|
|
538
|
-
},
|
|
539
|
-
required: ["name", "authors"],
|
|
540
|
-
additionalProperties: false,
|
|
541
|
-
});
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
test("all four types in one string", () => {
|
|
545
|
-
expect(buildSchemaFromFields(
|
|
546
|
-
"title, price:number, status:enum{a|b}, tags:array{string}",
|
|
547
|
-
)).toEqual({
|
|
548
|
-
type: "object",
|
|
549
|
-
properties: {
|
|
550
|
-
title: { type: "string" },
|
|
551
|
-
price: { type: "number" },
|
|
552
|
-
status: { type: "string", enum: ["a", "b"] },
|
|
553
|
-
tags: { type: "array", items: { type: "string" } },
|
|
554
|
-
},
|
|
555
|
-
required: ["title", "price", "status", "tags"],
|
|
556
|
-
additionalProperties: false,
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
test("realistic product schema", () => {
|
|
561
|
-
expect(buildSchemaFromFields(
|
|
562
|
-
"id, name, price:number, in_stock:boolean, tags:array{string}, condition:enum{new|used|refurbished}",
|
|
563
|
-
)).toEqual({
|
|
564
|
-
type: "object",
|
|
565
|
-
properties: {
|
|
566
|
-
id: { type: "string" },
|
|
567
|
-
name: { type: "string" },
|
|
568
|
-
price: { type: "number" },
|
|
569
|
-
in_stock: { type: "boolean" },
|
|
570
|
-
tags: { type: "array", items: { type: "string" } },
|
|
571
|
-
condition: { type: "string", enum: ["new", "used", "refurbished"] },
|
|
572
|
-
},
|
|
573
|
-
required: ["id", "name", "price", "in_stock", "tags", "condition"],
|
|
574
|
-
additionalProperties: false,
|
|
575
|
-
});
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
test("realistic article schema", () => {
|
|
579
|
-
expect(buildSchemaFromFields(
|
|
580
|
-
"title, author, word_count:integer, published:boolean, status:enum{draft|review|published}",
|
|
581
|
-
)).toEqual({
|
|
582
|
-
type: "object",
|
|
583
|
-
properties: {
|
|
584
|
-
title: { type: "string" },
|
|
585
|
-
author: { type: "string" },
|
|
586
|
-
word_count: { type: "integer" },
|
|
587
|
-
published: { type: "boolean" },
|
|
588
|
-
status: { type: "string", enum: ["draft", "review", "published"] },
|
|
589
|
-
},
|
|
590
|
-
required: ["title", "author", "word_count", "published", "status"],
|
|
591
|
-
additionalProperties: false,
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
test("numeric-looking enum values stay as strings in schema", () => {
|
|
596
|
-
const schema = buildSchemaFromFields("rating:enum{1|2|3|4|5}") as {
|
|
597
|
-
properties: { rating: { enum: unknown[] } };
|
|
598
|
-
};
|
|
599
|
-
expect(schema.properties.rating.enum).toEqual(["1", "2", "3", "4", "5"]);
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// ---------------------------------------------------------------------------
|
|
604
|
-
// extract() mutual-exclusion guard
|
|
605
|
-
// ---------------------------------------------------------------------------
|
|
606
|
-
|
|
607
|
-
describe("extract() schema mutual exclusion", () => {
|
|
608
|
-
const mockStrategy = () => ({
|
|
609
|
-
name: "mock",
|
|
610
|
-
run: async () => ({
|
|
611
|
-
data: {},
|
|
612
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
613
|
-
}),
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
test("error message tells you they are mutually exclusive", async () => {
|
|
617
|
-
const { extract } = await import("./extract");
|
|
618
|
-
const result = await extract({
|
|
619
|
-
artifacts: [],
|
|
620
|
-
schema: { type: "object", properties: {}, required: [] },
|
|
621
|
-
fields: "title",
|
|
622
|
-
strategy: mockStrategy(),
|
|
623
|
-
});
|
|
624
|
-
expect(result.error).toBeDefined();
|
|
625
|
-
expect(result.error?.message).toMatch(/mutually exclusive/);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
test("error message when neither schema nor fields are provided", async () => {
|
|
629
|
-
const { extract } = await import("./extract");
|
|
630
|
-
const result = await extract({
|
|
631
|
-
artifacts: [],
|
|
632
|
-
strategy: mockStrategy(),
|
|
633
|
-
} as Parameters<typeof extract>[0]);
|
|
634
|
-
expect(result.error).toBeDefined();
|
|
635
|
-
expect(result.error?.message).toMatch(/schema definition is required/);
|
|
636
|
-
// Tells you what to use instead
|
|
637
|
-
expect(result.error?.message).toMatch(/`schema`/);
|
|
638
|
-
expect(result.error?.message).toMatch(/`fields`/);
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
test("succeeds with only schema", async () => {
|
|
642
|
-
const { extract } = await import("./extract");
|
|
643
|
-
const result = await extract({
|
|
644
|
-
artifacts: [],
|
|
645
|
-
schema: { type: "object", properties: {}, required: [] },
|
|
646
|
-
strategy: mockStrategy(),
|
|
647
|
-
});
|
|
648
|
-
expect(result.error).toBeUndefined();
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
test("succeeds with only fields string", async () => {
|
|
652
|
-
const { extract } = await import("./extract");
|
|
653
|
-
const result = await extract({
|
|
654
|
-
artifacts: [],
|
|
655
|
-
fields: "title",
|
|
656
|
-
strategy: mockStrategy(),
|
|
657
|
-
});
|
|
658
|
-
expect(result.error).toBeUndefined();
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
test("succeeds with fields including enum and array", async () => {
|
|
662
|
-
const { extract } = await import("./extract");
|
|
663
|
-
const result = await extract({
|
|
664
|
-
artifacts: [],
|
|
665
|
-
fields: "title, status:enum{a|b}, tags:array{string}",
|
|
666
|
-
strategy: mockStrategy(),
|
|
667
|
-
});
|
|
668
|
-
expect(result.error).toBeUndefined();
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
test("invalid fields string surfaces as error on result", async () => {
|
|
672
|
-
const { extract } = await import("./extract");
|
|
673
|
-
const result = await extract({
|
|
674
|
-
artifacts: [],
|
|
675
|
-
fields: "title:badtype",
|
|
676
|
-
strategy: mockStrategy(),
|
|
677
|
-
});
|
|
678
|
-
expect(result.error).toBeDefined();
|
|
679
|
-
expect(result.error?.message).toMatch(/Unknown type "badtype"/);
|
|
680
|
-
});
|
|
681
|
-
});
|