@ontrails/testing 1.0.0-beta.4 → 1.0.0-beta.42
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/CHANGELOG.md +528 -12
- package/README.md +83 -18
- package/package.json +34 -5
- package/src/all-established.ts +168 -0
- package/src/all.ts +51 -12
- package/src/assertions.ts +253 -0
- package/src/cli.ts +6 -0
- package/src/composes.ts +433 -0
- package/src/context.ts +200 -14
- package/src/contracts.ts +44 -33
- package/src/detours.ts +155 -18
- package/src/effective-examples.ts +408 -0
- package/src/errors.ts +47 -0
- package/src/examples.ts +274 -113
- package/src/harness-cli.ts +83 -58
- package/src/harness-http.ts +341 -0
- package/src/harness-mcp.ts +49 -16
- package/src/http.ts +10 -0
- package/src/index.ts +22 -14
- package/src/logger.ts +3 -1
- package/src/mcp.ts +6 -0
- package/src/scenario.ts +375 -0
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +389 -0
- package/src/trail.ts +1 -1
- package/src/types.ts +24 -52
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/all.d.ts +0 -30
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -47
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -49
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -84
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -19
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -16
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -66
- package/dist/contracts.js.map +0 -1
- package/dist/detours.d.ts +0 -12
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -30
- package/dist/detours.js.map +0 -1
- package/dist/examples.d.ts +0 -22
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -175
- package/dist/examples.js.map +0 -1
- package/dist/follows.d.ts +0 -32
- package/dist/follows.d.ts.map +0 -1
- package/dist/follows.js +0 -169
- package/dist/follows.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -21
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js +0 -213
- package/dist/harness-cli.js.map +0 -1
- package/dist/harness-mcp.d.ts +0 -21
- package/dist/harness-mcp.d.ts.map +0 -1
- package/dist/harness-mcp.js +0 -50
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -14
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -15
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -87
- package/dist/logger.js.map +0 -1
- package/dist/trail.d.ts +0 -20
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -80
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -80
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/context.test.ts +0 -60
- package/src/__tests__/contracts.test.ts +0 -94
- package/src/__tests__/detours.test.ts +0 -55
- package/src/__tests__/examples.test.ts +0 -175
- package/src/__tests__/follows.test.ts +0 -163
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/trail.test.ts +0 -99
- package/src/follows.ts +0 -283
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import type { AnyEntity, Trail, TrailExample } from '@ontrails/core';
|
|
2
|
+
import {
|
|
3
|
+
getEntityReferences,
|
|
4
|
+
getTrailVersionEntryKind,
|
|
5
|
+
isArchivedTrailVersionEntry,
|
|
6
|
+
} from '@ontrails/core';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
type ExampleRecord = Readonly<Record<string, unknown>>;
|
|
10
|
+
|
|
11
|
+
export interface TrailExampleTarget {
|
|
12
|
+
readonly composes: readonly string[];
|
|
13
|
+
readonly current: boolean;
|
|
14
|
+
readonly examples: readonly TrailExample<unknown, unknown>[];
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly input: Trail<unknown, unknown, unknown>['input'];
|
|
17
|
+
readonly output: Trail<unknown, unknown, unknown>['output'];
|
|
18
|
+
readonly trail: Trail<unknown, unknown, unknown>;
|
|
19
|
+
readonly version?: number | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalizeComposeRef = (value: string | { readonly id: string }): string =>
|
|
23
|
+
typeof value === 'string' ? value : value.id;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tracks examples that `deriveTrailExamples` synthesizes from entity
|
|
27
|
+
* fixtures. Authored examples are passed through untouched and never
|
|
28
|
+
* appear here, so consumers can distinguish the two by identity.
|
|
29
|
+
*
|
|
30
|
+
* Exposed via `isDerivedExample` so downstream testing helpers (e.g.
|
|
31
|
+
* `testExamples` composing coverage) can relax invariants that only make
|
|
32
|
+
* sense for authored inputs.
|
|
33
|
+
*/
|
|
34
|
+
const derivedExamples = new WeakSet<TrailExample<unknown, unknown>>();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns `true` if the given example was synthesized from entity fixtures
|
|
38
|
+
* by `deriveTrailExamples`, `false` if it was authored on the trail.
|
|
39
|
+
*/
|
|
40
|
+
export const isDerivedExample = (
|
|
41
|
+
example: TrailExample<unknown, unknown>
|
|
42
|
+
): boolean => derivedExamples.has(example);
|
|
43
|
+
|
|
44
|
+
interface EntityFixture {
|
|
45
|
+
readonly entity: AnyEntity;
|
|
46
|
+
readonly example: ExampleRecord;
|
|
47
|
+
readonly index: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const capitalize = (value: string): string =>
|
|
51
|
+
value.length === 0 ? value : value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
52
|
+
|
|
53
|
+
const collectReferenceMap = (
|
|
54
|
+
entities: readonly AnyEntity[]
|
|
55
|
+
): ReadonlyMap<string, ReturnType<typeof getEntityReferences>> => {
|
|
56
|
+
const entityNames = new Set(entities.map((entity) => entity.name));
|
|
57
|
+
|
|
58
|
+
return new Map(
|
|
59
|
+
entities.map((entity) => [
|
|
60
|
+
entity.name,
|
|
61
|
+
getEntityReferences(entity).filter((reference) =>
|
|
62
|
+
entityNames.has(reference.entity)
|
|
63
|
+
),
|
|
64
|
+
])
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getIdentityValue = (fixture: EntityFixture): unknown =>
|
|
69
|
+
fixture.example[fixture.entity.identity];
|
|
70
|
+
|
|
71
|
+
const candidateMatchesSelectedReference = (
|
|
72
|
+
candidate: EntityFixture,
|
|
73
|
+
target: EntityFixture,
|
|
74
|
+
reference: ReturnType<typeof getEntityReferences>[number]
|
|
75
|
+
): boolean =>
|
|
76
|
+
Object.is(candidate.example[reference.field], getIdentityValue(target));
|
|
77
|
+
|
|
78
|
+
const selectedMatchesCandidateReference = (
|
|
79
|
+
fixture: EntityFixture,
|
|
80
|
+
candidate: EntityFixture,
|
|
81
|
+
reference: ReturnType<typeof getEntityReferences>[number]
|
|
82
|
+
): boolean =>
|
|
83
|
+
Object.is(fixture.example[reference.field], getIdentityValue(candidate));
|
|
84
|
+
|
|
85
|
+
const matchesCandidateReferences = (
|
|
86
|
+
candidate: EntityFixture,
|
|
87
|
+
selected: readonly EntityFixture[],
|
|
88
|
+
referencesByEntity: ReadonlyMap<
|
|
89
|
+
string,
|
|
90
|
+
ReturnType<typeof getEntityReferences>
|
|
91
|
+
>
|
|
92
|
+
): boolean => {
|
|
93
|
+
const candidateReferences =
|
|
94
|
+
referencesByEntity.get(candidate.entity.name) ?? [];
|
|
95
|
+
|
|
96
|
+
for (const reference of candidateReferences) {
|
|
97
|
+
const target = selected.find(
|
|
98
|
+
(fixture) => fixture.entity.name === reference.entity
|
|
99
|
+
);
|
|
100
|
+
if (target === undefined) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!candidateMatchesSelectedReference(candidate, target, reference)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const matchesSelectedReferences = (
|
|
112
|
+
candidate: EntityFixture,
|
|
113
|
+
selected: readonly EntityFixture[],
|
|
114
|
+
referencesByEntity: ReadonlyMap<
|
|
115
|
+
string,
|
|
116
|
+
ReturnType<typeof getEntityReferences>
|
|
117
|
+
>
|
|
118
|
+
): boolean => {
|
|
119
|
+
for (const fixture of selected) {
|
|
120
|
+
const fixtureReferences = referencesByEntity.get(fixture.entity.name) ?? [];
|
|
121
|
+
for (const reference of fixtureReferences) {
|
|
122
|
+
if (reference.entity !== candidate.entity.name) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!selectedMatchesCandidateReference(fixture, candidate, reference)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const matchesKnownReferences = (
|
|
135
|
+
candidate: EntityFixture,
|
|
136
|
+
selected: readonly EntityFixture[],
|
|
137
|
+
referencesByEntity: ReadonlyMap<
|
|
138
|
+
string,
|
|
139
|
+
ReturnType<typeof getEntityReferences>
|
|
140
|
+
>
|
|
141
|
+
): boolean =>
|
|
142
|
+
matchesCandidateReferences(candidate, selected, referencesByEntity) &&
|
|
143
|
+
matchesSelectedReferences(candidate, selected, referencesByEntity);
|
|
144
|
+
|
|
145
|
+
const selectEntityFixtures = (
|
|
146
|
+
entities: readonly AnyEntity[],
|
|
147
|
+
referencesByEntity: ReadonlyMap<
|
|
148
|
+
string,
|
|
149
|
+
ReturnType<typeof getEntityReferences>
|
|
150
|
+
>,
|
|
151
|
+
index = 0,
|
|
152
|
+
selected: readonly EntityFixture[] = []
|
|
153
|
+
): readonly (readonly EntityFixture[])[] => {
|
|
154
|
+
const entity = entities[index];
|
|
155
|
+
if (entity === undefined) {
|
|
156
|
+
return [selected];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const examples = entity.examples ?? [];
|
|
160
|
+
const matchingFixtures = examples.flatMap((example, exampleIndex) => {
|
|
161
|
+
const fixture = {
|
|
162
|
+
entity,
|
|
163
|
+
example: example as ExampleRecord,
|
|
164
|
+
index: exampleIndex,
|
|
165
|
+
} satisfies EntityFixture;
|
|
166
|
+
|
|
167
|
+
if (!matchesKnownReferences(fixture, selected, referencesByEntity)) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return selectEntityFixtures(entities, referencesByEntity, index + 1, [
|
|
172
|
+
...selected,
|
|
173
|
+
fixture,
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return matchingFixtures;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Merge selected entity fixtures into a single candidate input object.
|
|
182
|
+
*
|
|
183
|
+
* The resulting record contains:
|
|
184
|
+
* - `<entity>`: the full fixture payload keyed by entity name.
|
|
185
|
+
* - `<entity><Identity>`: the fixture's identity value on a prefixed key.
|
|
186
|
+
* - `<entity><Field>`: every fixture field on a prefixed key.
|
|
187
|
+
* - Unqualified `<field>` keys: first-write-wins across entities.
|
|
188
|
+
*
|
|
189
|
+
* The first-write-wins behaviour on unqualified keys is intentional but can
|
|
190
|
+
* silently drop a later entity's value when two entities share a field name
|
|
191
|
+
* (e.g. both declare `id`). The prefixed aliases above are unambiguous and
|
|
192
|
+
* always written, so schemas that consume the prefixed form are unaffected;
|
|
193
|
+
* schemas that rely on the bare field name should disambiguate via the
|
|
194
|
+
* prefixed alias instead.
|
|
195
|
+
*/
|
|
196
|
+
const buildDerivedInput = (
|
|
197
|
+
fixtures: readonly EntityFixture[]
|
|
198
|
+
): Record<string, unknown> => {
|
|
199
|
+
const candidate: Record<string, unknown> = {};
|
|
200
|
+
|
|
201
|
+
for (const fixture of fixtures) {
|
|
202
|
+
candidate[fixture.entity.name] = fixture.example;
|
|
203
|
+
candidate[`${fixture.entity.name}${capitalize(fixture.entity.identity)}`] =
|
|
204
|
+
getIdentityValue(fixture);
|
|
205
|
+
|
|
206
|
+
for (const [field, value] of Object.entries(fixture.example)) {
|
|
207
|
+
if (!Object.hasOwn(candidate, field)) {
|
|
208
|
+
candidate[field] = value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
candidate[`${fixture.entity.name}${capitalize(field)}`] = value;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return candidate;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Project the merged candidate input down to keys the trail's input schema
|
|
220
|
+
* knows about.
|
|
221
|
+
*
|
|
222
|
+
* `buildDerivedInput` emits synthesized prefixed aliases (e.g. `userEmail`)
|
|
223
|
+
* alongside bare field names. Strict schemas (`z.object(...).strict()`)
|
|
224
|
+
* reject any unknown key, which means an otherwise valid derived fixture
|
|
225
|
+
* would silently fail `safeParse` just because of the synthesized aliases.
|
|
226
|
+
* When the input is a `ZodObject`, trim the candidate to its declared keys
|
|
227
|
+
* before validation. Non-object inputs pass through unchanged — they are
|
|
228
|
+
* validated as-is and can decide for themselves.
|
|
229
|
+
*/
|
|
230
|
+
const projectInputForSchema = (
|
|
231
|
+
inputSchema: Trail<unknown, unknown, unknown>['input'],
|
|
232
|
+
candidate: Record<string, unknown>
|
|
233
|
+
): Record<string, unknown> => {
|
|
234
|
+
if (!(inputSchema instanceof z.ZodObject)) {
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const known = Object.keys(inputSchema.shape);
|
|
239
|
+
const projected: Record<string, unknown> = {};
|
|
240
|
+
for (const key of known) {
|
|
241
|
+
if (Object.hasOwn(candidate, key)) {
|
|
242
|
+
projected[key] = candidate[key];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return projected;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Derive an expected output value from the selected entity fixtures when
|
|
250
|
+
* exactly one fixture's payload satisfies the trail's output schema.
|
|
251
|
+
*
|
|
252
|
+
* Returns `undefined` when the trail has no output schema, when no fixture
|
|
253
|
+
* matches, or when more than one matches — callers should then leave the
|
|
254
|
+
* derived example without an `expected` and fall back to schema-only
|
|
255
|
+
* validation. We intentionally do **not** infer `expected` from the merged
|
|
256
|
+
* candidate input: input and output schemas frequently overlap structurally
|
|
257
|
+
* but represent different semantics, so inferring from the input would
|
|
258
|
+
* produce false deep-equality failures.
|
|
259
|
+
*/
|
|
260
|
+
const deriveExpectedValue = (
|
|
261
|
+
trail: Trail<unknown, unknown, unknown>,
|
|
262
|
+
fixtures: readonly EntityFixture[]
|
|
263
|
+
): unknown => {
|
|
264
|
+
if (trail.output === undefined) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const outputSchema = trail.output;
|
|
269
|
+
const entityMatches = fixtures
|
|
270
|
+
.map((fixture) => outputSchema.safeParse(fixture.example))
|
|
271
|
+
.filter((candidate) => candidate.success);
|
|
272
|
+
|
|
273
|
+
if (entityMatches.length !== 1) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const [singleMatch] = entityMatches;
|
|
278
|
+
if (singleMatch === undefined) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
return singleMatch.data;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const formatFixtureName = (
|
|
285
|
+
fixtures: readonly EntityFixture[],
|
|
286
|
+
index: number
|
|
287
|
+
): string => {
|
|
288
|
+
const label = fixtures
|
|
289
|
+
.map((fixture) => {
|
|
290
|
+
const identity = getIdentityValue(fixture);
|
|
291
|
+
const fallback = fixture.index + 1;
|
|
292
|
+
return `${fixture.entity.name}:${String(identity ?? fallback)}`;
|
|
293
|
+
})
|
|
294
|
+
.join(', ');
|
|
295
|
+
|
|
296
|
+
return label.length > 0
|
|
297
|
+
? `Derived fixture ${index + 1} (${label})`
|
|
298
|
+
: `Derived fixture ${index + 1}`;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Prefer authored trail examples and fall back to entity-derived fixtures.
|
|
303
|
+
*
|
|
304
|
+
* Examples returned by this helper come from one of two provenances:
|
|
305
|
+
* - **Authored.** When `trail.examples` is non-empty, its entries are
|
|
306
|
+
* returned verbatim. These are the developer's stated intent and carry
|
|
307
|
+
* full invariants — including composing-coverage assertions in
|
|
308
|
+
* `testExamples`.
|
|
309
|
+
* - **Derived.** When there are no authored examples but the trail has
|
|
310
|
+
* entities with examples, candidate inputs are synthesized from entity
|
|
311
|
+
* fixtures and validated against `trail.input`. These are opportunistic
|
|
312
|
+
* coverage that exists to let `testAll(app)` exercise entity-backed
|
|
313
|
+
* trails without per-test setup; they are not guaranteed to exercise
|
|
314
|
+
* every composition branch, so consumers should relax invariants that
|
|
315
|
+
* only make sense for authored inputs (see `isDerivedExample`).
|
|
316
|
+
*
|
|
317
|
+
* Entity examples stay as the raw input payload so Trails validation /
|
|
318
|
+
* transforms still happen exactly once inside the normal test execution
|
|
319
|
+
* path. Derived examples are additionally tagged via a module-level
|
|
320
|
+
* `WeakSet` so consumers can detect them without widening the public
|
|
321
|
+
* `TrailExample` shape.
|
|
322
|
+
*/
|
|
323
|
+
export const deriveTrailExamples = (
|
|
324
|
+
trail: Trail<unknown, unknown, unknown>
|
|
325
|
+
): readonly TrailExample<unknown, unknown>[] => {
|
|
326
|
+
if (trail.examples !== undefined && trail.examples.length > 0) {
|
|
327
|
+
return trail.examples;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (trail.entities.length === 0) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
trail.entities.some(
|
|
336
|
+
(entity) => entity.examples === undefined || entity.examples.length === 0
|
|
337
|
+
)
|
|
338
|
+
) {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const referencesByEntity = collectReferenceMap(trail.entities);
|
|
343
|
+
const fixtureSets = selectEntityFixtures(trail.entities, referencesByEntity);
|
|
344
|
+
|
|
345
|
+
return fixtureSets.flatMap((fixtures, index) => {
|
|
346
|
+
const merged = buildDerivedInput(fixtures);
|
|
347
|
+
const input = projectInputForSchema(trail.input, merged);
|
|
348
|
+
const validated = trail.input.safeParse(input);
|
|
349
|
+
if (!validated.success) {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const expected = deriveExpectedValue(trail, fixtures);
|
|
354
|
+
const derived: TrailExample<unknown, unknown> = {
|
|
355
|
+
...(expected === undefined ? {} : { expected }),
|
|
356
|
+
input,
|
|
357
|
+
name: formatFixtureName(fixtures, index),
|
|
358
|
+
};
|
|
359
|
+
derivedExamples.add(derived);
|
|
360
|
+
return [derived];
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
export const deriveTrailExampleTargets = (
|
|
365
|
+
trail: Trail<unknown, unknown, unknown>
|
|
366
|
+
): readonly TrailExampleTarget[] => {
|
|
367
|
+
const targets: TrailExampleTarget[] = [];
|
|
368
|
+
const currentExamples = deriveTrailExamples(trail);
|
|
369
|
+
if (currentExamples.length > 0) {
|
|
370
|
+
targets.push({
|
|
371
|
+
composes: trail.composes,
|
|
372
|
+
current: true,
|
|
373
|
+
examples: currentExamples,
|
|
374
|
+
id: trail.id,
|
|
375
|
+
input: trail.input,
|
|
376
|
+
output: trail.output,
|
|
377
|
+
trail,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (const [rawVersion, entry] of Object.entries(
|
|
382
|
+
trail.versions ?? {}
|
|
383
|
+
).toSorted(([left], [right]) => Number(left) - Number(right))) {
|
|
384
|
+
if (isArchivedTrailVersionEntry(entry)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const examples = entry.examples ?? [];
|
|
388
|
+
if (examples.length === 0) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const kind = getTrailVersionEntryKind(entry);
|
|
392
|
+
targets.push({
|
|
393
|
+
composes:
|
|
394
|
+
kind === 'fork'
|
|
395
|
+
? (entry.composes ?? []).map(normalizeComposeRef)
|
|
396
|
+
: trail.composes,
|
|
397
|
+
current: false,
|
|
398
|
+
examples,
|
|
399
|
+
id: `${trail.id}@${rawVersion}`,
|
|
400
|
+
input: entry.input,
|
|
401
|
+
output: entry.output,
|
|
402
|
+
trail,
|
|
403
|
+
version: Number(rawVersion),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return targets;
|
|
408
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
errorClasses,
|
|
3
|
+
InternalError,
|
|
4
|
+
RetryExhaustedError,
|
|
5
|
+
TrailsError,
|
|
6
|
+
} from '@ontrails/core';
|
|
7
|
+
|
|
8
|
+
type ErrorConstructor = new (...args: never[]) => Error;
|
|
9
|
+
type MessageErrorConstructor = new (message: string) => Error;
|
|
10
|
+
|
|
11
|
+
const ERROR_CLASS_BY_NAME = new Map<string, ErrorConstructor>([
|
|
12
|
+
...errorClasses.map(
|
|
13
|
+
(entry) => [entry.name, entry.ctor as ErrorConstructor] as const
|
|
14
|
+
),
|
|
15
|
+
['TrailsError', TrailsError as unknown as ErrorConstructor],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve an error class name string to the actual constructor.
|
|
20
|
+
* Falls back to generic Error if the name is not in the core taxonomy.
|
|
21
|
+
*/
|
|
22
|
+
export const resolveErrorClass = (name: string): ErrorConstructor =>
|
|
23
|
+
ERROR_CLASS_BY_NAME.get(name) ?? (Error as ErrorConstructor);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an error instance for an authored example error name.
|
|
27
|
+
*/
|
|
28
|
+
export const createErrorFromName = (name: string): Error => {
|
|
29
|
+
if (name === 'TrailsError') {
|
|
30
|
+
return new InternalError(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const entry = errorClasses.find((candidate) => candidate.name === name);
|
|
34
|
+
if (entry === undefined) {
|
|
35
|
+
return new Error(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (entry.name === 'RetryExhaustedError') {
|
|
39
|
+
return new RetryExhaustedError(new InternalError(name), {
|
|
40
|
+
attempts: 1,
|
|
41
|
+
detour: 'testComposes',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ErrorClass = entry.ctor as unknown as MessageErrorConstructor;
|
|
46
|
+
return new ErrorClass(name);
|
|
47
|
+
};
|