@ontrails/trails 0.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/CHANGELOG.md +1906 -0
- package/README.md +48 -0
- package/bin/trails.ts +3 -0
- package/package.json +57 -0
- package/src/app.ts +167 -0
- package/src/clack.ts +111 -0
- package/src/cli.ts +308 -0
- package/src/completions.ts +431 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +129 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +92 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/audit.ts +571 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +636 -0
- package/src/regrade/lifecycle.ts +76 -0
- package/src/regrade/live-api-preserve.ts +123 -0
- package/src/regrade/plan-artifact.ts +515 -0
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/prepared-run.ts +259 -0
- package/src/regrade/receipt-history.ts +446 -0
- package/src/regrade/source-transaction.ts +185 -0
- package/src/release/bindings.ts +58 -0
- package/src/release/changeset-packages.ts +99 -0
- package/src/release/check.ts +1191 -0
- package/src/release/cli-bundle.ts +575 -0
- package/src/release/config.ts +73 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/homebrew.ts +221 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +255 -0
- package/src/release/lock-roundtrip-workspace.ts +107 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +848 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1780 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/stable-version-release.ts +80 -0
- package/src/release/wayfinder-dogfood-smoke.ts +762 -0
- package/src/release/zero-line-transition.ts +68 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-argv.ts +133 -0
- package/src/run-collision.ts +126 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-regrade-progress.ts +47 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch-project.ts +52 -0
- package/src/run-watch.ts +381 -0
- package/src/run-wayfind-outline.ts +170 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +816 -0
- package/src/trails/add-trail.ts +141 -0
- package/src/trails/add-verify.ts +252 -0
- package/src/trails/compile.ts +118 -0
- package/src/trails/completions-complete.ts +236 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/config-explain.ts +43 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +1215 -0
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +652 -0
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +80 -0
- package/src/trails/dev-reset.ts +48 -0
- package/src/trails/dev-stats.ts +71 -0
- package/src/trails/dev-support.ts +360 -0
- package/src/trails/doctor.ts +77 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +106 -0
- package/src/trails/load-app.ts +1145 -0
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project-context-output.ts +304 -0
- package/src/trails/project-context.ts +613 -0
- package/src/trails/project.ts +65 -0
- package/src/trails/regrade.ts +4951 -0
- package/src/trails/release-check.ts +113 -0
- package/src/trails/release-smoke.ts +49 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +592 -0
- package/src/trails/run-examples.ts +149 -0
- package/src/trails/run.ts +496 -0
- package/src/trails/scaffold-json.ts +60 -0
- package/src/trails/scaffold-topo-identity.ts +479 -0
- package/src/trails/survey.ts +990 -0
- package/src/trails/topo-activation.ts +14 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +259 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +323 -0
- package/src/trails/topo-support.ts +247 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +348 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +598 -0
- package/src/trails/wayfind-diff.ts +716 -0
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1319 -0
- package/src/versions.ts +31 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run.example` trail -- run one named example and compare the result.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
NotFoundError,
|
|
7
|
+
Result,
|
|
8
|
+
TrailsError,
|
|
9
|
+
deriveStructuredTrailExamples,
|
|
10
|
+
run,
|
|
11
|
+
trail,
|
|
12
|
+
} from '@ontrails/core';
|
|
13
|
+
import type { BasePermit, StructuredTrailExample, Topo } from '@ontrails/core';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
import { withFreshAppLease } from './operator-context.js';
|
|
17
|
+
import { assertConfiguredAppBinding } from './project-context.js';
|
|
18
|
+
import {
|
|
19
|
+
operatorProjectContextOutput,
|
|
20
|
+
operatorProjectContextOutputSchema,
|
|
21
|
+
} from './project-context-output.js';
|
|
22
|
+
import { resolveRunTargetProject } from './run.js';
|
|
23
|
+
import type { RunTargetProject } from './run.js';
|
|
24
|
+
import {
|
|
25
|
+
createCurrentAppExampleInput,
|
|
26
|
+
readCurrentAppExampleSelection,
|
|
27
|
+
} from './topo-support.js';
|
|
28
|
+
import type { CurrentAppExampleSelection } from './topo-support.js';
|
|
29
|
+
|
|
30
|
+
export const RUN_EXAMPLE_COMPARISON_KIND = 'example-comparison' as const;
|
|
31
|
+
|
|
32
|
+
export const runExampleComparisonSchema = z.object({
|
|
33
|
+
actual: z.unknown(),
|
|
34
|
+
diff: z.array(z.string()).readonly().optional(),
|
|
35
|
+
exampleName: z.string(),
|
|
36
|
+
executedAppId: z.string(),
|
|
37
|
+
expected: z.unknown(),
|
|
38
|
+
input: z.unknown(),
|
|
39
|
+
kind: z.literal(RUN_EXAMPLE_COMPARISON_KIND),
|
|
40
|
+
match: z.boolean(),
|
|
41
|
+
mode: z.union([
|
|
42
|
+
z.literal('expected'),
|
|
43
|
+
z.literal('expectedMatch'),
|
|
44
|
+
z.literal('error'),
|
|
45
|
+
z.literal('none'),
|
|
46
|
+
]),
|
|
47
|
+
project: operatorProjectContextOutputSchema,
|
|
48
|
+
trailId: z.string(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type RunExampleComparison = z.infer<typeof runExampleComparisonSchema>;
|
|
52
|
+
export type RunExampleComparisonMode = RunExampleComparison['mode'];
|
|
53
|
+
|
|
54
|
+
interface ActualOutcomeOk {
|
|
55
|
+
readonly outcome: 'ok';
|
|
56
|
+
readonly value: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ActualOutcomeErr {
|
|
60
|
+
readonly errorCategory?: string;
|
|
61
|
+
readonly errorClassName: string;
|
|
62
|
+
readonly errorMessage: string;
|
|
63
|
+
readonly outcome: 'err';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type ActualOutcome = ActualOutcomeOk | ActualOutcomeErr;
|
|
67
|
+
|
|
68
|
+
const buildHappyExampleInput = (): {
|
|
69
|
+
readonly exampleName: string;
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly module: string;
|
|
72
|
+
readonly rootDir: string;
|
|
73
|
+
} => ({
|
|
74
|
+
...createCurrentAppExampleInput({ selection: 'configured-project' }),
|
|
75
|
+
exampleName: 'Brief capability report',
|
|
76
|
+
id: 'survey.brief',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const deriveActualOutcome = (result: Result<unknown, Error>): ActualOutcome => {
|
|
80
|
+
if (result.isOk()) {
|
|
81
|
+
return { outcome: 'ok', value: result.value };
|
|
82
|
+
}
|
|
83
|
+
const { error } = result;
|
|
84
|
+
return {
|
|
85
|
+
errorClassName: error.constructor.name,
|
|
86
|
+
errorMessage: error.message,
|
|
87
|
+
outcome: 'err',
|
|
88
|
+
...(error instanceof TrailsError ? { errorCategory: error.category } : {}),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const formatLeaf = (value: unknown): string => {
|
|
93
|
+
try {
|
|
94
|
+
const encoded = JSON.stringify(value);
|
|
95
|
+
return encoded === undefined ? String(value) : encoded;
|
|
96
|
+
} catch {
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const formatPath = (segments: readonly string[]): string =>
|
|
102
|
+
segments.length === 0 ? 'value' : `value.${segments.join('.')}`;
|
|
103
|
+
|
|
104
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
105
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
106
|
+
|
|
107
|
+
const deepEqualWithDiff = (
|
|
108
|
+
actual: unknown,
|
|
109
|
+
expected: unknown,
|
|
110
|
+
path: readonly string[],
|
|
111
|
+
diffs: string[]
|
|
112
|
+
): boolean => {
|
|
113
|
+
if (Array.isArray(expected)) {
|
|
114
|
+
if (!Array.isArray(actual)) {
|
|
115
|
+
diffs.push(`${formatPath(path)}: expected array, got ${typeof actual}`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (actual.length !== expected.length) {
|
|
119
|
+
diffs.push(
|
|
120
|
+
`${formatPath(path)}: array length ${actual.length} != ${expected.length}`
|
|
121
|
+
);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
let ok = true;
|
|
125
|
+
for (let i = 0; i < expected.length; i += 1) {
|
|
126
|
+
if (
|
|
127
|
+
!deepEqualWithDiff(actual[i], expected[i], [...path, `[${i}]`], diffs)
|
|
128
|
+
) {
|
|
129
|
+
ok = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ok;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (isPlainObject(expected)) {
|
|
136
|
+
if (!isPlainObject(actual)) {
|
|
137
|
+
diffs.push(`${formatPath(path)}: expected object, got ${typeof actual}`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
let ok = true;
|
|
141
|
+
for (const key of Object.keys(expected)) {
|
|
142
|
+
if (!(key in actual)) {
|
|
143
|
+
diffs.push(`${formatPath([...path, key])}: missing in actual`);
|
|
144
|
+
ok = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
!deepEqualWithDiff(actual[key], expected[key], [...path, key], diffs)
|
|
149
|
+
) {
|
|
150
|
+
ok = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const key of Object.keys(actual)) {
|
|
154
|
+
if (!(key in expected)) {
|
|
155
|
+
diffs.push(`${formatPath([...path, key])}: unexpected key in actual`);
|
|
156
|
+
ok = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return ok;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (actual !== expected) {
|
|
163
|
+
if (
|
|
164
|
+
typeof actual === 'number' &&
|
|
165
|
+
typeof expected === 'number' &&
|
|
166
|
+
Number.isNaN(actual) &&
|
|
167
|
+
Number.isNaN(expected)
|
|
168
|
+
) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
diffs.push(
|
|
172
|
+
`${formatPath(path)}: ${formatLeaf(actual)} != ${formatLeaf(expected)}`
|
|
173
|
+
);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const partialMatchWithDiff = (
|
|
180
|
+
actual: unknown,
|
|
181
|
+
expected: unknown,
|
|
182
|
+
path: readonly string[],
|
|
183
|
+
diffs: string[]
|
|
184
|
+
): boolean => {
|
|
185
|
+
if (Array.isArray(expected)) {
|
|
186
|
+
if (!Array.isArray(actual)) {
|
|
187
|
+
diffs.push(`${formatPath(path)}: expected array, got ${typeof actual}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
let ok = true;
|
|
191
|
+
const consumed = new Set<number>();
|
|
192
|
+
for (const [index, expectedEntry] of expected.entries()) {
|
|
193
|
+
const matchIndex = actual.findIndex((candidate, candidateIndex) => {
|
|
194
|
+
if (consumed.has(candidateIndex)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const probe: string[] = [];
|
|
198
|
+
return partialMatchWithDiff(candidate, expectedEntry, [], probe);
|
|
199
|
+
});
|
|
200
|
+
if (matchIndex === -1) {
|
|
201
|
+
diffs.push(
|
|
202
|
+
`${formatPath([...path, `[${index}]`])}: expected array to contain ${formatLeaf(expectedEntry)}`
|
|
203
|
+
);
|
|
204
|
+
ok = false;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
consumed.add(matchIndex);
|
|
208
|
+
}
|
|
209
|
+
return ok;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isPlainObject(expected)) {
|
|
213
|
+
if (!isPlainObject(actual)) {
|
|
214
|
+
diffs.push(`${formatPath(path)}: expected object, got ${typeof actual}`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
let ok = true;
|
|
218
|
+
for (const key of Object.keys(expected)) {
|
|
219
|
+
if (!(key in actual)) {
|
|
220
|
+
diffs.push(`${formatPath([...path, key])}: missing in actual`);
|
|
221
|
+
ok = false;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
!partialMatchWithDiff(actual[key], expected[key], [...path, key], diffs)
|
|
226
|
+
) {
|
|
227
|
+
ok = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return ok;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (actual !== expected) {
|
|
234
|
+
diffs.push(
|
|
235
|
+
`${formatPath(path)}: ${formatLeaf(actual)} != ${formatLeaf(expected)}`
|
|
236
|
+
);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const compareExpected = (
|
|
243
|
+
result: Result<unknown, Error>,
|
|
244
|
+
expected: unknown
|
|
245
|
+
): {
|
|
246
|
+
readonly diff?: readonly string[] | undefined;
|
|
247
|
+
readonly match: boolean;
|
|
248
|
+
} => {
|
|
249
|
+
if (result.isErr()) {
|
|
250
|
+
return {
|
|
251
|
+
diff: [
|
|
252
|
+
`value: expected Result.ok(...), got Result.err(${result.error.constructor.name}: ${result.error.message})`,
|
|
253
|
+
],
|
|
254
|
+
match: false,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const diffs: string[] = [];
|
|
258
|
+
const match = deepEqualWithDiff(result.value, expected, [], diffs);
|
|
259
|
+
return { diff: match ? undefined : diffs, match };
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const compareExpectedMatch = (
|
|
263
|
+
result: Result<unknown, Error>,
|
|
264
|
+
expectedMatch: unknown
|
|
265
|
+
): {
|
|
266
|
+
readonly diff?: readonly string[] | undefined;
|
|
267
|
+
readonly match: boolean;
|
|
268
|
+
} => {
|
|
269
|
+
if (result.isErr()) {
|
|
270
|
+
return {
|
|
271
|
+
diff: [
|
|
272
|
+
`value: expected Result.ok(...), got Result.err(${result.error.constructor.name}: ${result.error.message})`,
|
|
273
|
+
],
|
|
274
|
+
match: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const diffs: string[] = [];
|
|
278
|
+
const match = partialMatchWithDiff(result.value, expectedMatch, [], diffs);
|
|
279
|
+
return { diff: match ? undefined : diffs, match };
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const compareError = (
|
|
283
|
+
result: Result<unknown, Error>,
|
|
284
|
+
expectedErrorName: string
|
|
285
|
+
): {
|
|
286
|
+
readonly diff?: readonly string[] | undefined;
|
|
287
|
+
readonly match: boolean;
|
|
288
|
+
} => {
|
|
289
|
+
if (result.isOk()) {
|
|
290
|
+
return {
|
|
291
|
+
diff: [
|
|
292
|
+
`value: expected Result.err(${expectedErrorName}), got Result.ok(${formatLeaf(result.value)})`,
|
|
293
|
+
],
|
|
294
|
+
match: false,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const className = result.error.constructor.name;
|
|
298
|
+
if (className === expectedErrorName) {
|
|
299
|
+
return { diff: undefined, match: true };
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
diff: [
|
|
303
|
+
`value: expected Result.err(${expectedErrorName}), got Result.err(${className}: ${result.error.message})`,
|
|
304
|
+
],
|
|
305
|
+
match: false,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
interface ResolvedExample {
|
|
310
|
+
readonly example: StructuredTrailExample;
|
|
311
|
+
readonly selection?: CurrentAppExampleSelection | undefined;
|
|
312
|
+
readonly nestedInputSelection?: CurrentAppExampleSelection | undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const findExample = (
|
|
316
|
+
app: Topo,
|
|
317
|
+
trailId: string,
|
|
318
|
+
exampleName: string
|
|
319
|
+
): Result<ResolvedExample, Error> => {
|
|
320
|
+
const target = app.get(trailId);
|
|
321
|
+
if (target === undefined) {
|
|
322
|
+
return Result.err(
|
|
323
|
+
new NotFoundError(
|
|
324
|
+
`Trail '${trailId}' was not found in the resolved app.`,
|
|
325
|
+
{ context: { trailId } }
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const structured = deriveStructuredTrailExamples(target.examples) ?? [];
|
|
331
|
+
const match = structured.find((entry) => entry.name === exampleName);
|
|
332
|
+
if (match !== undefined) {
|
|
333
|
+
const authored = target.examples?.find(
|
|
334
|
+
(entry) => entry.name === exampleName
|
|
335
|
+
);
|
|
336
|
+
const authoredInput = authored?.input;
|
|
337
|
+
const nestedInput = isPlainObject(authoredInput)
|
|
338
|
+
? authoredInput['input']
|
|
339
|
+
: undefined;
|
|
340
|
+
return Result.ok({
|
|
341
|
+
example: match,
|
|
342
|
+
nestedInputSelection: readCurrentAppExampleSelection(nestedInput),
|
|
343
|
+
selection: readCurrentAppExampleSelection(authoredInput),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const available = structured.map((entry) => entry.name);
|
|
348
|
+
const listing = available.length === 0 ? '<none>' : available.join(', ');
|
|
349
|
+
return Result.err(
|
|
350
|
+
new NotFoundError(
|
|
351
|
+
`Example '${exampleName}' not found on trail '${trailId}'. Available: ${listing}.`,
|
|
352
|
+
{
|
|
353
|
+
context: {
|
|
354
|
+
available,
|
|
355
|
+
exampleName,
|
|
356
|
+
trailId,
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const determineMode = (
|
|
364
|
+
example: StructuredTrailExample
|
|
365
|
+
): RunExampleComparisonMode => {
|
|
366
|
+
if (example.error !== undefined) {
|
|
367
|
+
return 'error';
|
|
368
|
+
}
|
|
369
|
+
if (example.expectedMatch !== undefined) {
|
|
370
|
+
return 'expectedMatch';
|
|
371
|
+
}
|
|
372
|
+
if (example.expected !== undefined) {
|
|
373
|
+
return 'expected';
|
|
374
|
+
}
|
|
375
|
+
return 'none';
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const isCurrentAppExampleInput = (input: unknown): boolean =>
|
|
379
|
+
isPlainObject(input) &&
|
|
380
|
+
('rootDir' in input || 'module' in input) &&
|
|
381
|
+
(!('rootDir' in input) || input['rootDir'] === '.') &&
|
|
382
|
+
(!('module' in input) || input['module'] === './src/app.ts');
|
|
383
|
+
|
|
384
|
+
const rebaseCurrentAppMarker = (
|
|
385
|
+
input: Record<string, unknown>,
|
|
386
|
+
target: RunTargetProject,
|
|
387
|
+
selection: CurrentAppExampleSelection
|
|
388
|
+
): Record<string, unknown> => {
|
|
389
|
+
const configuredAppId =
|
|
390
|
+
selection === 'configured-project' &&
|
|
391
|
+
target.context.selectedExtent === 'configured-app'
|
|
392
|
+
? target.context.app.id
|
|
393
|
+
: undefined;
|
|
394
|
+
return {
|
|
395
|
+
...input,
|
|
396
|
+
...(configuredAppId === undefined ? {} : { app: configuredAppId }),
|
|
397
|
+
...('module' in input ? { module: target.modulePath } : {}),
|
|
398
|
+
...('rootDir' in input
|
|
399
|
+
? {
|
|
400
|
+
rootDir:
|
|
401
|
+
configuredAppId === undefined
|
|
402
|
+
? target.rootDir
|
|
403
|
+
: target.context.projectRoot,
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const resolveCurrentAppExampleInput = (
|
|
410
|
+
input: unknown,
|
|
411
|
+
target: RunTargetProject,
|
|
412
|
+
selection: CurrentAppExampleSelection | undefined,
|
|
413
|
+
nestedInputSelection: CurrentAppExampleSelection | undefined
|
|
414
|
+
): unknown => {
|
|
415
|
+
if (
|
|
416
|
+
selection === undefined ||
|
|
417
|
+
!(isPlainObject(input) && isCurrentAppExampleInput(input))
|
|
418
|
+
) {
|
|
419
|
+
return input;
|
|
420
|
+
}
|
|
421
|
+
const resolved = rebaseCurrentAppMarker(input, target, selection);
|
|
422
|
+
return nestedInputSelection !== undefined &&
|
|
423
|
+
isPlainObject(input['input']) &&
|
|
424
|
+
isCurrentAppExampleInput(input['input'])
|
|
425
|
+
? {
|
|
426
|
+
...resolved,
|
|
427
|
+
input: rebaseCurrentAppMarker(
|
|
428
|
+
input['input'],
|
|
429
|
+
target,
|
|
430
|
+
nestedInputSelection
|
|
431
|
+
),
|
|
432
|
+
}
|
|
433
|
+
: resolved;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const buildComparisonEnvelope = async (
|
|
437
|
+
app: Topo,
|
|
438
|
+
trailId: string,
|
|
439
|
+
exampleName: string,
|
|
440
|
+
permit: BasePermit | undefined,
|
|
441
|
+
target: RunTargetProject
|
|
442
|
+
): Promise<Result<RunExampleComparison, Error>> => {
|
|
443
|
+
const exampleResult = findExample(app, trailId, exampleName);
|
|
444
|
+
if (exampleResult.isErr()) {
|
|
445
|
+
return exampleResult;
|
|
446
|
+
}
|
|
447
|
+
const { example, nestedInputSelection, selection } = exampleResult.value;
|
|
448
|
+
const mode = determineMode(example);
|
|
449
|
+
const executed = await run(
|
|
450
|
+
app,
|
|
451
|
+
trailId,
|
|
452
|
+
resolveCurrentAppExampleInput(
|
|
453
|
+
example.input,
|
|
454
|
+
target,
|
|
455
|
+
selection,
|
|
456
|
+
nestedInputSelection
|
|
457
|
+
),
|
|
458
|
+
{
|
|
459
|
+
ctx: {
|
|
460
|
+
cwd: target.rootDir,
|
|
461
|
+
...(permit === undefined ? {} : { permit }),
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
const project = operatorProjectContextOutput(target.context);
|
|
466
|
+
const actual = deriveActualOutcome(executed);
|
|
467
|
+
|
|
468
|
+
if (mode === 'error') {
|
|
469
|
+
const expectedName = example.error ?? '';
|
|
470
|
+
const { diff, match } = compareError(executed, expectedName);
|
|
471
|
+
return Result.ok({
|
|
472
|
+
actual,
|
|
473
|
+
diff,
|
|
474
|
+
exampleName,
|
|
475
|
+
executedAppId: app.name,
|
|
476
|
+
expected: { errorClassName: expectedName },
|
|
477
|
+
input: example.input,
|
|
478
|
+
kind: RUN_EXAMPLE_COMPARISON_KIND,
|
|
479
|
+
match,
|
|
480
|
+
mode,
|
|
481
|
+
project,
|
|
482
|
+
trailId,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (mode === 'expectedMatch') {
|
|
486
|
+
const { diff, match } = compareExpectedMatch(
|
|
487
|
+
executed,
|
|
488
|
+
example.expectedMatch
|
|
489
|
+
);
|
|
490
|
+
return Result.ok({
|
|
491
|
+
actual,
|
|
492
|
+
diff,
|
|
493
|
+
exampleName,
|
|
494
|
+
executedAppId: app.name,
|
|
495
|
+
expected: example.expectedMatch,
|
|
496
|
+
input: example.input,
|
|
497
|
+
kind: RUN_EXAMPLE_COMPARISON_KIND,
|
|
498
|
+
match,
|
|
499
|
+
mode,
|
|
500
|
+
project,
|
|
501
|
+
trailId,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (mode === 'none') {
|
|
505
|
+
return Result.ok({
|
|
506
|
+
actual,
|
|
507
|
+
exampleName,
|
|
508
|
+
executedAppId: app.name,
|
|
509
|
+
expected: undefined,
|
|
510
|
+
input: example.input,
|
|
511
|
+
kind: RUN_EXAMPLE_COMPARISON_KIND,
|
|
512
|
+
match: true,
|
|
513
|
+
mode,
|
|
514
|
+
project,
|
|
515
|
+
trailId,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const { diff, match } = compareExpected(executed, example.expected);
|
|
520
|
+
return Result.ok({
|
|
521
|
+
actual,
|
|
522
|
+
diff,
|
|
523
|
+
exampleName,
|
|
524
|
+
executedAppId: app.name,
|
|
525
|
+
expected: example.expected,
|
|
526
|
+
input: example.input,
|
|
527
|
+
kind: RUN_EXAMPLE_COMPARISON_KIND,
|
|
528
|
+
match,
|
|
529
|
+
mode,
|
|
530
|
+
project,
|
|
531
|
+
trailId,
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const runExampleTrailInputSchema = z.object({
|
|
536
|
+
app: z
|
|
537
|
+
.string()
|
|
538
|
+
.optional()
|
|
539
|
+
.describe(
|
|
540
|
+
'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
|
|
541
|
+
),
|
|
542
|
+
exampleName: z.string().describe('Name of the example to run'),
|
|
543
|
+
id: z.string().describe('Trail ID whose example should run'),
|
|
544
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
545
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
type RunExampleTrailInput = z.output<typeof runExampleTrailInputSchema>;
|
|
549
|
+
|
|
550
|
+
export const runExampleTrail = trail('run.example', {
|
|
551
|
+
args: ['id', 'exampleName'],
|
|
552
|
+
description: 'Run a named example on a trail and compare actual vs expected',
|
|
553
|
+
examples: [
|
|
554
|
+
{
|
|
555
|
+
description: 'Run a named example on a target trail',
|
|
556
|
+
input: buildHappyExampleInput(),
|
|
557
|
+
name: 'Run named example',
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
implementation: async (input: RunExampleTrailInput, ctx) => {
|
|
561
|
+
const target = await resolveRunTargetProject(input, input.id, {
|
|
562
|
+
cwd: ctx.cwd,
|
|
563
|
+
});
|
|
564
|
+
if (target.isErr()) {
|
|
565
|
+
return target;
|
|
566
|
+
}
|
|
567
|
+
return withFreshAppLease(
|
|
568
|
+
target.value.modulePath,
|
|
569
|
+
target.value.rootDir,
|
|
570
|
+
(lease) => {
|
|
571
|
+
const binding = assertConfiguredAppBinding(
|
|
572
|
+
target.value.context,
|
|
573
|
+
lease.app.name
|
|
574
|
+
);
|
|
575
|
+
if (binding.isErr()) {
|
|
576
|
+
return binding;
|
|
577
|
+
}
|
|
578
|
+
return buildComparisonEnvelope(
|
|
579
|
+
lease.app,
|
|
580
|
+
input.id,
|
|
581
|
+
input.exampleName,
|
|
582
|
+
ctx.permit,
|
|
583
|
+
target.value
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
},
|
|
588
|
+
input: runExampleTrailInputSchema,
|
|
589
|
+
intent: 'write',
|
|
590
|
+
output: runExampleComparisonSchema,
|
|
591
|
+
permit: { scopes: ['trails:run'] },
|
|
592
|
+
});
|