@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.30
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 +305 -8
- package/README.md +114 -33
- package/package.json +11 -1
- 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/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +241 -16
- package/src/detours.ts +8 -0
- package/src/draft.ts +342 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +335 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/index.ts +495 -34
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- 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 +194 -0
- package/src/result.ts +18 -0
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +92 -55
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -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 +176 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -67
- package/src/tracing.ts +345 -0
- package/src/trail.ts +1168 -30
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +206 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +393 -45
- 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 +278 -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 -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/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -40
- 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 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -80
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.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 -156
- 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__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- 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 -220
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -280
- 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/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ValidationError,
|
|
5
|
-
AmbiguousError,
|
|
6
|
-
AssertionError,
|
|
7
|
-
NetworkError,
|
|
8
|
-
RateLimitError,
|
|
9
|
-
InternalError,
|
|
10
|
-
TimeoutError,
|
|
11
|
-
NotFoundError,
|
|
12
|
-
AlreadyExistsError,
|
|
13
|
-
ConflictError,
|
|
14
|
-
PermissionError,
|
|
15
|
-
AuthError,
|
|
16
|
-
CancelledError,
|
|
17
|
-
} from '../errors.js';
|
|
18
|
-
import {
|
|
19
|
-
serializeError,
|
|
20
|
-
deserializeError,
|
|
21
|
-
safeStringify,
|
|
22
|
-
} from '../serialization.js';
|
|
23
|
-
import { Result } from '../result.js';
|
|
24
|
-
import type { SerializedError } from '../serialization.js';
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// serializeError
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
describe('serializeError', () => {
|
|
31
|
-
test('serializes a plain Error', () => {
|
|
32
|
-
const err = new Error('boom');
|
|
33
|
-
const serialized = serializeError(err);
|
|
34
|
-
expect(serialized.name).toBe('Error');
|
|
35
|
-
expect(serialized.message).toBe('boom');
|
|
36
|
-
expect(serialized.stack).toBeDefined();
|
|
37
|
-
expect(serialized.category).toBeUndefined();
|
|
38
|
-
expect(serialized.retryable).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('serializes a TrailsError with category', () => {
|
|
42
|
-
const err = new NetworkError('disconnected');
|
|
43
|
-
const serialized = serializeError(err);
|
|
44
|
-
expect(serialized.name).toBe('NetworkError');
|
|
45
|
-
expect(serialized.message).toBe('disconnected');
|
|
46
|
-
expect(serialized.category).toBe('network');
|
|
47
|
-
expect(serialized.retryable).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('serializes context', () => {
|
|
51
|
-
const err = new ValidationError('bad input', {
|
|
52
|
-
context: { field: 'email' },
|
|
53
|
-
});
|
|
54
|
-
const serialized = serializeError(err);
|
|
55
|
-
expect(serialized.context).toEqual({ field: 'email' });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('serializes RateLimitError.retryAfter', () => {
|
|
59
|
-
const err = new RateLimitError('slow down', { retryAfter: 30 });
|
|
60
|
-
const serialized = serializeError(err);
|
|
61
|
-
expect(serialized.retryAfter).toBe(30);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('retryAfter is undefined for non-RateLimitError', () => {
|
|
65
|
-
const err = new TimeoutError('timed out');
|
|
66
|
-
const serialized = serializeError(err);
|
|
67
|
-
expect(serialized.retryAfter).toBeUndefined();
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// deserializeError
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
describe('deserializeError', () => {
|
|
76
|
-
test('reconstructs a validation error', () => {
|
|
77
|
-
const data: SerializedError = {
|
|
78
|
-
category: 'validation',
|
|
79
|
-
message: 'bad input',
|
|
80
|
-
name: 'ValidationError',
|
|
81
|
-
retryable: false,
|
|
82
|
-
};
|
|
83
|
-
const err = deserializeError(data);
|
|
84
|
-
expect(err).toBeInstanceOf(ValidationError);
|
|
85
|
-
expect(err.message).toBe('bad input');
|
|
86
|
-
expect(err.category).toBe('validation');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('reconstructs a rate limit error with retryAfter', () => {
|
|
90
|
-
const data: SerializedError = {
|
|
91
|
-
category: 'rate_limit',
|
|
92
|
-
message: 'slow down',
|
|
93
|
-
name: 'RateLimitError',
|
|
94
|
-
retryAfter: 60,
|
|
95
|
-
retryable: true,
|
|
96
|
-
};
|
|
97
|
-
const err = deserializeError(data);
|
|
98
|
-
expect(err).toBeInstanceOf(RateLimitError);
|
|
99
|
-
expect((err as RateLimitError).retryAfter).toBe(60);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('restores stack trace', () => {
|
|
103
|
-
const data: SerializedError = {
|
|
104
|
-
category: 'internal',
|
|
105
|
-
message: 'oops',
|
|
106
|
-
name: 'InternalError',
|
|
107
|
-
stack: 'Error: oops\n at test.ts:1:1',
|
|
108
|
-
};
|
|
109
|
-
const err = deserializeError(data);
|
|
110
|
-
expect(err.stack).toBe('Error: oops\n at test.ts:1:1');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('restores context', () => {
|
|
114
|
-
const data: SerializedError = {
|
|
115
|
-
category: 'auth',
|
|
116
|
-
context: { endpoint: '/api/secret' },
|
|
117
|
-
message: 'unauthorized',
|
|
118
|
-
name: 'AuthError',
|
|
119
|
-
};
|
|
120
|
-
const err = deserializeError(data);
|
|
121
|
-
expect(err.context).toEqual({ endpoint: '/api/secret' });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('defaults to InternalError when category is missing', () => {
|
|
125
|
-
const data: SerializedError = {
|
|
126
|
-
message: 'unknown',
|
|
127
|
-
name: 'SomeError',
|
|
128
|
-
};
|
|
129
|
-
const err = deserializeError(data);
|
|
130
|
-
expect(err).toBeInstanceOf(InternalError);
|
|
131
|
-
expect(err.category).toBe('internal');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('round-trips through serialize/deserialize', () => {
|
|
135
|
-
const original = new NotFoundError('missing resource', {
|
|
136
|
-
context: { id: 'abc' },
|
|
137
|
-
});
|
|
138
|
-
const serialized = serializeError(original);
|
|
139
|
-
const restored = deserializeError(serialized);
|
|
140
|
-
expect(restored).toBeInstanceOf(NotFoundError);
|
|
141
|
-
expect(restored.message).toBe('missing resource');
|
|
142
|
-
expect(restored.context).toEqual({ id: 'abc' });
|
|
143
|
-
expect(restored.category).toBe('not_found');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('handles all error categories', () => {
|
|
147
|
-
const categories = [
|
|
148
|
-
'validation',
|
|
149
|
-
'not_found',
|
|
150
|
-
'conflict',
|
|
151
|
-
'permission',
|
|
152
|
-
'timeout',
|
|
153
|
-
'rate_limit',
|
|
154
|
-
'network',
|
|
155
|
-
'internal',
|
|
156
|
-
'auth',
|
|
157
|
-
'cancelled',
|
|
158
|
-
] as const;
|
|
159
|
-
|
|
160
|
-
for (const category of categories) {
|
|
161
|
-
const data: SerializedError = {
|
|
162
|
-
category,
|
|
163
|
-
message: 'test',
|
|
164
|
-
name: 'Test',
|
|
165
|
-
};
|
|
166
|
-
const err = deserializeError(data);
|
|
167
|
-
expect(err.category).toBe(category);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('round-trips all subclasses by name', () => {
|
|
172
|
-
const subclasses = [
|
|
173
|
-
{ Ctor: ValidationError, category: 'validation' },
|
|
174
|
-
{ Ctor: AmbiguousError, category: 'validation' },
|
|
175
|
-
{ Ctor: AssertionError, category: 'internal' },
|
|
176
|
-
{ Ctor: NotFoundError, category: 'not_found' },
|
|
177
|
-
{ Ctor: AlreadyExistsError, category: 'conflict' },
|
|
178
|
-
{ Ctor: ConflictError, category: 'conflict' },
|
|
179
|
-
{ Ctor: PermissionError, category: 'permission' },
|
|
180
|
-
{ Ctor: TimeoutError, category: 'timeout' },
|
|
181
|
-
{ Ctor: NetworkError, category: 'network' },
|
|
182
|
-
{ Ctor: InternalError, category: 'internal' },
|
|
183
|
-
{ Ctor: AuthError, category: 'auth' },
|
|
184
|
-
{ Ctor: CancelledError, category: 'cancelled' },
|
|
185
|
-
] as const;
|
|
186
|
-
|
|
187
|
-
test.each(subclasses)(
|
|
188
|
-
'$Ctor.name round-trips with correct identity',
|
|
189
|
-
({ Ctor, category }) => {
|
|
190
|
-
const original = new Ctor(`test ${Ctor.name}`, {
|
|
191
|
-
context: { key: 'value' },
|
|
192
|
-
});
|
|
193
|
-
const serialized = serializeError(original);
|
|
194
|
-
const restored = deserializeError(serialized);
|
|
195
|
-
|
|
196
|
-
expect(restored).toBeInstanceOf(Ctor);
|
|
197
|
-
expect(restored.constructor.name).toBe(Ctor.name);
|
|
198
|
-
expect(restored.name).toBe(Ctor.name);
|
|
199
|
-
expect(restored.category).toBe(category);
|
|
200
|
-
expect(restored.message).toBe(`test ${Ctor.name}`);
|
|
201
|
-
expect(restored.context).toEqual({ key: 'value' });
|
|
202
|
-
}
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
test('RateLimitError round-trips with retryAfter', () => {
|
|
206
|
-
const original = new RateLimitError('slow down', {
|
|
207
|
-
context: { endpoint: '/api' },
|
|
208
|
-
retryAfter: 42,
|
|
209
|
-
});
|
|
210
|
-
const serialized = serializeError(original);
|
|
211
|
-
const restored = deserializeError(serialized);
|
|
212
|
-
|
|
213
|
-
expect(restored).toBeInstanceOf(RateLimitError);
|
|
214
|
-
expect(restored.constructor.name).toBe('RateLimitError');
|
|
215
|
-
expect((restored as RateLimitError).retryAfter).toBe(42);
|
|
216
|
-
expect(restored.context).toEqual({ endpoint: '/api' });
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('falls back to category when name is unknown', () => {
|
|
220
|
-
const data: SerializedError = {
|
|
221
|
-
category: 'conflict',
|
|
222
|
-
message: 'custom error',
|
|
223
|
-
name: 'CustomConflictError',
|
|
224
|
-
};
|
|
225
|
-
const err = deserializeError(data);
|
|
226
|
-
expect(err).toBeInstanceOf(ConflictError);
|
|
227
|
-
expect(err.category).toBe('conflict');
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
// Result.fromJson
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
describe('Result.fromJson (safeParse)', () => {
|
|
237
|
-
test('parses valid JSON', () => {
|
|
238
|
-
const result = Result.fromJson('{"key": "value"}');
|
|
239
|
-
expect(result.isOk()).toBe(true);
|
|
240
|
-
expect(result.unwrap()).toEqual({ key: 'value' });
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('parses JSON arrays', () => {
|
|
244
|
-
const result = Result.fromJson('[1, 2, 3]');
|
|
245
|
-
expect(result.isOk()).toBe(true);
|
|
246
|
-
expect(result.unwrap()).toEqual([1, 2, 3]);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test('parses JSON primitives', () => {
|
|
250
|
-
expect(Result.fromJson('"hello"').unwrap()).toBe('hello');
|
|
251
|
-
expect(Result.fromJson('42').unwrap()).toBe(42);
|
|
252
|
-
expect(Result.fromJson('true').unwrap()).toBe(true);
|
|
253
|
-
expect(Result.fromJson('null').unwrap()).toBe(null);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test('returns ValidationError for invalid JSON', () => {
|
|
257
|
-
const result = Result.fromJson('not json');
|
|
258
|
-
expect(result.isErr()).toBe(true);
|
|
259
|
-
const err = result as unknown as { error: ValidationError };
|
|
260
|
-
expect(err.error).toBeInstanceOf(ValidationError);
|
|
261
|
-
expect(err.error.message).toBe('Invalid JSON');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('includes truncated input in context', () => {
|
|
265
|
-
const result = Result.fromJson('{bad}');
|
|
266
|
-
expect(result.isErr()).toBe(true);
|
|
267
|
-
const err = result as unknown as { error: ValidationError };
|
|
268
|
-
expect(err.error.context).toBeDefined();
|
|
269
|
-
expect(err.error.context?.['input']).toBe('{bad}');
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
// Result.toJson
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
|
|
277
|
-
describe('Result.toJson (safeStringify)', () => {
|
|
278
|
-
test('stringifies objects', () => {
|
|
279
|
-
const result = Result.toJson({ key: 'value' });
|
|
280
|
-
expect(result.isOk()).toBe(true);
|
|
281
|
-
expect(result.unwrap()).toBe('{"key":"value"}');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('stringifies arrays', () => {
|
|
285
|
-
const result = Result.toJson([1, 2, 3]);
|
|
286
|
-
expect(result.isOk()).toBe(true);
|
|
287
|
-
expect(result.unwrap()).toBe('[1,2,3]');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test('stringifies primitives', () => {
|
|
291
|
-
expect(Result.toJson('hello').unwrap()).toBe('"hello"');
|
|
292
|
-
expect(Result.toJson(42).unwrap()).toBe('42');
|
|
293
|
-
expect(Result.toJson(true).unwrap()).toBe('true');
|
|
294
|
-
expect(Result.toJson(null).unwrap()).toBe('null');
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test('handles circular references gracefully', () => {
|
|
298
|
-
const obj: Record<string, unknown> = { a: 1 };
|
|
299
|
-
obj['self'] = obj;
|
|
300
|
-
const result = Result.toJson(obj);
|
|
301
|
-
expect(result.isOk()).toBe(true);
|
|
302
|
-
const ok = result as unknown as { value: string };
|
|
303
|
-
const parsed = JSON.parse(ok.value) as Record<string, unknown>;
|
|
304
|
-
expect(parsed['a']).toBe(1);
|
|
305
|
-
expect(parsed['self']).toBe('[Circular]');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test('serializes shared references in a DAG without marking as circular', () => {
|
|
309
|
-
const shared = { x: 1 };
|
|
310
|
-
const obj = { a: shared, b: shared };
|
|
311
|
-
const result = Result.toJson(obj);
|
|
312
|
-
expect(result.isOk()).toBe(true);
|
|
313
|
-
const parsed = JSON.parse(result.unwrap()) as Record<string, unknown>;
|
|
314
|
-
expect(parsed['a']).toEqual({ x: 1 });
|
|
315
|
-
expect(parsed['b']).toEqual({ x: 1 });
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test('detects deep circular references', () => {
|
|
319
|
-
const inner: Record<string, unknown> = { value: 'deep' };
|
|
320
|
-
const obj: Record<string, unknown> = { child: { nested: inner } };
|
|
321
|
-
inner['back'] = obj;
|
|
322
|
-
const result = Result.toJson(obj);
|
|
323
|
-
expect(result.isOk()).toBe(true);
|
|
324
|
-
const parsed = JSON.parse(result.unwrap()) as Record<string, unknown>;
|
|
325
|
-
const child = parsed['child'] as Record<string, unknown>;
|
|
326
|
-
const nested = child['nested'] as Record<string, unknown>;
|
|
327
|
-
expect(nested['value']).toBe('deep');
|
|
328
|
-
expect(nested['back']).toBe('[Circular]');
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test('handles shared ref used in sibling subtrees of a DAG', () => {
|
|
332
|
-
const shared = { id: 42 };
|
|
333
|
-
const obj = {
|
|
334
|
-
left: { extra: 'l', ref: shared },
|
|
335
|
-
right: { extra: 'r', ref: shared },
|
|
336
|
-
};
|
|
337
|
-
const result = Result.toJson(obj);
|
|
338
|
-
expect(result.isOk()).toBe(true);
|
|
339
|
-
const parsed = JSON.parse(result.unwrap()) as Record<
|
|
340
|
-
string,
|
|
341
|
-
Record<string, unknown>
|
|
342
|
-
>;
|
|
343
|
-
expect(parsed['left']?.['ref']).toEqual({ id: 42 });
|
|
344
|
-
expect(parsed['right']?.['ref']).toEqual({ id: 42 });
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
// safeStringify (shared DAG / circular detection)
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
|
|
352
|
-
describe('safeStringify', () => {
|
|
353
|
-
test('serializes shared references in a DAG without marking as circular', () => {
|
|
354
|
-
const shared = { x: 1 };
|
|
355
|
-
const obj = { a: shared, b: shared };
|
|
356
|
-
const result = safeStringify(obj);
|
|
357
|
-
expect(result.isOk()).toBe(true);
|
|
358
|
-
const parsed = JSON.parse(result.unwrap()) as Record<string, unknown>;
|
|
359
|
-
expect(parsed['a']).toEqual({ x: 1 });
|
|
360
|
-
expect(parsed['b']).toEqual({ x: 1 });
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
test('detects true circular references', () => {
|
|
364
|
-
const obj: Record<string, unknown> = { a: 1 };
|
|
365
|
-
obj['self'] = obj;
|
|
366
|
-
const result = safeStringify(obj);
|
|
367
|
-
expect(result.isOk()).toBe(true);
|
|
368
|
-
const parsed = JSON.parse(result.unwrap()) as Record<string, unknown>;
|
|
369
|
-
expect(parsed['a']).toBe(1);
|
|
370
|
-
expect(parsed['self']).toBe('[Circular]');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test('detects deep circular references', () => {
|
|
374
|
-
const inner: Record<string, unknown> = { value: 'deep' };
|
|
375
|
-
const obj: Record<string, unknown> = { child: { nested: inner } };
|
|
376
|
-
inner['back'] = obj;
|
|
377
|
-
const result = safeStringify(obj);
|
|
378
|
-
expect(result.isOk()).toBe(true);
|
|
379
|
-
const parsed = JSON.parse(result.unwrap()) as Record<string, unknown>;
|
|
380
|
-
const child = parsed['child'] as Record<string, unknown>;
|
|
381
|
-
const nested = child['nested'] as Record<string, unknown>;
|
|
382
|
-
expect(nested['value']).toBe('deep');
|
|
383
|
-
expect(nested['back']).toBe('[Circular]');
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
test('handles shared ref used in sibling subtrees of a DAG', () => {
|
|
387
|
-
const shared = { id: 42 };
|
|
388
|
-
const obj = {
|
|
389
|
-
left: { extra: 'l', ref: shared },
|
|
390
|
-
right: { extra: 'r', ref: shared },
|
|
391
|
-
};
|
|
392
|
-
const result = safeStringify(obj);
|
|
393
|
-
expect(result.isOk()).toBe(true);
|
|
394
|
-
const parsed = JSON.parse(result.unwrap()) as Record<
|
|
395
|
-
string,
|
|
396
|
-
Record<string, unknown>
|
|
397
|
-
>;
|
|
398
|
-
expect(parsed['left']?.['ref']).toEqual({ id: 42 });
|
|
399
|
-
expect(parsed['right']?.['ref']).toEqual({ id: 42 });
|
|
400
|
-
});
|
|
401
|
-
});
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { ValidationError } from '../errors.js';
|
|
6
|
-
import { Result } from '../result.js';
|
|
7
|
-
import { topo } from '../topo.js';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Mock factories
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
// oxlint-disable-next-line require-await -- satisfies async interface without needing await
|
|
14
|
-
const noop = async () => Result.ok();
|
|
15
|
-
|
|
16
|
-
const mockTrail = (id: string) => ({
|
|
17
|
-
id,
|
|
18
|
-
implementation: noop,
|
|
19
|
-
input: z.object({ x: z.number() }),
|
|
20
|
-
kind: 'trail' as const,
|
|
21
|
-
output: z.object({ y: z.number() }),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const mockHike = (id: string) => ({
|
|
25
|
-
follows: [] as readonly string[],
|
|
26
|
-
id,
|
|
27
|
-
implementation: noop,
|
|
28
|
-
input: z.object({ q: z.string() }),
|
|
29
|
-
kind: 'hike' as const,
|
|
30
|
-
output: z.object({ r: z.string() }),
|
|
31
|
-
path: `/${id}`,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const mockEvent = (id: string) => ({
|
|
35
|
-
id,
|
|
36
|
-
kind: 'event' as const,
|
|
37
|
-
payload: z.object({ payload: z.string() }),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// topo()
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
describe('topo', () => {
|
|
45
|
-
describe('collection', () => {
|
|
46
|
-
test('returns Topo with name', () => {
|
|
47
|
-
const t = topo('my-app');
|
|
48
|
-
expect(t.name).toBe('my-app');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('collects trails from modules', () => {
|
|
52
|
-
const mod = { myTrail: mockTrail('create-user') };
|
|
53
|
-
const t = topo('app', mod);
|
|
54
|
-
|
|
55
|
-
expect(t.trails.size).toBe(1);
|
|
56
|
-
expect(t.trails.get('create-user')).toBe(mod.myTrail);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('auto-scans exports by kind discriminant', () => {
|
|
60
|
-
const mod = {
|
|
61
|
-
event1: mockEvent('e1'),
|
|
62
|
-
hike1: mockHike('r1'),
|
|
63
|
-
trail1: mockTrail('t1'),
|
|
64
|
-
};
|
|
65
|
-
const t = topo('app', mod);
|
|
66
|
-
|
|
67
|
-
expect(t.trails.size).toBe(1);
|
|
68
|
-
expect(t.hikes.size).toBe(1);
|
|
69
|
-
expect(t.events.size).toBe(1);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('collects from multiple modules', () => {
|
|
73
|
-
const mod1 = { a: mockTrail('t1') };
|
|
74
|
-
const mod2 = { b: mockTrail('t2'), c: mockHike('r1') };
|
|
75
|
-
const t = topo('app', mod1, mod2);
|
|
76
|
-
|
|
77
|
-
expect(t.trails.size).toBe(2);
|
|
78
|
-
expect(t.hikes.size).toBe(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('non-trail exports are silently ignored', () => {
|
|
82
|
-
const mod = {
|
|
83
|
-
config: { port: 3000 },
|
|
84
|
-
helper: () => 'not a trail',
|
|
85
|
-
name: 'some-string',
|
|
86
|
-
nothing: null,
|
|
87
|
-
num: 42,
|
|
88
|
-
trail1: mockTrail('t1'),
|
|
89
|
-
undef: undefined,
|
|
90
|
-
};
|
|
91
|
-
const t = topo('app', mod);
|
|
92
|
-
|
|
93
|
-
expect(t.trails.size).toBe(1);
|
|
94
|
-
expect(t.hikes.size).toBe(0);
|
|
95
|
-
expect(t.events.size).toBe(0);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('allows different IDs across trails and hikes', () => {
|
|
99
|
-
const mod = { h: mockHike('hike-1'), t: mockTrail('trail-1') };
|
|
100
|
-
const t = topo('app', mod);
|
|
101
|
-
|
|
102
|
-
expect(t.trails.size).toBe(1);
|
|
103
|
-
expect(t.hikes.size).toBe(1);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('duplicate rejection', () => {
|
|
108
|
-
test('rejects duplicate trail IDs', () => {
|
|
109
|
-
const mod1 = { a: mockTrail('dup') };
|
|
110
|
-
const mod2 = { b: mockTrail('dup') };
|
|
111
|
-
|
|
112
|
-
expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
|
|
113
|
-
expect(() => topo('app', mod1, mod2)).toThrow(
|
|
114
|
-
'Duplicate trail ID: "dup"'
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('rejects duplicate hike IDs', () => {
|
|
119
|
-
const mod1 = { a: mockHike('dup') };
|
|
120
|
-
const mod2 = { b: mockHike('dup') };
|
|
121
|
-
|
|
122
|
-
expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
|
|
123
|
-
expect(() => topo('app', mod1, mod2)).toThrow('Duplicate hike ID: "dup"');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('rejects duplicate event IDs', () => {
|
|
127
|
-
const mod1 = { a: mockEvent('dup') };
|
|
128
|
-
const mod2 = { b: mockEvent('dup') };
|
|
129
|
-
|
|
130
|
-
expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
|
|
131
|
-
expect(() => topo('app', mod1, mod2)).toThrow(
|
|
132
|
-
'Duplicate event ID: "dup"'
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('rejects a hike whose ID collides with an existing trail', () => {
|
|
137
|
-
const modTrail = { a: mockTrail('shared-id') };
|
|
138
|
-
const modHike = { b: mockHike('shared-id') };
|
|
139
|
-
|
|
140
|
-
expect(() => topo('app', modTrail, modHike)).toThrow(ValidationError);
|
|
141
|
-
expect(() => topo('app', modTrail, modHike)).toThrow(
|
|
142
|
-
/ID collision.*hike "shared-id".*trail/
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('rejects a trail whose ID collides with an existing hike', () => {
|
|
147
|
-
const modHike = { a: mockHike('shared-id') };
|
|
148
|
-
const modTrail = { b: mockTrail('shared-id') };
|
|
149
|
-
|
|
150
|
-
expect(() => topo('app', modHike, modTrail)).toThrow(ValidationError);
|
|
151
|
-
expect(() => topo('app', modHike, modTrail)).toThrow(
|
|
152
|
-
/ID collision.*trail "shared-id".*hike/
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// Topo
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
|
|
162
|
-
describe('Topo', () => {
|
|
163
|
-
const mod = {
|
|
164
|
-
e1: mockEvent('event-1'),
|
|
165
|
-
h1: mockHike('hike-1'),
|
|
166
|
-
t1: mockTrail('trail-1'),
|
|
167
|
-
t2: mockTrail('trail-2'),
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// Build once for the describe block
|
|
171
|
-
const app = topo('app', mod);
|
|
172
|
-
|
|
173
|
-
describe('get()', () => {
|
|
174
|
-
test('retrieves trail by ID', () => {
|
|
175
|
-
expect(app.get('trail-1')).toBe(mod.t1);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('retrieves hike by ID', () => {
|
|
179
|
-
expect(app.get('hike-1')).toBe(mod.h1);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('returns undefined for unknown ID', () => {
|
|
183
|
-
expect(app.get('nope')).toBeUndefined();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('has()', () => {
|
|
188
|
-
test('returns true for known trail', () => {
|
|
189
|
-
expect(app.has('trail-1')).toBe(true);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('returns true for known hike', () => {
|
|
193
|
-
expect(app.has('hike-1')).toBe(true);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('returns false for unknown ID', () => {
|
|
197
|
-
expect(app.has('nope')).toBe(false);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('returns false for event ID (events are not trails/hikes)', () => {
|
|
201
|
-
expect(app.has('event-1')).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('listing', () => {
|
|
206
|
-
test('list() returns all trails and hikes', () => {
|
|
207
|
-
const items = app.list();
|
|
208
|
-
expect(items).toHaveLength(3);
|
|
209
|
-
expect(items).toContain(mod.t1);
|
|
210
|
-
expect(items).toContain(mod.t2);
|
|
211
|
-
expect(items).toContain(mod.h1);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('listEvents() returns all events', () => {
|
|
215
|
-
const items = app.listEvents();
|
|
216
|
-
expect(items).toHaveLength(1);
|
|
217
|
-
expect(items).toContain(mod.e1);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
});
|