@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.
Files changed (314) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +73 -24
  3. package/package.json +11 -4
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/context.ts +28 -4
  11. package/src/contour.ts +344 -0
  12. package/src/cross-batch.ts +69 -0
  13. package/src/cross-schema.ts +36 -0
  14. package/src/detours.ts +8 -0
  15. package/src/draft.ts +60 -52
  16. package/src/error-projection.ts +51 -0
  17. package/src/errors.ts +245 -48
  18. package/src/execute.ts +1250 -40
  19. package/src/fire.ts +1169 -0
  20. package/src/index.ts +383 -49
  21. package/src/internal/fork-ctx.ts +69 -0
  22. package/src/layer-projection.ts +192 -0
  23. package/src/layer.ts +81 -0
  24. package/src/observe.ts +361 -0
  25. package/src/path-security.ts +1 -1
  26. package/src/permits.ts +12 -0
  27. package/src/redaction/patterns.ts +6 -3
  28. package/src/resilience.ts +11 -11
  29. package/src/resource-config.ts +792 -0
  30. package/src/resource.ts +175 -0
  31. package/src/result.ts +18 -0
  32. package/src/run.ts +7 -6
  33. package/src/schedule-runtime.ts +689 -0
  34. package/src/schedule.ts +324 -0
  35. package/src/serialization.ts +90 -55
  36. package/src/signal-diagnostics.ts +633 -0
  37. package/src/signal-ref.ts +87 -0
  38. package/src/signal.ts +37 -0
  39. package/src/store/accessor-protocol.ts +56 -0
  40. package/src/store/index.ts +4 -0
  41. package/src/structured-examples.ts +245 -0
  42. package/src/surface-derivation.ts +91 -0
  43. package/src/surface-filter.ts +176 -0
  44. package/src/topo.ts +710 -59
  45. package/src/tracing.ts +345 -0
  46. package/src/trail.ts +347 -33
  47. package/src/trails/derive-trail.ts +842 -0
  48. package/src/trails/index.ts +9 -0
  49. package/src/trails/ingest.ts +138 -0
  50. package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
  51. package/src/transport-error-map.ts +174 -0
  52. package/src/type-utils.ts +17 -2
  53. package/src/types.ts +208 -17
  54. package/src/validate-established-topo.ts +8 -2
  55. package/src/validate-topo.ts +244 -13
  56. package/src/validation.ts +58 -1
  57. package/src/webhook.ts +276 -0
  58. package/src/workspace.ts +1 -1
  59. package/src/zod-wrappers.ts +72 -0
  60. package/.turbo/turbo-build.log +0 -1
  61. package/.turbo/turbo-lint.log +0 -3
  62. package/.turbo/turbo-typecheck.log +0 -1
  63. package/dist/adapters.d.ts +0 -39
  64. package/dist/adapters.d.ts.map +0 -1
  65. package/dist/adapters.js +0 -2
  66. package/dist/adapters.js.map +0 -1
  67. package/dist/blob-ref.d.ts +0 -20
  68. package/dist/blob-ref.d.ts.map +0 -1
  69. package/dist/blob-ref.js +0 -22
  70. package/dist/blob-ref.js.map +0 -1
  71. package/dist/branded.d.ts +0 -36
  72. package/dist/branded.d.ts.map +0 -1
  73. package/dist/branded.js +0 -89
  74. package/dist/branded.js.map +0 -1
  75. package/dist/collections.d.ts +0 -31
  76. package/dist/collections.d.ts.map +0 -1
  77. package/dist/collections.js +0 -60
  78. package/dist/collections.js.map +0 -1
  79. package/dist/context.d.ts +0 -10
  80. package/dist/context.d.ts.map +0 -1
  81. package/dist/context.js +0 -21
  82. package/dist/context.js.map +0 -1
  83. package/dist/derive.d.ts +0 -44
  84. package/dist/derive.d.ts.map +0 -1
  85. package/dist/derive.js +0 -149
  86. package/dist/derive.js.map +0 -1
  87. package/dist/dispatch.d.ts +0 -27
  88. package/dist/dispatch.d.ts.map +0 -1
  89. package/dist/dispatch.js +0 -34
  90. package/dist/dispatch.js.map +0 -1
  91. package/dist/draft.d.ts +0 -28
  92. package/dist/draft.d.ts.map +0 -1
  93. package/dist/draft.js +0 -156
  94. package/dist/draft.js.map +0 -1
  95. package/dist/errors.d.ts +0 -83
  96. package/dist/errors.d.ts.map +0 -1
  97. package/dist/errors.js +0 -142
  98. package/dist/errors.js.map +0 -1
  99. package/dist/event.d.ts +0 -8
  100. package/dist/event.d.ts.map +0 -1
  101. package/dist/event.js +0 -7
  102. package/dist/event.js.map +0 -1
  103. package/dist/execute.d.ts +0 -35
  104. package/dist/execute.d.ts.map +0 -1
  105. package/dist/execute.js +0 -90
  106. package/dist/execute.js.map +0 -1
  107. package/dist/fetch.d.ts +0 -15
  108. package/dist/fetch.d.ts.map +0 -1
  109. package/dist/fetch.js +0 -102
  110. package/dist/fetch.js.map +0 -1
  111. package/dist/gate.d.ts +0 -17
  112. package/dist/gate.d.ts.map +0 -1
  113. package/dist/gate.js +0 -21
  114. package/dist/gate.js.map +0 -1
  115. package/dist/guards.d.ts +0 -17
  116. package/dist/guards.d.ts.map +0 -1
  117. package/dist/guards.js +0 -25
  118. package/dist/guards.js.map +0 -1
  119. package/dist/health.d.ts +0 -18
  120. package/dist/health.d.ts.map +0 -1
  121. package/dist/health.js +0 -5
  122. package/dist/health.js.map +0 -1
  123. package/dist/index.d.ts +0 -48
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js +0 -51
  126. package/dist/index.js.map +0 -1
  127. package/dist/internal/topo-saves.d.ts +0 -47
  128. package/dist/internal/topo-saves.d.ts.map +0 -1
  129. package/dist/internal/topo-saves.js +0 -310
  130. package/dist/internal/topo-saves.js.map +0 -1
  131. package/dist/internal/topo-store-read.d.ts +0 -67
  132. package/dist/internal/topo-store-read.d.ts.map +0 -1
  133. package/dist/internal/topo-store-read.js +0 -222
  134. package/dist/internal/topo-store-read.js.map +0 -1
  135. package/dist/internal/topo-store.d.ts +0 -12
  136. package/dist/internal/topo-store.d.ts.map +0 -1
  137. package/dist/internal/topo-store.js +0 -571
  138. package/dist/internal/topo-store.js.map +0 -1
  139. package/dist/internal/trails-db.d.ts +0 -16
  140. package/dist/internal/trails-db.d.ts.map +0 -1
  141. package/dist/internal/trails-db.js +0 -118
  142. package/dist/internal/trails-db.js.map +0 -1
  143. package/dist/job.d.ts +0 -24
  144. package/dist/job.d.ts.map +0 -1
  145. package/dist/job.js +0 -17
  146. package/dist/job.js.map +0 -1
  147. package/dist/layer.d.ts +0 -6
  148. package/dist/layer.d.ts.map +0 -1
  149. package/dist/layer.js +0 -4
  150. package/dist/layer.js.map +0 -1
  151. package/dist/path-security.d.ts +0 -28
  152. package/dist/path-security.d.ts.map +0 -1
  153. package/dist/path-security.js +0 -63
  154. package/dist/path-security.js.map +0 -1
  155. package/dist/patterns/bulk.d.ts +0 -15
  156. package/dist/patterns/bulk.d.ts.map +0 -1
  157. package/dist/patterns/bulk.js +0 -14
  158. package/dist/patterns/bulk.js.map +0 -1
  159. package/dist/patterns/change.d.ts +0 -10
  160. package/dist/patterns/change.d.ts.map +0 -1
  161. package/dist/patterns/change.js +0 -10
  162. package/dist/patterns/change.js.map +0 -1
  163. package/dist/patterns/date-range.d.ts +0 -10
  164. package/dist/patterns/date-range.d.ts.map +0 -1
  165. package/dist/patterns/date-range.js +0 -10
  166. package/dist/patterns/date-range.js.map +0 -1
  167. package/dist/patterns/index.d.ts +0 -9
  168. package/dist/patterns/index.d.ts.map +0 -1
  169. package/dist/patterns/index.js +0 -9
  170. package/dist/patterns/index.js.map +0 -1
  171. package/dist/patterns/pagination.d.ts +0 -18
  172. package/dist/patterns/pagination.d.ts.map +0 -1
  173. package/dist/patterns/pagination.js +0 -18
  174. package/dist/patterns/pagination.js.map +0 -1
  175. package/dist/patterns/progress.d.ts +0 -11
  176. package/dist/patterns/progress.d.ts.map +0 -1
  177. package/dist/patterns/progress.js +0 -11
  178. package/dist/patterns/progress.js.map +0 -1
  179. package/dist/patterns/sorting.d.ts +0 -13
  180. package/dist/patterns/sorting.d.ts.map +0 -1
  181. package/dist/patterns/sorting.js +0 -10
  182. package/dist/patterns/sorting.js.map +0 -1
  183. package/dist/patterns/status.d.ts +0 -15
  184. package/dist/patterns/status.d.ts.map +0 -1
  185. package/dist/patterns/status.js +0 -9
  186. package/dist/patterns/status.js.map +0 -1
  187. package/dist/patterns/timestamps.d.ts +0 -10
  188. package/dist/patterns/timestamps.d.ts.map +0 -1
  189. package/dist/patterns/timestamps.js +0 -10
  190. package/dist/patterns/timestamps.js.map +0 -1
  191. package/dist/provision-config.d.ts +0 -22
  192. package/dist/provision-config.d.ts.map +0 -1
  193. package/dist/provision-config.js +0 -210
  194. package/dist/provision-config.js.map +0 -1
  195. package/dist/provision.d.ts +0 -71
  196. package/dist/provision.d.ts.map +0 -1
  197. package/dist/provision.js +0 -56
  198. package/dist/provision.js.map +0 -1
  199. package/dist/redaction/index.d.ts +0 -4
  200. package/dist/redaction/index.d.ts.map +0 -1
  201. package/dist/redaction/index.js +0 -3
  202. package/dist/redaction/index.js.map +0 -1
  203. package/dist/redaction/patterns.d.ts +0 -9
  204. package/dist/redaction/patterns.d.ts.map +0 -1
  205. package/dist/redaction/patterns.js +0 -39
  206. package/dist/redaction/patterns.js.map +0 -1
  207. package/dist/redaction/redactor.d.ts +0 -27
  208. package/dist/redaction/redactor.d.ts.map +0 -1
  209. package/dist/redaction/redactor.js +0 -89
  210. package/dist/redaction/redactor.js.map +0 -1
  211. package/dist/resilience.d.ts +0 -34
  212. package/dist/resilience.d.ts.map +0 -1
  213. package/dist/resilience.js +0 -164
  214. package/dist/resilience.js.map +0 -1
  215. package/dist/result.d.ts +0 -57
  216. package/dist/result.d.ts.map +0 -1
  217. package/dist/result.js +0 -156
  218. package/dist/result.js.map +0 -1
  219. package/dist/run.d.ts +0 -27
  220. package/dist/run.d.ts.map +0 -1
  221. package/dist/run.js +0 -34
  222. package/dist/run.js.map +0 -1
  223. package/dist/serialization.d.ts +0 -27
  224. package/dist/serialization.d.ts.map +0 -1
  225. package/dist/serialization.js +0 -153
  226. package/dist/serialization.js.map +0 -1
  227. package/dist/service-config.d.ts +0 -22
  228. package/dist/service-config.d.ts.map +0 -1
  229. package/dist/service-config.js +0 -210
  230. package/dist/service-config.js.map +0 -1
  231. package/dist/service.d.ts +0 -71
  232. package/dist/service.d.ts.map +0 -1
  233. package/dist/service.js +0 -56
  234. package/dist/service.js.map +0 -1
  235. package/dist/signal.d.ts +0 -33
  236. package/dist/signal.d.ts.map +0 -1
  237. package/dist/signal.js +0 -17
  238. package/dist/signal.js.map +0 -1
  239. package/dist/topo-store.d.ts +0 -48
  240. package/dist/topo-store.d.ts.map +0 -1
  241. package/dist/topo-store.js +0 -175
  242. package/dist/topo-store.js.map +0 -1
  243. package/dist/topo.d.ts +0 -25
  244. package/dist/topo.d.ts.map +0 -1
  245. package/dist/topo.js +0 -112
  246. package/dist/topo.js.map +0 -1
  247. package/dist/trail.d.ts +0 -96
  248. package/dist/trail.d.ts.map +0 -1
  249. package/dist/trail.js +0 -20
  250. package/dist/trail.js.map +0 -1
  251. package/dist/type-utils.d.ts +0 -24
  252. package/dist/type-utils.d.ts.map +0 -1
  253. package/dist/type-utils.js +0 -12
  254. package/dist/type-utils.js.map +0 -1
  255. package/dist/types.d.ts +0 -71
  256. package/dist/types.d.ts.map +0 -1
  257. package/dist/types.js +0 -3
  258. package/dist/types.js.map +0 -1
  259. package/dist/validate-established-topo.d.ts +0 -76
  260. package/dist/validate-established-topo.d.ts.map +0 -1
  261. package/dist/validate-established-topo.js +0 -43
  262. package/dist/validate-established-topo.js.map +0 -1
  263. package/dist/validate-topo.d.ts +0 -24
  264. package/dist/validate-topo.d.ts.map +0 -1
  265. package/dist/validate-topo.js +0 -176
  266. package/dist/validate-topo.js.map +0 -1
  267. package/dist/validation.d.ts +0 -27
  268. package/dist/validation.d.ts.map +0 -1
  269. package/dist/validation.js +0 -165
  270. package/dist/validation.js.map +0 -1
  271. package/dist/workspace.d.ts +0 -25
  272. package/dist/workspace.d.ts.map +0 -1
  273. package/dist/workspace.js +0 -57
  274. package/dist/workspace.js.map +0 -1
  275. package/src/__tests__/blob-ref.test.ts +0 -103
  276. package/src/__tests__/branded.test.ts +0 -148
  277. package/src/__tests__/collections.test.ts +0 -126
  278. package/src/__tests__/context.test.ts +0 -77
  279. package/src/__tests__/derive.test.ts +0 -260
  280. package/src/__tests__/errors.test.ts +0 -309
  281. package/src/__tests__/execute.test.ts +0 -525
  282. package/src/__tests__/fetch.test.ts +0 -217
  283. package/src/__tests__/gate.test.ts +0 -125
  284. package/src/__tests__/guards.test.ts +0 -102
  285. package/src/__tests__/path-security.test.ts +0 -114
  286. package/src/__tests__/patterns.test.ts +0 -273
  287. package/src/__tests__/redaction.test.ts +0 -244
  288. package/src/__tests__/resilience.test.ts +0 -246
  289. package/src/__tests__/result.test.ts +0 -155
  290. package/src/__tests__/run.test.ts +0 -181
  291. package/src/__tests__/serialization.test.ts +0 -401
  292. package/src/__tests__/service-config.test.ts +0 -228
  293. package/src/__tests__/service.test.ts +0 -197
  294. package/src/__tests__/signal.test.ts +0 -82
  295. package/src/__tests__/topo-store-read.test.ts +0 -251
  296. package/src/__tests__/topo-store.test.ts +0 -469
  297. package/src/__tests__/topo.test.ts +0 -277
  298. package/src/__tests__/trail-permit.test.ts +0 -60
  299. package/src/__tests__/trail.test.ts +0 -261
  300. package/src/__tests__/trails-db.test.ts +0 -191
  301. package/src/__tests__/type-utils.test.ts +0 -90
  302. package/src/__tests__/validate-topo.test.ts +0 -489
  303. package/src/__tests__/validation.test.ts +0 -336
  304. package/src/__tests__/workspace.test.ts +0 -183
  305. package/src/event.ts +0 -15
  306. package/src/gate.ts +0 -44
  307. package/src/internal/topo-saves.ts +0 -429
  308. package/src/internal/topo-store-read.ts +0 -473
  309. package/src/internal/topo-store.ts +0 -1087
  310. package/src/provision-config.ts +0 -373
  311. package/src/provision.ts +0 -148
  312. package/src/topo-store.ts +0 -301
  313. package/tsconfig.json +0 -9
  314. 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,228 +0,0 @@
