@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.16
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 +74 -0
- package/README.md +73 -24
- package/package.json +11 -4
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +28 -4
- package/src/contour.ts +344 -0
- package/src/cross-batch.ts +69 -0
- package/src/cross-schema.ts +36 -0
- package/src/detours.ts +8 -0
- package/src/draft.ts +60 -52
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +245 -48
- package/src/execute.ts +1250 -40
- package/src/fire.ts +1169 -0
- package/src/index.ts +383 -49
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +192 -0
- package/src/layer.ts +81 -0
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +175 -0
- package/src/result.ts +18 -0
- package/src/run.ts +7 -6
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +324 -0
- package/src/serialization.ts +90 -55
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +37 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +245 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +176 -0
- package/src/topo.ts +710 -59
- package/src/tracing.ts +345 -0
- package/src/trail.ts +347 -33
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +138 -0
- package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +208 -17
- package/src/validate-established-topo.ts +8 -2
- package/src/validate-topo.ts +244 -13
- package/src/validation.ts +58 -1
- package/src/webhook.ts +276 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -21
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -44
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -149
- package/dist/derive.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/draft.d.ts +0 -28
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -156
- package/dist/draft.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/execute.d.ts +0 -35
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js +0 -90
- package/dist/execute.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/index.d.ts +0 -48
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/internal/topo-saves.d.ts +0 -47
- package/dist/internal/topo-saves.d.ts.map +0 -1
- package/dist/internal/topo-saves.js +0 -310
- package/dist/internal/topo-saves.js.map +0 -1
- package/dist/internal/topo-store-read.d.ts +0 -67
- package/dist/internal/topo-store-read.d.ts.map +0 -1
- package/dist/internal/topo-store-read.js +0 -222
- package/dist/internal/topo-store-read.js.map +0 -1
- package/dist/internal/topo-store.d.ts +0 -12
- package/dist/internal/topo-store.d.ts.map +0 -1
- package/dist/internal/topo-store.js +0 -571
- package/dist/internal/topo-store.js.map +0 -1
- package/dist/internal/trails-db.d.ts +0 -16
- package/dist/internal/trails-db.d.ts.map +0 -1
- package/dist/internal/trails-db.js +0 -118
- package/dist/internal/trails-db.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -6
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -4
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/run.d.ts +0 -27
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -34
- package/dist/run.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -153
- package/dist/serialization.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/dist/signal.d.ts +0 -33
- package/dist/signal.d.ts.map +0 -1
- package/dist/signal.js +0 -17
- package/dist/signal.js.map +0 -1
- package/dist/topo-store.d.ts +0 -48
- package/dist/topo-store.d.ts.map +0 -1
- package/dist/topo-store.js +0 -175
- package/dist/topo-store.js.map +0 -1
- package/dist/topo.d.ts +0 -25
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -112
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -96
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -20
- package/dist/trail.js.map +0 -1
- package/dist/type-utils.d.ts +0 -24
- package/dist/type-utils.d.ts.map +0 -1
- package/dist/type-utils.js +0 -12
- package/dist/type-utils.js.map +0 -1
- package/dist/types.d.ts +0 -71
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/validate-established-topo.d.ts +0 -76
- package/dist/validate-established-topo.d.ts.map +0 -1
- package/dist/validate-established-topo.js +0 -43
- package/dist/validate-established-topo.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -176
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -165
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -77
- package/src/__tests__/derive.test.ts +0 -260
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/execute.test.ts +0 -525
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/gate.test.ts +0 -125
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/run.test.ts +0 -181
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/service-config.test.ts +0 -228
- package/src/__tests__/service.test.ts +0 -197
- package/src/__tests__/signal.test.ts +0 -82
- package/src/__tests__/topo-store-read.test.ts +0 -251
- package/src/__tests__/topo-store.test.ts +0 -469
- package/src/__tests__/topo.test.ts +0 -277
- package/src/__tests__/trail-permit.test.ts +0 -60
- package/src/__tests__/trail.test.ts +0 -261
- package/src/__tests__/trails-db.test.ts +0 -191
- package/src/__tests__/type-utils.test.ts +0 -90
- package/src/__tests__/validate-topo.test.ts +0 -489
- package/src/__tests__/validation.test.ts +0 -336
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/internal/topo-saves.ts +0 -429
- package/src/internal/topo-store-read.ts +0 -473
- package/src/internal/topo-store.ts +0 -1087
- package/src/provision-config.ts +0 -373
- package/src/provision.ts +0 -148
- package/src/topo-store.ts +0 -301
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
/* oxlint-disable require-await -- trail implementations satisfy async interface without awaiting */
|
|
2
|
-
import { describe, test, expect } from 'bun:test';
|
|
3
|
-
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { InternalError, ValidationError } from '../errors';
|
|
7
|
-
import { executeTrail } from '../execute';
|
|
8
|
-
import { createTrailContext } from '../context';
|
|
9
|
-
import type { Gate } from '../gate';
|
|
10
|
-
import { Result } from '../result';
|
|
11
|
-
import { provision } from '../provision';
|
|
12
|
-
import { trail } from '../trail';
|
|
13
|
-
import type { TrailContext, TrailContextInit } from '../types';
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Fixtures
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
const echoTrail = trail('echo', {
|
|
20
|
-
blaze: (input) => Result.ok({ value: input.value }),
|
|
21
|
-
input: z.object({ value: z.string() }),
|
|
22
|
-
output: z.object({ value: z.string() }),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const failingTrail = trail('fails', {
|
|
26
|
-
blaze: () => Result.err(new ValidationError('bad input')),
|
|
27
|
-
input: z.object({}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const throwingTrail = trail('throws', {
|
|
31
|
-
blaze: () => {
|
|
32
|
-
throw new Error('kaboom');
|
|
33
|
-
},
|
|
34
|
-
input: z.object({}),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const nextProvisionId = (name: string): string =>
|
|
38
|
-
`test.provision.${name}.${Bun.randomUUIDv7()}`;
|
|
39
|
-
|
|
40
|
-
const createResolvedValueProvision = (
|
|
41
|
-
id: string,
|
|
42
|
-
onCreate: () => void,
|
|
43
|
-
value: number
|
|
44
|
-
) =>
|
|
45
|
-
provision(id, {
|
|
46
|
-
create: () => {
|
|
47
|
-
onCreate();
|
|
48
|
-
return Result.ok({ value });
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const createEagerProvisionTrail = (
|
|
53
|
-
id: string,
|
|
54
|
-
counter: ReturnType<typeof createResolvedValueProvision>,
|
|
55
|
-
onRun: (value: number) => void
|
|
56
|
-
) =>
|
|
57
|
-
trail('provision.eager', {
|
|
58
|
-
blaze: (_input, ctx) => {
|
|
59
|
-
const fromAccessor = ctx.provision<{ value: number }>(id);
|
|
60
|
-
const fromDefinition = counter.from(ctx);
|
|
61
|
-
onRun(fromDefinition.value);
|
|
62
|
-
return Result.ok({ total: fromAccessor.value + 1 });
|
|
63
|
-
},
|
|
64
|
-
input: z.object({}),
|
|
65
|
-
output: z.object({ total: z.number() }),
|
|
66
|
-
provisions: [counter],
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const createProvisionProbeGate = (
|
|
70
|
-
counter: ReturnType<typeof createResolvedValueProvision>,
|
|
71
|
-
onResolve: (value: number) => void
|
|
72
|
-
): Gate => ({
|
|
73
|
-
name: 'uses-provision',
|
|
74
|
-
wrap(_trail, impl) {
|
|
75
|
-
return async (input, ctx) => {
|
|
76
|
-
onResolve(ctx.provision<{ value: number }>(counter).value);
|
|
77
|
-
return await impl(input, ctx);
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const createSingletonProvision = (id: string, onCreate: () => number) =>
|
|
83
|
-
provision(id, {
|
|
84
|
-
create: () => Result.ok({ createdAtCall: onCreate() }),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const createSingletonTrail = (
|
|
88
|
-
singleton: ReturnType<typeof createSingletonProvision>
|
|
89
|
-
) =>
|
|
90
|
-
trail('provision.singleton', {
|
|
91
|
-
blaze: (_input, ctx) =>
|
|
92
|
-
Result.ok({
|
|
93
|
-
createdAtCall: singleton.from(ctx).createdAtCall,
|
|
94
|
-
}),
|
|
95
|
-
input: z.object({}),
|
|
96
|
-
output: z.object({ createdAtCall: z.number() }),
|
|
97
|
-
provisions: [singleton],
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const unwrapExecution = async (
|
|
101
|
-
target: ReturnType<typeof createSingletonTrail>
|
|
102
|
-
) => {
|
|
103
|
-
const result = await executeTrail(target, {});
|
|
104
|
-
return result.unwrap();
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Tests
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
|
|
111
|
-
describe('executeTrail', () => {
|
|
112
|
-
describe('happy path', () => {
|
|
113
|
-
test('validates input and executes trail', async () => {
|
|
114
|
-
const result = await executeTrail(echoTrail, { value: 'hello' });
|
|
115
|
-
|
|
116
|
-
expect(result.isOk()).toBe(true);
|
|
117
|
-
expect(result.unwrap()).toEqual({ value: 'hello' });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('eagerly resolves declared provisions before gates and implementation run', async () => {
|
|
121
|
-
const id = nextProvisionId('eager');
|
|
122
|
-
const captures = {
|
|
123
|
-
createCalls: 0,
|
|
124
|
-
gateResolved: undefined as number | undefined,
|
|
125
|
-
runResolved: undefined as number | undefined,
|
|
126
|
-
};
|
|
127
|
-
const counter = createResolvedValueProvision(
|
|
128
|
-
id,
|
|
129
|
-
() => {
|
|
130
|
-
captures.createCalls += 1;
|
|
131
|
-
},
|
|
132
|
-
41
|
|
133
|
-
);
|
|
134
|
-
const layeredTrail = createEagerProvisionTrail(id, counter, (value) => {
|
|
135
|
-
captures.runResolved = value;
|
|
136
|
-
});
|
|
137
|
-
const gate = createProvisionProbeGate(counter, (value) => {
|
|
138
|
-
captures.gateResolved = value;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const result = await executeTrail(layeredTrail, {}, { gates: [gate] });
|
|
142
|
-
|
|
143
|
-
expect(result.unwrap()).toEqual({ total: 42 });
|
|
144
|
-
expect(captures.createCalls).toBe(1);
|
|
145
|
-
expect(captures.gateResolved).toBe(41);
|
|
146
|
-
expect(captures.runResolved).toBe(41);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('awaits async provision factories before running the trail', async () => {
|
|
150
|
-
const db = provision(nextProvisionId('async-factory'), {
|
|
151
|
-
create: async () => {
|
|
152
|
-
await Bun.sleep(0);
|
|
153
|
-
return Result.ok({ source: 'async-factory' });
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
const provisionTrail = trail('provision.async-factory', {
|
|
157
|
-
blaze: (_input, ctx) =>
|
|
158
|
-
Result.ok({ source: db.from(ctx).source as string }),
|
|
159
|
-
input: z.object({}),
|
|
160
|
-
output: z.object({ source: z.string() }),
|
|
161
|
-
provisions: [db],
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const result = await executeTrail(provisionTrail, {});
|
|
165
|
-
|
|
166
|
-
expect(result.isOk()).toBe(true);
|
|
167
|
-
expect(result.unwrap()).toEqual({ source: 'async-factory' });
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('reuses cached singleton provisions across executions', async () => {
|
|
171
|
-
const id = nextProvisionId('singleton');
|
|
172
|
-
const captures = { createCalls: 0 };
|
|
173
|
-
const singleton = createSingletonProvision(id, () => {
|
|
174
|
-
captures.createCalls += 1;
|
|
175
|
-
return captures.createCalls;
|
|
176
|
-
});
|
|
177
|
-
const singletonTrail = createSingletonTrail(singleton);
|
|
178
|
-
|
|
179
|
-
const outputs = [
|
|
180
|
-
await unwrapExecution(singletonTrail),
|
|
181
|
-
await unwrapExecution(singletonTrail),
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
expect(outputs).toEqual([{ createdAtCall: 1 }, { createdAtCall: 1 }]);
|
|
185
|
-
expect(captures.createCalls).toBe(1);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('scopes cached singleton provisions to compatible provision contexts', async () => {
|
|
189
|
-
const id = nextProvisionId('singleton-context');
|
|
190
|
-
const envAwareProvision = provision(id, {
|
|
191
|
-
create: (ctx) => Result.ok({ value: String(ctx.env?.VAL) }),
|
|
192
|
-
});
|
|
193
|
-
const envAwareTrail = trail('provision.singleton-context', {
|
|
194
|
-
blaze: (_input, ctx) =>
|
|
195
|
-
Result.ok({ value: envAwareProvision.from(ctx).value }),
|
|
196
|
-
input: z.object({}),
|
|
197
|
-
output: z.object({ value: z.string() }),
|
|
198
|
-
provisions: [envAwareProvision],
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const first = await executeTrail(
|
|
202
|
-
envAwareTrail,
|
|
203
|
-
{},
|
|
204
|
-
{
|
|
205
|
-
createContext: () =>
|
|
206
|
-
createTrailContext({
|
|
207
|
-
env: { VAL: 'first' },
|
|
208
|
-
}),
|
|
209
|
-
}
|
|
210
|
-
);
|
|
211
|
-
const second = await executeTrail(
|
|
212
|
-
envAwareTrail,
|
|
213
|
-
{},
|
|
214
|
-
{
|
|
215
|
-
createContext: () =>
|
|
216
|
-
createTrailContext({
|
|
217
|
-
env: { VAL: 'second' },
|
|
218
|
-
}),
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
expect(first.unwrap()).toEqual({ value: 'first' });
|
|
223
|
-
expect(second.unwrap()).toEqual({ value: 'second' });
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
describe('validation', () => {
|
|
228
|
-
test('returns validation error for invalid input', async () => {
|
|
229
|
-
const result = await executeTrail(echoTrail, { value: 42 });
|
|
230
|
-
|
|
231
|
-
expect(result.isErr()).toBe(true);
|
|
232
|
-
expect(result.error).toBeInstanceOf(ValidationError);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe('gates', () => {
|
|
237
|
-
test('composes gates around execution', async () => {
|
|
238
|
-
const log: string[] = [];
|
|
239
|
-
const gate: Gate = {
|
|
240
|
-
name: 'test-gate',
|
|
241
|
-
wrap(_trail, impl) {
|
|
242
|
-
return async (input, ctx) => {
|
|
243
|
-
log.push('before');
|
|
244
|
-
const r = await impl(input, ctx);
|
|
245
|
-
log.push('after');
|
|
246
|
-
return r;
|
|
247
|
-
};
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const result = await executeTrail(
|
|
252
|
-
echoTrail,
|
|
253
|
-
{ value: 'x' },
|
|
254
|
-
{ gates: [gate] }
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
expect(result.isOk()).toBe(true);
|
|
258
|
-
expect(log).toEqual(['before', 'after']);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('context', () => {
|
|
263
|
-
test('accepts context overrides', async () => {
|
|
264
|
-
let capturedCtx: TrailContext | undefined;
|
|
265
|
-
const ctxTrail = trail('ctx-test', {
|
|
266
|
-
blaze: (_input, ctx) => {
|
|
267
|
-
capturedCtx = ctx;
|
|
268
|
-
return Result.ok(null);
|
|
269
|
-
},
|
|
270
|
-
input: z.object({}),
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
await executeTrail(ctxTrail, {}, { ctx: { requestId: 'override-id' } });
|
|
274
|
-
|
|
275
|
-
expect(capturedCtx?.requestId).toBe('override-id');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test('accepts abortSignal override', async () => {
|
|
279
|
-
let capturedSignal: AbortSignal | undefined;
|
|
280
|
-
const sigTrail = trail('sig-test', {
|
|
281
|
-
blaze: (_input, ctx) => {
|
|
282
|
-
capturedSignal = ctx.abortSignal;
|
|
283
|
-
return Result.ok(null);
|
|
284
|
-
},
|
|
285
|
-
input: z.object({}),
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const abortSignal = AbortSignal.timeout(9999);
|
|
289
|
-
await executeTrail(sigTrail, {}, { abortSignal });
|
|
290
|
-
|
|
291
|
-
expect(capturedSignal).toBe(abortSignal);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('accepts context factory', async () => {
|
|
295
|
-
let capturedCtx: TrailContext | undefined;
|
|
296
|
-
const ctxTrail = trail('factory-test', {
|
|
297
|
-
blaze: (_input, ctx) => {
|
|
298
|
-
capturedCtx = ctx;
|
|
299
|
-
return Result.ok(null);
|
|
300
|
-
},
|
|
301
|
-
input: z.object({}),
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const customCtx: TrailContextInit = {
|
|
305
|
-
abortSignal: new AbortController().signal,
|
|
306
|
-
cwd: '/custom',
|
|
307
|
-
requestId: 'factory-id',
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
await executeTrail(ctxTrail, {}, { createContext: () => customCtx });
|
|
311
|
-
|
|
312
|
-
expect(capturedCtx?.requestId).toBe('factory-id');
|
|
313
|
-
expect(capturedCtx?.cwd).toBe('/custom');
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test('context factory + ctx overrides merge correctly', async () => {
|
|
317
|
-
let capturedCtx: TrailContext | undefined;
|
|
318
|
-
const ctxTrail = trail('merge-test', {
|
|
319
|
-
blaze: (_input, ctx) => {
|
|
320
|
-
capturedCtx = ctx;
|
|
321
|
-
return Result.ok(null);
|
|
322
|
-
},
|
|
323
|
-
input: z.object({}),
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const baseCtx: TrailContextInit = {
|
|
327
|
-
abortSignal: new AbortController().signal,
|
|
328
|
-
cwd: '/factory',
|
|
329
|
-
requestId: 'factory-id',
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
await executeTrail(
|
|
333
|
-
ctxTrail,
|
|
334
|
-
{},
|
|
335
|
-
{
|
|
336
|
-
createContext: () => baseCtx,
|
|
337
|
-
ctx: { requestId: 'overridden-id' },
|
|
338
|
-
}
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
expect(capturedCtx?.requestId).toBe('overridden-id');
|
|
342
|
-
expect(capturedCtx?.cwd).toBe('/factory');
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('deep-merges extensions from factory and overrides', async () => {
|
|
346
|
-
let captured: TrailContext | undefined;
|
|
347
|
-
const t = trail('ext.test', {
|
|
348
|
-
blaze: (_input, ctx) => {
|
|
349
|
-
captured = ctx;
|
|
350
|
-
return Result.ok({});
|
|
351
|
-
},
|
|
352
|
-
input: z.object({}),
|
|
353
|
-
output: z.object({}),
|
|
354
|
-
});
|
|
355
|
-
await executeTrail(
|
|
356
|
-
t,
|
|
357
|
-
{},
|
|
358
|
-
{
|
|
359
|
-
createContext: () =>
|
|
360
|
-
createTrailContext({ extensions: { store: 'db' } }),
|
|
361
|
-
ctx: { extensions: { userId: '123' } },
|
|
362
|
-
}
|
|
363
|
-
);
|
|
364
|
-
expect(captured?.extensions).toEqual({ store: 'db', userId: '123' });
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test('rebinds ctx.provision after merging extension overrides from createContext', async () => {
|
|
368
|
-
let resolvedSource: string | undefined;
|
|
369
|
-
const db = provision(nextProvisionId('context-override'), {
|
|
370
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
371
|
-
});
|
|
372
|
-
const provisionTrail = trail('ctx.provision.override', {
|
|
373
|
-
blaze: (_input, ctx) => {
|
|
374
|
-
resolvedSource = ctx.provision<{ source: string }>(db).source;
|
|
375
|
-
return Result.ok({ source: resolvedSource });
|
|
376
|
-
},
|
|
377
|
-
input: z.object({}),
|
|
378
|
-
output: z.object({ source: z.string() }),
|
|
379
|
-
provisions: [db],
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const result = await executeTrail(
|
|
383
|
-
provisionTrail,
|
|
384
|
-
{},
|
|
385
|
-
{
|
|
386
|
-
createContext: () =>
|
|
387
|
-
createTrailContext({
|
|
388
|
-
extensions: {
|
|
389
|
-
[db.id]: { source: 'factory-context' },
|
|
390
|
-
},
|
|
391
|
-
}),
|
|
392
|
-
ctx: {
|
|
393
|
-
extensions: {
|
|
394
|
-
[db.id]: { source: 'override-context' },
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
}
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
expect(result.unwrap()).toEqual({ source: 'override-context' });
|
|
401
|
-
expect(resolvedSource).toBe('override-context');
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
test('context factory seeds the provision accessor when omitted', async () => {
|
|
405
|
-
let capturedCtx: TrailContext | undefined;
|
|
406
|
-
const id = nextProvisionId('factory-seed');
|
|
407
|
-
const widget = { id: 'widget-1' };
|
|
408
|
-
const widgetTrail = trail('provision.factory-seed', {
|
|
409
|
-
blaze: (_input, ctx) => {
|
|
410
|
-
capturedCtx = ctx;
|
|
411
|
-
return Result.ok(null);
|
|
412
|
-
},
|
|
413
|
-
input: z.object({}),
|
|
414
|
-
provisions: [],
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
await executeTrail(
|
|
418
|
-
widgetTrail,
|
|
419
|
-
{},
|
|
420
|
-
{
|
|
421
|
-
createContext: () => ({
|
|
422
|
-
abortSignal: new AbortController().signal,
|
|
423
|
-
extensions: { [id]: widget },
|
|
424
|
-
requestId: 'seeded-provision',
|
|
425
|
-
}),
|
|
426
|
-
}
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
expect(capturedCtx?.provision(id)).toBe(widget);
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
describe('error handling', () => {
|
|
434
|
-
test('propagates Result.err from run function', async () => {
|
|
435
|
-
const result = await executeTrail(failingTrail, {});
|
|
436
|
-
|
|
437
|
-
expect(result.isErr()).toBe(true);
|
|
438
|
-
expect(result.error).toBeInstanceOf(ValidationError);
|
|
439
|
-
expect(result.error.message).toBe('bad input');
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test('catches thrown exceptions and returns InternalError', async () => {
|
|
443
|
-
const result = await executeTrail(throwingTrail, {});
|
|
444
|
-
|
|
445
|
-
expect(result.isErr()).toBe(true);
|
|
446
|
-
expect(result.error).toBeInstanceOf(InternalError);
|
|
447
|
-
expect(result.error.message).toBe('kaboom');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test('short-circuits when a provision factory returns Result.err', async () => {
|
|
451
|
-
const failingProvision = provision(nextProvisionId('factory-error'), {
|
|
452
|
-
create: () =>
|
|
453
|
-
Result.err(new ValidationError('DATABASE_URL is required')),
|
|
454
|
-
});
|
|
455
|
-
let ran = false;
|
|
456
|
-
const provisionTrail = trail('provision.factory-error', {
|
|
457
|
-
blaze: () => {
|
|
458
|
-
ran = true;
|
|
459
|
-
return Result.ok(null);
|
|
460
|
-
},
|
|
461
|
-
input: z.object({}),
|
|
462
|
-
provisions: [failingProvision],
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
const result = await executeTrail(provisionTrail, {});
|
|
466
|
-
|
|
467
|
-
expect(result.isErr()).toBe(true);
|
|
468
|
-
expect(result.error).toBeInstanceOf(ValidationError);
|
|
469
|
-
expect(result.error.message).toBe('DATABASE_URL is required');
|
|
470
|
-
expect(ran).toBe(false);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
test('wraps thrown provision factory exceptions with the provision ID in context', async () => {
|
|
474
|
-
const explodingProvision = provision(nextProvisionId('factory-throw'), {
|
|
475
|
-
create: () => {
|
|
476
|
-
throw new Error('boom');
|
|
477
|
-
},
|
|
478
|
-
});
|
|
479
|
-
const provisionTrail = trail('provision.factory-throw', {
|
|
480
|
-
blaze: () => Result.ok(null),
|
|
481
|
-
input: z.object({}),
|
|
482
|
-
provisions: [explodingProvision],
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
const result = await executeTrail(provisionTrail, {});
|
|
486
|
-
|
|
487
|
-
expect(result.isErr()).toBe(true);
|
|
488
|
-
expect(result.error).toBeInstanceOf(InternalError);
|
|
489
|
-
expect(result.error.message).toContain(explodingProvision.id);
|
|
490
|
-
expect(result.error.context).toEqual({
|
|
491
|
-
provisionId: explodingProvision.id,
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
test('prefers explicit provision overrides over cached or created instances', async () => {
|
|
496
|
-
const id = nextProvisionId('override');
|
|
497
|
-
let createCalls = 0;
|
|
498
|
-
const db = provision(id, {
|
|
499
|
-
create: () => {
|
|
500
|
-
createCalls += 1;
|
|
501
|
-
return Result.ok({ source: 'factory' });
|
|
502
|
-
},
|
|
503
|
-
});
|
|
504
|
-
const provisionTrail = trail('provision.override', {
|
|
505
|
-
blaze: (_input, ctx) =>
|
|
506
|
-
Result.ok({ source: db.from(ctx).source as string }),
|
|
507
|
-
input: z.object({}),
|
|
508
|
-
output: z.object({ source: z.string() }),
|
|
509
|
-
provisions: [db],
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
const result = await executeTrail(
|
|
513
|
-
provisionTrail,
|
|
514
|
-
{},
|
|
515
|
-
{
|
|
516
|
-
provisions: { [id]: { source: 'override' } },
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
expect(result.isOk()).toBe(true);
|
|
521
|
-
expect(result.unwrap()).toEqual({ source: 'override' });
|
|
522
|
-
expect(createCalls).toBe(0);
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
});
|