@ontrails/core 1.0.0-beta.5 → 1.0.0-beta.50
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 +459 -14
- package/README.md +103 -28
- package/package.json +16 -1
- package/src/activation-provenance.ts +116 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-derivation.ts +227 -0
- package/src/activation-source.ts +93 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +59 -11
- package/src/derive.ts +271 -19
- package/src/detours.ts +8 -0
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +350 -0
- package/src/entity.ts +346 -0
- package/src/error-rendering.ts +87 -0
- package/src/errors.ts +335 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1172 -0
- package/src/glob.ts +81 -0
- package/src/index.ts +569 -33
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-field-rendering.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-scope.ts +66 -0
- package/src/path-security.ts +11 -3
- package/src/permits.ts +12 -0
- package/src/queue.ts +163 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +804 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +18 -0
- package/src/run.ts +76 -0
- package/src/runtime-builtins.ts +69 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +92 -55
- package/src/sha256.ts +136 -0
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +111 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +101 -0
- package/src/surface-overlay.ts +694 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +762 -38
- package/src/tracing.ts +346 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +1244 -34
- package/src/trails/derive-trail.ts +835 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +152 -0
- package/src/trails-db.ts +212 -0
- package/src/transport-error-map.ts +163 -0
- package/src/type-utils.ts +63 -10
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +73 -0
- package/src/validate-topo.ts +569 -55
- package/src/validation.ts +152 -4
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +461 -0
- package/src/workspace.ts +183 -16
- 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 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -125
- package/dist/derive.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 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.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/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 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -39
- package/dist/index.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 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- 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/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/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/topo.d.ts +0 -16
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -66
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -89
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -18
- package/dist/trail.js.map +0 -1
- package/dist/type-utils.d.ts +0 -14
- 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 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.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 -158
- 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 -134
- 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 -66
- package/src/__tests__/derive.test.ts +0 -203
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -212
- 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__/serialization.test.ts +0 -401
- package/src/__tests__/topo.test.ts +0 -181
- package/src/__tests__/trail.test.ts +0 -217
- package/src/__tests__/type-utils.test.ts +0 -63
- package/src/__tests__/validate-topo.test.ts +0 -278
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { Result } from '../result';
|
|
6
|
-
import { trail } from '../trail';
|
|
7
|
-
import type { TrailContext } from '../types';
|
|
8
|
-
|
|
9
|
-
const stubCtx: TrailContext = {
|
|
10
|
-
requestId: 'test-123',
|
|
11
|
-
signal: AbortSignal.timeout(5000),
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
describe('trail()', () => {
|
|
15
|
-
const inputSchema = z.object({ name: z.string() });
|
|
16
|
-
const outputSchema = z.object({ greeting: z.string() });
|
|
17
|
-
|
|
18
|
-
const greet = trail('greet', {
|
|
19
|
-
description: 'Greet someone',
|
|
20
|
-
input: inputSchema,
|
|
21
|
-
output: outputSchema,
|
|
22
|
-
run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('basics', () => {
|
|
26
|
-
test('returns correct id', () => {
|
|
27
|
-
expect(greet.id).toBe('greet');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("returns kind 'trail'", () => {
|
|
31
|
-
expect(greet.kind).toBe('trail');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('preserves input schema', () => {
|
|
35
|
-
const parsed = greet.input.safeParse({ name: 'World' });
|
|
36
|
-
expect(parsed.success).toBe(true);
|
|
37
|
-
|
|
38
|
-
const bad = greet.input.safeParse({ name: 42 });
|
|
39
|
-
expect(bad.success).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('output schema is optional', () => {
|
|
43
|
-
const minimal = trail('noop', {
|
|
44
|
-
input: z.object({}),
|
|
45
|
-
run: () => Result.ok(),
|
|
46
|
-
});
|
|
47
|
-
expect(minimal.output).toBeUndefined();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('implementation is callable', async () => {
|
|
51
|
-
const result = await greet.run({ name: 'World' }, stubCtx);
|
|
52
|
-
expect(result.isOk()).toBe(true);
|
|
53
|
-
expect(result.unwrap()).toEqual({ greeting: 'Hello, World!' });
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('metadata', () => {
|
|
58
|
-
test('examples are stored', () => {
|
|
59
|
-
const withExamples = trail('echo', {
|
|
60
|
-
examples: [
|
|
61
|
-
{ error: 'ValidationError', input: { text: '' }, name: 'error-case' },
|
|
62
|
-
{ expected: { text: 'hi' }, input: { text: 'hi' }, name: 'basic' },
|
|
63
|
-
],
|
|
64
|
-
input: z.object({ text: z.string() }),
|
|
65
|
-
run: (input) => Result.ok({ text: input.text }),
|
|
66
|
-
});
|
|
67
|
-
expect(withExamples.examples).toHaveLength(2);
|
|
68
|
-
const first = withExamples.examples?.[0];
|
|
69
|
-
expect(first?.name).toBe('error-case');
|
|
70
|
-
const second = withExamples.examples?.[1];
|
|
71
|
-
expect(second?.name).toBe('basic');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('metadata is stored', () => {
|
|
75
|
-
const withMetadata = trail('tagged', {
|
|
76
|
-
input: z.object({}),
|
|
77
|
-
metadata: { domain: 'billing', tier: 1 },
|
|
78
|
-
run: () => Result.ok(),
|
|
79
|
-
});
|
|
80
|
-
expect(withMetadata.metadata).toEqual({ domain: 'billing', tier: 1 });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('detours are stored', () => {
|
|
84
|
-
const withDetours = trail('orchestrator', {
|
|
85
|
-
detours: {
|
|
86
|
-
onFailure: ['alert'],
|
|
87
|
-
onSuccess: ['notify', 'audit'],
|
|
88
|
-
},
|
|
89
|
-
input: z.object({}),
|
|
90
|
-
run: () => Result.ok(),
|
|
91
|
-
});
|
|
92
|
-
expect(withDetours.detours).toEqual({
|
|
93
|
-
onFailure: ['alert'],
|
|
94
|
-
onSuccess: ['notify', 'audit'],
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('follow', () => {
|
|
100
|
-
test('defaults to empty frozen array when omitted', () => {
|
|
101
|
-
const minimal = trail('bare', {
|
|
102
|
-
input: z.object({}),
|
|
103
|
-
run: () => Result.ok(),
|
|
104
|
-
});
|
|
105
|
-
expect(minimal.follow).toEqual([]);
|
|
106
|
-
expect(Object.isFrozen(minimal.follow)).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('preserves follow array', () => {
|
|
110
|
-
const withFollow = trail('composed', {
|
|
111
|
-
follow: ['authenticate', 'validate-session'],
|
|
112
|
-
input: z.object({}),
|
|
113
|
-
run: () => Result.ok(),
|
|
114
|
-
});
|
|
115
|
-
expect(withFollow.follow).toEqual(['authenticate', 'validate-session']);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('follow array is frozen', () => {
|
|
119
|
-
const withFollow = trail('composed', {
|
|
120
|
-
follow: ['authenticate'],
|
|
121
|
-
input: z.object({}),
|
|
122
|
-
run: () => Result.ok(),
|
|
123
|
-
});
|
|
124
|
-
expect(Object.isFrozen(withFollow.follow)).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('intent and idempotent', () => {
|
|
129
|
-
test('intent defaults to write', () => {
|
|
130
|
-
const minimal = trail('bare', {
|
|
131
|
-
input: z.object({}),
|
|
132
|
-
run: () => Result.ok(),
|
|
133
|
-
});
|
|
134
|
-
expect(minimal.intent).toBe('write');
|
|
135
|
-
expect(minimal.idempotent).toBeUndefined();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('intent is preserved when set', () => {
|
|
139
|
-
const readTrail = trail('reader', {
|
|
140
|
-
input: z.object({}),
|
|
141
|
-
intent: 'read',
|
|
142
|
-
run: () => Result.ok(),
|
|
143
|
-
});
|
|
144
|
-
expect(readTrail.intent).toBe('read');
|
|
145
|
-
|
|
146
|
-
const destroyTrail = trail('destroyer', {
|
|
147
|
-
input: z.object({}),
|
|
148
|
-
intent: 'destroy',
|
|
149
|
-
run: () => Result.ok(),
|
|
150
|
-
});
|
|
151
|
-
expect(destroyTrail.intent).toBe('destroy');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('idempotent is preserved when set', () => {
|
|
155
|
-
const t = trail('idempotent', {
|
|
156
|
-
idempotent: true,
|
|
157
|
-
input: z.object({}),
|
|
158
|
-
run: () => Result.ok(),
|
|
159
|
-
});
|
|
160
|
-
expect(t.idempotent).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('single-object overload', () => {
|
|
165
|
-
test('accepts spec with id property', () => {
|
|
166
|
-
const t = trail({
|
|
167
|
-
id: 'entity.show',
|
|
168
|
-
input: inputSchema,
|
|
169
|
-
run: (input: { name: string }, _ctx: TrailContext) =>
|
|
170
|
-
Result.ok({ greeting: `Hi, ${input.name}` }),
|
|
171
|
-
});
|
|
172
|
-
expect(t.id).toBe('entity.show');
|
|
173
|
-
expect(t.kind).toBe('trail');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('preserves all spec fields', () => {
|
|
177
|
-
const t = trail({
|
|
178
|
-
description: 'A full trail',
|
|
179
|
-
examples: [{ input: { name: 'World' }, name: 'test' }],
|
|
180
|
-
id: 'full',
|
|
181
|
-
input: inputSchema,
|
|
182
|
-
intent: 'read',
|
|
183
|
-
output: outputSchema,
|
|
184
|
-
run: (input: { name: string }, _ctx: TrailContext) =>
|
|
185
|
-
Result.ok({ greeting: `Hi, ${input.name}` }),
|
|
186
|
-
});
|
|
187
|
-
expect(t.description).toBe('A full trail');
|
|
188
|
-
expect(t.intent).toBe('read');
|
|
189
|
-
expect(t.examples).toHaveLength(1);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('implementation is callable', async () => {
|
|
193
|
-
const t = trail({
|
|
194
|
-
id: 'callable',
|
|
195
|
-
input: z.object({ x: z.number() }),
|
|
196
|
-
run: (input: { x: number }) => Result.ok(input.x * 2),
|
|
197
|
-
});
|
|
198
|
-
const result = await t.run({ x: 5 }, stubCtx);
|
|
199
|
-
expect(result.isOk()).toBe(true);
|
|
200
|
-
expect(result.unwrap()).toBe(10);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('sync implementations are normalized to an awaitable runtime function', async () => {
|
|
204
|
-
const t = trail('normalized', {
|
|
205
|
-
input: z.object({ value: z.number() }),
|
|
206
|
-
run: (input: { value: number }) => Result.ok(input.value + 1),
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const promise = t.run({ value: 2 }, stubCtx);
|
|
210
|
-
expect(promise).toBeInstanceOf(Promise);
|
|
211
|
-
|
|
212
|
-
const result = await promise;
|
|
213
|
-
expect(result.isOk()).toBe(true);
|
|
214
|
-
expect(result.unwrap()).toBe(3);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { Result } from '../result';
|
|
6
|
-
import { trail } from '../trail';
|
|
7
|
-
import type { TrailInput, TrailOutput } from '../type-utils';
|
|
8
|
-
import { inputOf, outputOf } from '../type-utils';
|
|
9
|
-
|
|
10
|
-
const greetTrail = trail('greet', {
|
|
11
|
-
input: z.object({ name: z.string() }),
|
|
12
|
-
output: z.object({ message: z.string() }),
|
|
13
|
-
run: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const noOutputTrail = trail('ping', {
|
|
17
|
-
input: z.object({}),
|
|
18
|
-
run: () => Result.ok(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('type-utils', () => {
|
|
22
|
-
describe('inputOf', () => {
|
|
23
|
-
test('returns the Zod input schema and can parse valid input', () => {
|
|
24
|
-
const schema = inputOf(greetTrail);
|
|
25
|
-
const result = schema.safeParse({ name: 'Alice' });
|
|
26
|
-
expect(result.success).toBe(true);
|
|
27
|
-
expect(
|
|
28
|
-
(result as { success: true; data: { name: string } }).data
|
|
29
|
-
).toEqual({
|
|
30
|
-
name: 'Alice',
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('outputOf', () => {
|
|
36
|
-
test('returns the Zod output schema when defined', () => {
|
|
37
|
-
const schema = outputOf(greetTrail);
|
|
38
|
-
expect(schema).toBeDefined();
|
|
39
|
-
// oxlint-disable-next-line no-non-null-assertion -- guarded by toBeDefined() above
|
|
40
|
-
const result = schema!.safeParse({ message: 'hello' });
|
|
41
|
-
expect(result.success).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('returns undefined when no output schema', () => {
|
|
45
|
-
const schema = outputOf(noOutputTrail);
|
|
46
|
-
expect(schema).toBeUndefined();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('type-level checks', () => {
|
|
51
|
-
test('TrailInput matches expected shape', () => {
|
|
52
|
-
// If this compiles, the type is correct
|
|
53
|
-
const _input: TrailInput<typeof greetTrail> = { name: 'test' };
|
|
54
|
-
expect(_input.name).toBe('test');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('TrailOutput matches expected shape', () => {
|
|
58
|
-
// If this compiles, the type is correct
|
|
59
|
-
const _output: TrailOutput<typeof greetTrail> = { message: 'hello' };
|
|
60
|
-
expect(_output.message).toBe('hello');
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { Result } from '../result.js';
|
|
6
|
-
import { topo } from '../topo.js';
|
|
7
|
-
import type { TopoIssue } from '../validate-topo.js';
|
|
8
|
-
import { validateTopo } from '../validate-topo.js';
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
// oxlint-disable-next-line require-await -- satisfies async interface
|
|
15
|
-
const noop = async () => Result.ok();
|
|
16
|
-
|
|
17
|
-
const mockTrail = (
|
|
18
|
-
id: string,
|
|
19
|
-
overrides?: {
|
|
20
|
-
follow?: readonly string[];
|
|
21
|
-
examples?: readonly {
|
|
22
|
-
name: string;
|
|
23
|
-
input: unknown;
|
|
24
|
-
expected?: unknown;
|
|
25
|
-
error?: string;
|
|
26
|
-
}[];
|
|
27
|
-
output?: z.ZodType;
|
|
28
|
-
}
|
|
29
|
-
) => ({
|
|
30
|
-
follow: Object.freeze([...(overrides?.follow ?? [])]),
|
|
31
|
-
id,
|
|
32
|
-
input: z.object({ name: z.string() }),
|
|
33
|
-
kind: 'trail' as const,
|
|
34
|
-
run: noop,
|
|
35
|
-
...overrides,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const mockEvent = (id: string, from?: readonly string[]) => ({
|
|
39
|
-
from,
|
|
40
|
-
id,
|
|
41
|
-
kind: 'event' as const,
|
|
42
|
-
payload: z.object({ data: z.string() }),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/** Extract issues from a failed validateTopo result. */
|
|
46
|
-
const extractIssues = (result: Result<void, Error>): TopoIssue[] => {
|
|
47
|
-
if (result.isOk()) {
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
const ctx = (result.error as { context?: { issues?: TopoIssue[] } }).context;
|
|
51
|
-
return (ctx?.issues ?? []) as TopoIssue[];
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Tests
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
describe('validateTopo', () => {
|
|
59
|
-
test('valid topo passes', () => {
|
|
60
|
-
const app = topo('app', {
|
|
61
|
-
add: mockTrail('entity.add'),
|
|
62
|
-
onboard: mockTrail('entity.onboard', {
|
|
63
|
-
follow: ['entity.add'],
|
|
64
|
-
}),
|
|
65
|
-
updated: mockEvent('entity.updated', ['entity.add']),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const result = validateTopo(app);
|
|
69
|
-
expect(result.isOk()).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('trail follow', () => {
|
|
73
|
-
test('trail following non-existent trail fails', () => {
|
|
74
|
-
const app = topo('app', {
|
|
75
|
-
onboard: mockTrail('entity.onboard', {
|
|
76
|
-
follow: ['entity.missing'],
|
|
77
|
-
}),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const result = validateTopo(app);
|
|
81
|
-
expect(result.isErr()).toBe(true);
|
|
82
|
-
|
|
83
|
-
const issues = extractIssues(result);
|
|
84
|
-
expect(issues).toHaveLength(1);
|
|
85
|
-
expect(issues[0]?.rule).toBe('follow-exists');
|
|
86
|
-
expect(issues[0]?.message).toContain('entity.missing');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('trail following itself fails', () => {
|
|
90
|
-
const app = topo('app', {
|
|
91
|
-
loop: mockTrail('entity.loop', { follow: ['entity.loop'] }),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const result = validateTopo(app);
|
|
95
|
-
expect(result.isErr()).toBe(true);
|
|
96
|
-
|
|
97
|
-
const issues = extractIssues(result);
|
|
98
|
-
expect(issues.some((i) => i.rule === 'no-self-follow')).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('two-node cycle (a→b→a) is detected', () => {
|
|
102
|
-
const app = topo('app', {
|
|
103
|
-
a: mockTrail('a', { follow: ['b'] }),
|
|
104
|
-
b: mockTrail('b', { follow: ['a'] }),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const result = validateTopo(app);
|
|
108
|
-
expect(result.isErr()).toBe(true);
|
|
109
|
-
|
|
110
|
-
const issues = extractIssues(result);
|
|
111
|
-
const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
|
|
112
|
-
expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
|
|
113
|
-
expect(cycleIssues[0]?.message).toContain('Cycle detected');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('three-node cycle (a→b→c→a) is detected', () => {
|
|
117
|
-
const app = topo('app', {
|
|
118
|
-
a: mockTrail('a', { follow: ['b'] }),
|
|
119
|
-
b: mockTrail('b', { follow: ['c'] }),
|
|
120
|
-
c: mockTrail('c', { follow: ['a'] }),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const result = validateTopo(app);
|
|
124
|
-
expect(result.isErr()).toBe(true);
|
|
125
|
-
|
|
126
|
-
const issues = extractIssues(result);
|
|
127
|
-
const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
|
|
128
|
-
expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
|
|
129
|
-
expect(cycleIssues[0]?.message).toContain('Cycle detected');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('valid DAG with shared targets is not flagged', () => {
|
|
133
|
-
const app = topo('app', {
|
|
134
|
-
a: mockTrail('a', { follow: ['c'] }),
|
|
135
|
-
b: mockTrail('b', { follow: ['c'] }),
|
|
136
|
-
c: mockTrail('c'),
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const result = validateTopo(app);
|
|
140
|
-
expect(result.isOk()).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('example validation', () => {
|
|
145
|
-
test('example with invalid input fails', () => {
|
|
146
|
-
const app = topo('app', {
|
|
147
|
-
show: mockTrail('entity.show', {
|
|
148
|
-
examples: [{ input: { name: 123 }, name: 'Bad input' }],
|
|
149
|
-
}),
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const result = validateTopo(app);
|
|
153
|
-
expect(result.isErr()).toBe(true);
|
|
154
|
-
|
|
155
|
-
const issues = extractIssues(result);
|
|
156
|
-
expect(issues).toHaveLength(1);
|
|
157
|
-
expect(issues[0]?.rule).toBe('example-input-valid');
|
|
158
|
-
expect(issues[0]?.message).toContain('Bad input');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('example with expected output but no output schema warns', () => {
|
|
162
|
-
const app = topo('app', {
|
|
163
|
-
show: mockTrail('entity.show', {
|
|
164
|
-
examples: [
|
|
165
|
-
{
|
|
166
|
-
expected: { result: 'ok' },
|
|
167
|
-
input: { name: 'test' },
|
|
168
|
-
name: 'Has expected',
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
}),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const result = validateTopo(app);
|
|
175
|
-
expect(result.isErr()).toBe(true);
|
|
176
|
-
|
|
177
|
-
const issues = extractIssues(result);
|
|
178
|
-
expect(issues).toHaveLength(1);
|
|
179
|
-
expect(issues[0]?.rule).toBe('output-schema-present');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('ValidationError example with invalid input is allowed', () => {
|
|
183
|
-
const app = topo('app', {
|
|
184
|
-
show: mockTrail('entity.show', {
|
|
185
|
-
examples: [
|
|
186
|
-
{
|
|
187
|
-
error: 'ValidationError',
|
|
188
|
-
input: { name: 123 },
|
|
189
|
-
name: 'Error case',
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
}),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const result = validateTopo(app);
|
|
196
|
-
expect(result.isOk()).toBe(true);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('NotFoundError example with invalid input fails', () => {
|
|
200
|
-
const app = topo('app', {
|
|
201
|
-
show: mockTrail('entity.show', {
|
|
202
|
-
examples: [
|
|
203
|
-
{
|
|
204
|
-
error: 'NotFoundError',
|
|
205
|
-
input: { name: 123 },
|
|
206
|
-
name: 'Not found case',
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
}),
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const result = validateTopo(app);
|
|
213
|
-
expect(result.isErr()).toBe(true);
|
|
214
|
-
|
|
215
|
-
const issues = extractIssues(result);
|
|
216
|
-
expect(issues).toHaveLength(1);
|
|
217
|
-
expect(issues[0]?.rule).toBe('example-input-valid');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('NotFoundError example with valid input passes', () => {
|
|
221
|
-
const app = topo('app', {
|
|
222
|
-
show: mockTrail('entity.show', {
|
|
223
|
-
examples: [
|
|
224
|
-
{
|
|
225
|
-
error: 'NotFoundError',
|
|
226
|
-
input: { name: 'test' },
|
|
227
|
-
name: 'Not found case',
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
}),
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const result = validateTopo(app);
|
|
234
|
-
expect(result.isOk()).toBe(true);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe('event origins', () => {
|
|
239
|
-
test('event with non-existent origin fails', () => {
|
|
240
|
-
const app = topo('app', {
|
|
241
|
-
updated: mockEvent('entity.updated', ['entity.ghost']),
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const result = validateTopo(app);
|
|
245
|
-
expect(result.isErr()).toBe(true);
|
|
246
|
-
|
|
247
|
-
const issues = extractIssues(result);
|
|
248
|
-
expect(issues).toHaveLength(1);
|
|
249
|
-
expect(issues[0]?.rule).toBe('event-origin-exists');
|
|
250
|
-
expect(issues[0]?.message).toContain('entity.ghost');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test('event without origins is accepted', () => {
|
|
254
|
-
const app = topo('app', {
|
|
255
|
-
updated: mockEvent('entity.updated'),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const result = validateTopo(app);
|
|
259
|
-
expect(result.isOk()).toBe(true);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('collects multiple issues', () => {
|
|
264
|
-
const app = topo('app', {
|
|
265
|
-
broken: mockTrail('entity.broken', { follow: ['entity.missing'] }),
|
|
266
|
-
show: mockTrail('entity.show', {
|
|
267
|
-
examples: [{ input: { name: 123 }, name: 'Bad' }],
|
|
268
|
-
}),
|
|
269
|
-
updated: mockEvent('entity.updated', ['entity.ghost']),
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const result = validateTopo(app);
|
|
273
|
-
expect(result.isErr()).toBe(true);
|
|
274
|
-
|
|
275
|
-
const issues = extractIssues(result);
|
|
276
|
-
expect(issues).toHaveLength(3);
|
|
277
|
-
});
|
|
278
|
-
});
|