1
- /* oxlint-disable require-await -- trail implementations satisfy async interface without awaiting */
2
- import { describe, expect, test } from 'bun:test';
3
-
4
- import { z } from 'zod';
5
-
6
- import { executeTrail } from '../execute.js';
7
- import { Result } from '../result.js';
8
- import { provision } from '../provision.js';
9
- import { trail } from '../trail.js';
10
- import type { ProvisionContext } from '../provision.js';
11
-
12
- // ---------------------------------------------------------------------------
13
- // Helpers
14
- // ---------------------------------------------------------------------------
15
-
16
- const nextId = (name: string): string =>
17
- `test.svc-config.${name}.${Bun.randomUUIDv7()}`;
18
-
19
- const createSingletonConfigTrail = (id: string) => {
20
- const captures = { createCalls: 0 };
21
- const svc = provision(id, {
22
- config: z.object({ key: z.string() }),
23
- create: (ctx: ProvisionContext<{ key: string }>) => {
24
- captures.createCalls += 1;
25
- return Result.ok({
26
- createCall: captures.createCalls,
27
- key: ctx.config.key,
28
- });
29
- },
30
- });
31
-
32
- return {
33
- captures,
34
- trail: trail('svc-config.singleton', {
35
- blaze: (_input, ctx) =>
36
- Result.ok({
37
- createCall: svc.from(ctx).createCall,
38
- key: svc.from(ctx).key,
39
- }),
40
- input: z.object({}),
41
- output: z.object({ createCall: z.number(), key: z.string() }),
42
- provisions: [svc],
43
- }),
44
- };
45
- };
46
-
47
- // ---------------------------------------------------------------------------
48
- // Tests
49
- // ---------------------------------------------------------------------------
50
-
51
- describe('ProvisionContext.config', () => {
52
- test('provision with config schema receives validated config in svc.config', async () => {
53
- const id = nextId('typed-config');
54
- let capturedConfig: unknown;
55
-
56
- const db = provision(id, {
57
- config: z.object({ poolSize: z.number(), url: z.string().url() }),
58
- create: (svc: ProvisionContext<{ url: string; poolSize: number }>) => {
59
- capturedConfig = svc.config;
60
- return Result.ok({ connected: true });
61
- },
62
- });
63
-
64
- const dbTrail = trail('svc-config.typed', {
65
- blaze: (_input, ctx) => Result.ok({ connected: db.from(ctx).connected }),
66
- input: z.object({}),
67
- output: z.object({ connected: z.boolean() }),
68
- provisions: [db],
69
- });
70
-
71
- const result = await executeTrail(
72
- dbTrail,
73
- {},
74
- {
75
- configValues: {
76
- [id]: { poolSize: 5, url: 'https://example.com' },
77
- },
78
- }
79
- );
80
-
81
- expect(result.isOk()).toBe(true);
82
- expect(capturedConfig).toEqual({ poolSize: 5, url: 'https://example.com' });
83
- });
84
-
85
- test('provision without config still works — svc.config is undefined', async () => {
86
- const id = nextId('no-config');
87
- let capturedConfig: unknown = 'sentinel';
88
-
89
- const counter = provision(id, {
90
- create: (svc) => {
91
- capturedConfig = svc.config;
92
- return Result.ok(42);
93
- },
94
- });
95
-
96
- const counterTrail = trail('svc-config.no-config', {
97
- blaze: (_input, ctx) => Result.ok({ value: counter.from(ctx) }),
98
- input: z.object({}),
99
- provisions: [counter],
100
- });
101
-
102
- const result = await executeTrail(counterTrail, {});
103
-
104
- expect(result.isOk()).toBe(true);
105
- expect(capturedConfig).toBeUndefined();
106
- });
107
-
108
- test('config validation failure returns Result.err at provision creation time', async () => {
109
- const id = nextId('invalid-config');
110
-
111
- const db = provision(id, {
112
- config: z.object({ url: z.string().url() }),
113
- create: () => Result.ok({ connected: true }),
114
- });
115
-
116
- const dbTrail = trail('svc-config.invalid', {
117
- blaze: () => Result.ok(null),
118
- input: z.object({}),
119
- provisions: [db],
120
- });
121
-
122
- const result = await executeTrail(
123
- dbTrail,
124
- {},
125
- {
126
- configValues: {
127
- [id]: { url: 'not-a-url' },
128
- },
129
- }
130
- );
131
-
132
- expect(result.isErr()).toBe(true);
133
- expect(result.error.message).toContain(id);
134
- });
135
-
136
- test('missing configValues for a provision with config schema returns Result.err', async () => {
137
- const id = nextId('missing-config');
138
-
139
- const db = provision(id, {
140
- config: z.object({ url: z.string().url() }),
141
- create: () => Result.ok({ connected: true }),
142
- });
143
-
144
- const dbTrail = trail('svc-config.missing', {
145
- blaze: () => Result.ok(null),
146
- input: z.object({}),
147
- provisions: [db],
148
- });
149
-
150
- const result = await executeTrail(dbTrail, {});
151
-
152
- expect(result.isErr()).toBe(true);
153
- expect(result.error.message).toContain(id);
154
- });
155
-
156
- test('provision override bypasses config validation', async () => {
157
- const id = nextId('override-bypass');
158
-
159
- const db = provision(id, {
160
- config: z.object({ url: z.string().url() }),
161
- create: () => Result.ok({ connected: true }),
162
- });
163
-
164
- const dbTrail = trail('svc-config.override', {
165
- blaze: (_input, ctx) => Result.ok({ value: db.from(ctx) as number }),
166
- input: z.object({}),
167
- output: z.object({ value: z.number() }),
168
- provisions: [db],
169
- });
170
-
171
- // Provide the provision via overrides without any configValues — should NOT fail
172
- const result = await executeTrail(
173
- dbTrail,
174
- {},
175
- { provisions: { [id]: 42 } }
176
- );
177
-
178
- expect(result.isOk()).toBe(true);
179
- expect(result.unwrap()).toEqual({ value: 42 });
180
- });
181
- test('config values passed through ExecuteTrailOptions.configValues', async () => {
182
- const id = nextId('options-config');
183
- const captures: unknown[] = [];
184
-
185
- const svc = provision(id, {
186
- config: z.object({ key: z.string() }),
187
- create: (ctx: ProvisionContext<{ key: string }>) => {
188
- captures.push(ctx.config);
189
- return Result.ok({ key: ctx.config.key });
190
- },
191
- });
192
-
193
- const svcTrail = trail('svc-config.options', {
194
- blaze: (_input, ctx) => Result.ok({ key: svc.from(ctx).key }),
195
- input: z.object({}),
196
- output: z.object({ key: z.string() }),
197
- provisions: [svc],
198
- });
199
-
200
- const result = await executeTrail(
201
- svcTrail,
202
- {},
203
- {
204
- configValues: { [id]: { key: 'hello' } },
205
- }
206
- );
207
-
208
- expect(result.isOk()).toBe(true);
209
- expect(result.unwrap()).toEqual({ key: 'hello' });
210
- expect(captures).toEqual([{ key: 'hello' }]);
211
- });
212
-
213
- test('config-aware singleton provisions reuse the cached instance', async () => {
214
- const id = nextId('singleton-config');
215
- const { captures, trail: singletonTrail } = createSingletonConfigTrail(id);
216
- const options = {
217
- configValues: { [id]: { key: 'hello' } },
218
- };
219
- const first = await executeTrail(singletonTrail, {}, options);
220
- const second = await executeTrail(singletonTrail, {}, options);
221
-
222
- expect(first.isOk()).toBe(true);
223
- expect(second.isOk()).toBe(true);
224
- expect(first.unwrap()).toEqual({ createCall: 1, key: 'hello' });
225
- expect(second.unwrap()).toEqual({ createCall: 1, key: 'hello' });
226
- expect(captures.createCalls).toBe(1);
227
- });
228
- });