@ontrails/core 1.0.0-beta.15 → 1.0.0-beta.17

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 (348) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +64 -19
  3. package/package.json +9 -8
  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/context.ts +4 -0
  10. package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
  11. package/src/detours.ts +8 -0
  12. package/src/draft.ts +4 -0
  13. package/src/error-projection.ts +51 -0
  14. package/src/errors.ts +192 -39
  15. package/src/execute.ts +443 -44
  16. package/src/fire.ts +965 -91
  17. package/src/index.ts +285 -38
  18. package/src/layer-projection.ts +192 -0
  19. package/src/layer.ts +32 -1
  20. package/src/observe.ts +361 -0
  21. package/src/permits.ts +12 -0
  22. package/src/redaction/patterns.ts +6 -3
  23. package/src/resilience.ts +8 -8
  24. package/src/resource-config.ts +477 -44
  25. package/src/resource.ts +21 -1
  26. package/src/result.ts +18 -0
  27. package/src/run.ts +2 -2
  28. package/src/schedule-runtime.ts +689 -0
  29. package/src/schedule.ts +324 -0
  30. package/src/serialization.ts +90 -57
  31. package/src/signal-diagnostics.ts +633 -0
  32. package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
  33. package/src/signal.ts +37 -0
  34. package/src/store/accessor-protocol.ts +5 -5
  35. package/src/structured-examples.ts +245 -0
  36. package/src/surface-derivation.ts +91 -0
  37. package/src/surface-filter.ts +7 -13
  38. package/src/topo.ts +386 -6
  39. package/src/tracing.ts +345 -0
  40. package/src/trail.ts +168 -22
  41. package/src/trails/derive-trail.ts +1 -1
  42. package/src/trails/ingest.ts +7 -15
  43. package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
  44. package/src/transport-error-map.ts +146 -28
  45. package/src/types.ts +82 -28
  46. package/src/validate-established-topo.ts +6 -0
  47. package/src/validate-topo.ts +212 -5
  48. package/src/validation.ts +58 -1
  49. package/src/webhook.ts +276 -0
  50. package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
  51. package/.turbo/turbo-build.log +0 -1
  52. package/.turbo/turbo-lint.log +0 -3
  53. package/.turbo/turbo-typecheck.log +0 -1
  54. package/dist/blob-ref.d.ts +0 -20
  55. package/dist/blob-ref.d.ts.map +0 -1
  56. package/dist/blob-ref.js +0 -22
  57. package/dist/blob-ref.js.map +0 -1
  58. package/dist/branded.d.ts +0 -36
  59. package/dist/branded.d.ts.map +0 -1
  60. package/dist/branded.js +0 -89
  61. package/dist/branded.js.map +0 -1
  62. package/dist/collections.d.ts +0 -31
  63. package/dist/collections.d.ts.map +0 -1
  64. package/dist/collections.js +0 -60
  65. package/dist/collections.js.map +0 -1
  66. package/dist/context.d.ts +0 -22
  67. package/dist/context.d.ts.map +0 -1
  68. package/dist/context.js +0 -37
  69. package/dist/context.js.map +0 -1
  70. package/dist/contour.d.ts +0 -81
  71. package/dist/contour.d.ts.map +0 -1
  72. package/dist/contour.js +0 -176
  73. package/dist/contour.js.map +0 -1
  74. package/dist/cross-schema.d.ts +0 -18
  75. package/dist/cross-schema.d.ts.map +0 -1
  76. package/dist/cross-schema.js +0 -29
  77. package/dist/cross-schema.js.map +0 -1
  78. package/dist/derive.d.ts +0 -44
  79. package/dist/derive.d.ts.map +0 -1
  80. package/dist/derive.js +0 -149
  81. package/dist/derive.js.map +0 -1
  82. package/dist/draft.d.ts +0 -28
  83. package/dist/draft.d.ts.map +0 -1
  84. package/dist/draft.js +0 -157
  85. package/dist/draft.js.map +0 -1
  86. package/dist/errors.d.ts +0 -141
  87. package/dist/errors.d.ts.map +0 -1
  88. package/dist/errors.js +0 -185
  89. package/dist/errors.js.map +0 -1
  90. package/dist/execute.d.ts +0 -52
  91. package/dist/execute.d.ts.map +0 -1
  92. package/dist/execute.js +0 -511
  93. package/dist/execute.js.map +0 -1
  94. package/dist/fetch.d.ts +0 -15
  95. package/dist/fetch.d.ts.map +0 -1
  96. package/dist/fetch.js +0 -102
  97. package/dist/fetch.js.map +0 -1
  98. package/dist/fire.d.ts +0 -43
  99. package/dist/fire.d.ts.map +0 -1
  100. package/dist/fire.js +0 -185
  101. package/dist/fire.js.map +0 -1
  102. package/dist/guards.d.ts +0 -17
  103. package/dist/guards.d.ts.map +0 -1
  104. package/dist/guards.js +0 -25
  105. package/dist/guards.js.map +0 -1
  106. package/dist/index.d.ts +0 -59
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/index.js +0 -61
  109. package/dist/index.js.map +0 -1
  110. package/dist/internal/cross-batch.d.ts +0 -44
  111. package/dist/internal/cross-batch.d.ts.map +0 -1
  112. package/dist/internal/cross-batch.js +0 -58
  113. package/dist/internal/cross-batch.js.map +0 -1
  114. package/dist/internal/fork-ctx.d.ts +0 -41
  115. package/dist/internal/fork-ctx.d.ts.map +0 -1
  116. package/dist/internal/fork-ctx.js +0 -47
  117. package/dist/internal/fork-ctx.js.map +0 -1
  118. package/dist/internal/signal-ref.d.ts +0 -15
  119. package/dist/internal/signal-ref.d.ts.map +0 -1
  120. package/dist/internal/signal-ref.js +0 -41
  121. package/dist/internal/signal-ref.js.map +0 -1
  122. package/dist/internal/topo-saves.d.ts +0 -53
  123. package/dist/internal/topo-saves.d.ts.map +0 -1
  124. package/dist/internal/topo-saves.js +0 -449
  125. package/dist/internal/topo-saves.js.map +0 -1
  126. package/dist/internal/topo-snapshots.d.ts +0 -57
  127. package/dist/internal/topo-snapshots.d.ts.map +0 -1
  128. package/dist/internal/topo-snapshots.js +0 -372
  129. package/dist/internal/topo-snapshots.js.map +0 -1
  130. package/dist/internal/topo-store-read.d.ts +0 -70
  131. package/dist/internal/topo-store-read.d.ts.map +0 -1
  132. package/dist/internal/topo-store-read.js +0 -221
  133. package/dist/internal/topo-store-read.js.map +0 -1
  134. package/dist/internal/topo-store.d.ts +0 -26
  135. package/dist/internal/topo-store.d.ts.map +0 -1
  136. package/dist/internal/topo-store.js +0 -621
  137. package/dist/internal/topo-store.js.map +0 -1
  138. package/dist/internal/tracing.d.ts +0 -98
  139. package/dist/internal/tracing.d.ts.map +0 -1
  140. package/dist/internal/tracing.js +0 -102
  141. package/dist/internal/tracing.js.map +0 -1
  142. package/dist/internal/trails-db.d.ts +0 -16
  143. package/dist/internal/trails-db.d.ts.map +0 -1
  144. package/dist/internal/trails-db.js +0 -118
  145. package/dist/internal/trails-db.js.map +0 -1
  146. package/dist/internal/zod-wrappers.d.ts +0 -40
  147. package/dist/internal/zod-wrappers.d.ts.map +0 -1
  148. package/dist/internal/zod-wrappers.js +0 -59
  149. package/dist/internal/zod-wrappers.js.map +0 -1
  150. package/dist/layer.d.ts +0 -23
  151. package/dist/layer.d.ts.map +0 -1
  152. package/dist/layer.js +0 -21
  153. package/dist/layer.js.map +0 -1
  154. package/dist/path-security.d.ts +0 -28
  155. package/dist/path-security.d.ts.map +0 -1
  156. package/dist/path-security.js +0 -63
  157. package/dist/path-security.js.map +0 -1
  158. package/dist/patterns/bulk.d.ts +0 -15
  159. package/dist/patterns/bulk.d.ts.map +0 -1
  160. package/dist/patterns/bulk.js +0 -14
  161. package/dist/patterns/bulk.js.map +0 -1
  162. package/dist/patterns/change.d.ts +0 -10
  163. package/dist/patterns/change.d.ts.map +0 -1
  164. package/dist/patterns/change.js +0 -10
  165. package/dist/patterns/change.js.map +0 -1
  166. package/dist/patterns/date-range.d.ts +0 -10
  167. package/dist/patterns/date-range.d.ts.map +0 -1
  168. package/dist/patterns/date-range.js +0 -10
  169. package/dist/patterns/date-range.js.map +0 -1
  170. package/dist/patterns/index.d.ts +0 -9
  171. package/dist/patterns/index.d.ts.map +0 -1
  172. package/dist/patterns/index.js +0 -9
  173. package/dist/patterns/index.js.map +0 -1
  174. package/dist/patterns/pagination.d.ts +0 -18
  175. package/dist/patterns/pagination.d.ts.map +0 -1
  176. package/dist/patterns/pagination.js +0 -18
  177. package/dist/patterns/pagination.js.map +0 -1
  178. package/dist/patterns/progress.d.ts +0 -11
  179. package/dist/patterns/progress.d.ts.map +0 -1
  180. package/dist/patterns/progress.js +0 -11
  181. package/dist/patterns/progress.js.map +0 -1
  182. package/dist/patterns/sorting.d.ts +0 -13
  183. package/dist/patterns/sorting.d.ts.map +0 -1
  184. package/dist/patterns/sorting.js +0 -10
  185. package/dist/patterns/sorting.js.map +0 -1
  186. package/dist/patterns/status.d.ts +0 -15
  187. package/dist/patterns/status.d.ts.map +0 -1
  188. package/dist/patterns/status.js +0 -9
  189. package/dist/patterns/status.js.map +0 -1
  190. package/dist/patterns/timestamps.d.ts +0 -10
  191. package/dist/patterns/timestamps.d.ts.map +0 -1
  192. package/dist/patterns/timestamps.js +0 -10
  193. package/dist/patterns/timestamps.js.map +0 -1
  194. package/dist/redaction/index.d.ts +0 -4
  195. package/dist/redaction/index.d.ts.map +0 -1
  196. package/dist/redaction/index.js +0 -3
  197. package/dist/redaction/index.js.map +0 -1
  198. package/dist/redaction/patterns.d.ts +0 -9
  199. package/dist/redaction/patterns.d.ts.map +0 -1
  200. package/dist/redaction/patterns.js +0 -39
  201. package/dist/redaction/patterns.js.map +0 -1
  202. package/dist/redaction/redactor.d.ts +0 -27
  203. package/dist/redaction/redactor.d.ts.map +0 -1
  204. package/dist/redaction/redactor.js +0 -89
  205. package/dist/redaction/redactor.js.map +0 -1
  206. package/dist/resilience.d.ts +0 -34
  207. package/dist/resilience.d.ts.map +0 -1
  208. package/dist/resilience.js +0 -164
  209. package/dist/resilience.js.map +0 -1
  210. package/dist/resource-config.d.ts +0 -22
  211. package/dist/resource-config.d.ts.map +0 -1
  212. package/dist/resource-config.js +0 -210
  213. package/dist/resource-config.js.map +0 -1
  214. package/dist/resource.d.ts +0 -74
  215. package/dist/resource.d.ts.map +0 -1
  216. package/dist/resource.js +0 -61
  217. package/dist/resource.js.map +0 -1
  218. package/dist/result.d.ts +0 -57
  219. package/dist/result.d.ts.map +0 -1
  220. package/dist/result.js +0 -156
  221. package/dist/result.js.map +0 -1
  222. package/dist/run.d.ts +0 -28
  223. package/dist/run.d.ts.map +0 -1
  224. package/dist/run.js +0 -35
  225. package/dist/run.js.map +0 -1
  226. package/dist/serialization.d.ts +0 -27
  227. package/dist/serialization.d.ts.map +0 -1
  228. package/dist/serialization.js +0 -154
  229. package/dist/serialization.js.map +0 -1
  230. package/dist/signal.d.ts +0 -33
  231. package/dist/signal.d.ts.map +0 -1
  232. package/dist/signal.js +0 -17
  233. package/dist/signal.js.map +0 -1
  234. package/dist/store/accessor-protocol.d.ts +0 -51
  235. package/dist/store/accessor-protocol.d.ts.map +0 -1
  236. package/dist/store/accessor-protocol.js +0 -2
  237. package/dist/store/accessor-protocol.js.map +0 -1
  238. package/dist/store/index.d.ts +0 -2
  239. package/dist/store/index.d.ts.map +0 -1
  240. package/dist/store/index.js +0 -2
  241. package/dist/store/index.js.map +0 -1
  242. package/dist/surface-filter.d.ts +0 -13
  243. package/dist/surface-filter.d.ts.map +0 -1
  244. package/dist/surface-filter.js +0 -76
  245. package/dist/surface-filter.js.map +0 -1
  246. package/dist/topo-store.d.ts +0 -62
  247. package/dist/topo-store.d.ts.map +0 -1
  248. package/dist/topo-store.js +0 -319
  249. package/dist/topo-store.js.map +0 -1
  250. package/dist/topo.d.ts +0 -39
  251. package/dist/topo.d.ts.map +0 -1
  252. package/dist/topo.js +0 -241
  253. package/dist/topo.js.map +0 -1
  254. package/dist/trail.d.ts +0 -163
  255. package/dist/trail.d.ts.map +0 -1
  256. package/dist/trail.js +0 -75
  257. package/dist/trail.js.map +0 -1
  258. package/dist/trails/derive-trail.d.ts +0 -66
  259. package/dist/trails/derive-trail.d.ts.map +0 -1
  260. package/dist/trails/derive-trail.js +0 -378
  261. package/dist/trails/derive-trail.js.map +0 -1
  262. package/dist/trails/index.d.ts +0 -5
  263. package/dist/trails/index.d.ts.map +0 -1
  264. package/dist/trails/index.js +0 -3
  265. package/dist/trails/index.js.map +0 -1
  266. package/dist/trails/ingest.d.ts +0 -24
  267. package/dist/trails/ingest.d.ts.map +0 -1
  268. package/dist/trails/ingest.js +0 -63
  269. package/dist/trails/ingest.js.map +0 -1
  270. package/dist/transport-error-map.d.ts +0 -106
  271. package/dist/transport-error-map.d.ts.map +0 -1
  272. package/dist/transport-error-map.js +0 -24
  273. package/dist/transport-error-map.js.map +0 -1
  274. package/dist/type-checks.test-d.d.ts +0 -71
  275. package/dist/type-checks.test-d.d.ts.map +0 -1
  276. package/dist/type-checks.test-d.js +0 -12
  277. package/dist/type-checks.test-d.js.map +0 -1
  278. package/dist/type-utils.d.ts +0 -33
  279. package/dist/type-utils.d.ts.map +0 -1
  280. package/dist/type-utils.js +0 -12
  281. package/dist/type-utils.js.map +0 -1
  282. package/dist/types.d.ts +0 -177
  283. package/dist/types.d.ts.map +0 -1
  284. package/dist/types.js +0 -3
  285. package/dist/types.js.map +0 -1
  286. package/dist/validate-established-topo.d.ts +0 -76
  287. package/dist/validate-established-topo.d.ts.map +0 -1
  288. package/dist/validate-established-topo.js +0 -43
  289. package/dist/validate-established-topo.js.map +0 -1
  290. package/dist/validate-topo.d.ts +0 -24
  291. package/dist/validate-topo.d.ts.map +0 -1
  292. package/dist/validate-topo.js +0 -193
  293. package/dist/validate-topo.js.map +0 -1
  294. package/dist/validation.d.ts +0 -27
  295. package/dist/validation.d.ts.map +0 -1
  296. package/dist/validation.js +0 -165
  297. package/dist/validation.js.map +0 -1
  298. package/dist/workspace.d.ts +0 -25
  299. package/dist/workspace.d.ts.map +0 -1
  300. package/dist/workspace.js +0 -57
  301. package/dist/workspace.js.map +0 -1
  302. package/src/__tests__/blob-ref.test.ts +0 -103
  303. package/src/__tests__/branded.test.ts +0 -148
  304. package/src/__tests__/collections.test.ts +0 -126
  305. package/src/__tests__/context.test.ts +0 -77
  306. package/src/__tests__/contour.test.ts +0 -263
  307. package/src/__tests__/derive-trail.test.ts +0 -668
  308. package/src/__tests__/derive.test.ts +0 -260
  309. package/src/__tests__/errors.test.ts +0 -427
  310. package/src/__tests__/execute.test.ts +0 -1563
  311. package/src/__tests__/fetch.test.ts +0 -217
  312. package/src/__tests__/fire.test.ts +0 -1056
  313. package/src/__tests__/fork-ctx.test.ts +0 -83
  314. package/src/__tests__/guards.test.ts +0 -102
  315. package/src/__tests__/ingest.test.ts +0 -191
  316. package/src/__tests__/layer.test.ts +0 -125
  317. package/src/__tests__/path-security.test.ts +0 -114
  318. package/src/__tests__/patterns.test.ts +0 -273
  319. package/src/__tests__/redaction.test.ts +0 -244
  320. package/src/__tests__/resilience.test.ts +0 -246
  321. package/src/__tests__/resource.test.ts +0 -204
  322. package/src/__tests__/result.test.ts +0 -155
  323. package/src/__tests__/run.test.ts +0 -181
  324. package/src/__tests__/serialization.test.ts +0 -401
  325. package/src/__tests__/service-config.test.ts +0 -224
  326. package/src/__tests__/signal.test.ts +0 -82
  327. package/src/__tests__/surface-filter.test.ts +0 -196
  328. package/src/__tests__/topo-store-read.test.ts +0 -256
  329. package/src/__tests__/topo-store.test.ts +0 -869
  330. package/src/__tests__/topo.test.ts +0 -573
  331. package/src/__tests__/trail-permit.test.ts +0 -60
  332. package/src/__tests__/trail.test.ts +0 -426
  333. package/src/__tests__/trails-db.test.ts +0 -198
  334. package/src/__tests__/transport-error-map.test.ts +0 -82
  335. package/src/__tests__/type-utils.test.ts +0 -118
  336. package/src/__tests__/validate-topo.test.ts +0 -611
  337. package/src/__tests__/validation.test.ts +0 -336
  338. package/src/__tests__/workspace.test.ts +0 -183
  339. package/src/__tests__/zod-wrappers.test.ts +0 -62
  340. package/src/internal/topo-snapshots.ts +0 -517
  341. package/src/internal/topo-store-read.ts +0 -481
  342. package/src/internal/topo-store.ts +0 -1192
  343. package/src/internal/tracing.ts +0 -187
  344. package/src/topo-store.ts +0 -510
  345. package/src/type-checks.test-d.ts +0 -106
  346. package/tsconfig.json +0 -9
  347. package/tsconfig.tests.json +0 -10
  348. package/tsconfig.tsbuildinfo +0 -1
@@ -1,246 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import {
4
- NetworkError,
5
- ValidationError,
6
- TimeoutError,
7
- CancelledError,
8
- RateLimitError,
9
- } from '../errors.js';
10
- import {
11
- retry,
12
- withTimeout,
13
- shouldRetry,
14
- deriveBackoffDelay,
15
- } from '../resilience.js';
16
- import { Result } from '../result.js';
17
-
18
- // ---------------------------------------------------------------------------
19
- // shouldRetry
20
- // ---------------------------------------------------------------------------
21
-
22
- describe('shouldRetry', () => {
23
- test('returns true for retryable TrailsErrors', () => {
24
- expect(shouldRetry(new NetworkError('down'))).toBe(true);
25
- expect(shouldRetry(new TimeoutError('slow'))).toBe(true);
26
- expect(shouldRetry(new RateLimitError('throttled'))).toBe(true);
27
- });
28
-
29
- test('returns false for non-retryable TrailsErrors', () => {
30
- expect(shouldRetry(new ValidationError('bad'))).toBe(false);
31
- expect(shouldRetry(new CancelledError('stop'))).toBe(false);
32
- });
33
-
34
- test('returns false for plain Error', () => {
35
- expect(shouldRetry(new Error('generic'))).toBe(false);
36
- });
37
- });
38
-
39
- // ---------------------------------------------------------------------------
40
- // deriveBackoffDelay
41
- // ---------------------------------------------------------------------------
42
-
43
- describe('deriveBackoffDelay', () => {
44
- test('returns a number >= 0', () => {
45
- for (let i = 0; i < 20; i += 1) {
46
- const delay = deriveBackoffDelay(0);
47
- expect(delay).toBeGreaterThanOrEqual(0);
48
- }
49
- });
50
-
51
- test('respects maxDelay cap', () => {
52
- for (let i = 0; i < 20; i += 1) {
53
- const delay = deriveBackoffDelay(10, { maxDelay: 100 });
54
- expect(delay).toBeLessThanOrEqual(100);
55
- }
56
- });
57
-
58
- test('uses provided baseDelay and backoffFactor', () => {
59
- // With attempt=0, exponential = base * factor^0 = base
60
- // Jitter is [0, base], so delay <= base
61
- for (let i = 0; i < 20; i += 1) {
62
- const delay = deriveBackoffDelay(0, { backoffFactor: 3, baseDelay: 500 });
63
- expect(delay).toBeLessThanOrEqual(500);
64
- }
65
- });
66
-
67
- test('scales with attempt number', () => {
68
- // Higher attempts should have higher caps (on average)
69
- const samples0 = Array.from({ length: 100 }, () =>
70
- deriveBackoffDelay(0, { baseDelay: 100 })
71
- );
72
- const samples3 = Array.from({ length: 100 }, () =>
73
- deriveBackoffDelay(3, { baseDelay: 100 })
74
- );
75
- const avg0 = samples0.reduce((a, b) => a + b, 0) / samples0.length;
76
- const avg3 = samples3.reduce((a, b) => a + b, 0) / samples3.length;
77
- // Attempt 3 should have a noticeably higher average
78
- expect(avg3).toBeGreaterThan(avg0);
79
- });
80
- });
81
-
82
- // ---------------------------------------------------------------------------
83
- // retry
84
- // ---------------------------------------------------------------------------
85
-
86
- describe('retry', () => {
87
- test('returns Ok on first success', async () => {
88
- let calls = 0;
89
- const result = await retry(() => {
90
- calls += 1;
91
- return Promise.resolve(Result.ok('done'));
92
- });
93
- expect(result.isOk()).toBe(true);
94
- expect(result.unwrap()).toBe('done');
95
- expect(calls).toBe(1);
96
- });
97
-
98
- test('retries on retryable error', async () => {
99
- const responses: Result<string, Error>[] = [
100
- Result.err(new NetworkError('fail')),
101
- Result.err(new NetworkError('fail')),
102
- Result.ok('recovered'),
103
- ];
104
- let calls = 0;
105
- const result = await retry(
106
- () => {
107
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- index is bounded by maxAttempts
108
- const r = responses[calls]!;
109
- calls += 1;
110
- return Promise.resolve(r);
111
- },
112
- { baseDelay: 1, maxAttempts: 3 }
113
- );
114
- expect(result.isOk()).toBe(true);
115
- expect(result.unwrap()).toBe('recovered');
116
- expect(calls).toBe(3);
117
- });
118
-
119
- test('does not retry non-retryable errors', async () => {
120
- let calls = 0;
121
- const result = await retry(
122
- () => {
123
- calls += 1;
124
- return Promise.resolve(Result.err(new ValidationError('bad')));
125
- },
126
- { baseDelay: 1, maxAttempts: 3 }
127
- );
128
- expect(result.isErr()).toBe(true);
129
- expect(calls).toBe(1);
130
- });
131
-
132
- test('gives up after maxAttempts', async () => {
133
- let calls = 0;
134
- const result = await retry(
135
- () => {
136
- calls += 1;
137
- return Promise.resolve(Result.err(new NetworkError('fail')));
138
- },
139
- { baseDelay: 1, maxAttempts: 2 }
140
- );
141
- expect(result.isErr()).toBe(true);
142
- expect(calls).toBe(2);
143
- });
144
-
145
- test('respects custom shouldRetry predicate', async () => {
146
- let calls = 0;
147
- const result = await retry(
148
- () => {
149
- calls += 1;
150
- return Promise.resolve(Result.err(new ValidationError('bad')));
151
- },
152
- {
153
- baseDelay: 1,
154
- maxAttempts: 3,
155
- // always retry
156
- shouldRetry: () => true,
157
- }
158
- );
159
- expect(result.isErr()).toBe(true);
160
- expect(calls).toBe(3);
161
- });
162
-
163
- test('cancels on abort signal', async () => {
164
- const controller = new AbortController();
165
- controller.abort();
166
-
167
- const result = await retry(
168
- () => Promise.resolve(Result.ok('should not run')),
169
- {
170
- signal: controller.signal,
171
- }
172
- );
173
- expect(result.isErr()).toBe(true);
174
- const err = result as unknown as { error: Error };
175
- expect(err.error).toBeInstanceOf(CancelledError);
176
- });
177
- });
178
-
179
- // ---------------------------------------------------------------------------
180
- // withTimeout
181
- // ---------------------------------------------------------------------------
182
-
183
- describe('withTimeout', () => {
184
- test('returns result when function completes in time', async () => {
185
- const result = await withTimeout(
186
- () => Promise.resolve(Result.ok('fast')),
187
- 1000
188
- );
189
- expect(result.isOk()).toBe(true);
190
- expect(result.unwrap()).toBe('fast');
191
- });
192
-
193
- test('returns TimeoutError when function exceeds timeout', async () => {
194
- const result = await withTimeout(
195
- () =>
196
- // oxlint-disable-next-line avoid-new -- Promise constructor needed for setTimeout-based delay
197
- new Promise<Result<string, Error>>((resolve) => {
198
- setTimeout(() => resolve(Result.ok('slow')), 500);
199
- }),
200
- 10
201
- );
202
- expect(result.isErr()).toBe(true);
203
- const err1 = result as unknown as { error: TimeoutError };
204
- expect(err1.error).toBeInstanceOf(TimeoutError);
205
- expect(err1.error.message).toContain('timed out');
206
- });
207
-
208
- test('returns CancelledError when signal is already aborted', async () => {
209
- const controller = new AbortController();
210
- controller.abort();
211
-
212
- const result = await withTimeout(
213
- () => Promise.resolve(Result.ok('value')),
214
- 1000,
215
- controller.signal
216
- );
217
- expect(result.isErr()).toBe(true);
218
- const err2 = result as unknown as { error: Error };
219
- expect(err2.error).toBeInstanceOf(CancelledError);
220
- });
221
-
222
- test('handles function that throws', async () => {
223
- const result = await withTimeout(
224
- () => Promise.reject(new Error('unexpected')),
225
- 1000
226
- );
227
- expect(result.isErr()).toBe(true);
228
- const err3 = result as unknown as { error: Error };
229
- expect(err3.error.message).toBe('unexpected');
230
- });
231
-
232
- test('includes timeout ms in error context', async () => {
233
- const result = await withTimeout(
234
- () =>
235
- // oxlint-disable-next-line avoid-new -- Promise constructor needed for setTimeout-based delay
236
- new Promise<Result<string, Error>>((resolve) => {
237
- setTimeout(() => resolve(Result.ok('slow')), 500);
238
- }),
239
- 5
240
- );
241
- expect(result.isErr()).toBe(true);
242
- const err4 = result as unknown as { error: TimeoutError };
243
- expect(err4.error).toBeInstanceOf(TimeoutError);
244
- expect(err4.error.context?.['timeoutMs']).toBe(5);
245
- });
246
- });
@@ -1,204 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { z } from 'zod';
3
-
4
- import {
5
- Result,
6
- createTrailContext,
7
- findDuplicateResourceId,
8
- isResource,
9
- resource,
10
- } from '../index.js';
11
- import type {
12
- Resource,
13
- ResourceContext,
14
- ResourceSpec,
15
- TrailContext,
16
- } from '../index.js';
17
-
18
- const resourceCtx: ResourceContext = {
19
- config: {},
20
- cwd: '/tmp/trails',
21
- env: { DATABASE_URL: 'file::memory:' },
22
- workspaceRoot: '/tmp',
23
- };
24
-
25
- let disposedValue: number | undefined;
26
-
27
- const counterResourceSpec: ResourceSpec<number> = {
28
- create: () => Result.ok(3),
29
- description: 'Counter resource',
30
- dispose: (value) => {
31
- disposedValue = value;
32
- },
33
- health: (value) => Result.ok({ healthy: value > 0 }),
34
- meta: { domain: 'data' },
35
- mock: () => 1,
36
- };
37
-
38
- const resolvedResourceCtx = (id: string, instance: unknown): TrailContext =>
39
- createTrailContext({
40
- abortSignal: new AbortController().signal,
41
- extensions: { [id]: instance },
42
- requestId: `${id}-request`,
43
- });
44
-
45
- describe('resource types', () => {
46
- test('ResourceContext exposes the stable process-scoped fields', () => {
47
- expect(resourceCtx.cwd).toBe('/tmp/trails');
48
- expect(resourceCtx.env?.DATABASE_URL).toBe('file::memory:');
49
- expect(resourceCtx.workspaceRoot).toBe('/tmp');
50
- });
51
-
52
- test('ResourceSpec stores description and meta', () => {
53
- expect(counterResourceSpec.description).toBe('Counter resource');
54
- expect(counterResourceSpec.meta).toEqual({ domain: 'data' });
55
- });
56
-
57
- test('ResourceSpec can reserve a config schema for future composition', () => {
58
- const config = z.object({ url: z.string().url() });
59
- const spec: ResourceSpec<number> = {
60
- config,
61
- create: () => Result.ok(1),
62
- };
63
-
64
- expect(spec.config).toBe(config);
65
- });
66
-
67
- test('ResourceSpec create and health callbacks are callable', async () => {
68
- const result = await counterResourceSpec.create(resourceCtx);
69
- expect(result.isOk()).toBe(true);
70
- expect(result.unwrap()).toBe(3);
71
-
72
- const health = await counterResourceSpec.health?.(result.unwrap());
73
- expect(health?.isOk()).toBe(true);
74
- expect(health?.unwrap()).toEqual({ healthy: true });
75
- });
76
-
77
- test('ResourceSpec mock and dispose callbacks are callable', async () => {
78
- disposedValue = undefined;
79
- const result = await counterResourceSpec.create(resourceCtx);
80
- expect(result.isOk()).toBe(true);
81
-
82
- const instance = result.unwrap();
83
- expect(await counterResourceSpec.mock?.()).toBe(1);
84
-
85
- await counterResourceSpec.dispose?.(instance);
86
- expect(disposedValue).toBe(3);
87
- });
88
-
89
- test('ResourceSpec create can be async', async () => {
90
- const spec: ResourceSpec<number> = {
91
- create: async () => {
92
- await Bun.sleep(0);
93
- return Result.ok(7);
94
- },
95
- };
96
-
97
- const result = await spec.create(resourceCtx);
98
- expect(result.isOk()).toBe(true);
99
- expect(result.unwrap()).toBe(7);
100
- });
101
-
102
- test('Resource carries identity alongside the shared spec fields', async () => {
103
- const counterResource: Resource<number> = {
104
- from(ctx) {
105
- return ctx.resource(this);
106
- },
107
- id: 'counter.main',
108
- kind: 'resource',
109
- ...counterResourceSpec,
110
- };
111
-
112
- expect(counterResource.kind).toBe('resource');
113
- expect(counterResource.id).toBe('counter.main');
114
-
115
- const created = await counterResource.create(resourceCtx);
116
- expect(created.isOk()).toBe(true);
117
- expect(created.unwrap()).toBe(3);
118
- expect(await counterResource.mock?.()).toBe(1);
119
- });
120
- });
121
-
122
- describe('resource()', () => {
123
- test('returns a frozen resource object with kind and id', () => {
124
- const counter = resource('counter.main', counterResourceSpec);
125
-
126
- expect(counter.kind).toBe('resource');
127
- expect(counter.id).toBe('counter.main');
128
- expect(Object.isFrozen(counter)).toBe(true);
129
- });
130
-
131
- test('rejects resource ids containing a scope separator', () => {
132
- expect(() => resource('billing:primary', counterResourceSpec)).toThrow(
133
- 'Resource "billing:primary" is invalid because resource ids may not contain ":"'
134
- );
135
- });
136
-
137
- test('infers the resource type through from(ctx)', () => {
138
- const db = resource('db.main', {
139
- create: () =>
140
- Result.ok({
141
- query(sql: string) {
142
- return sql.length;
143
- },
144
- }),
145
- description: 'Typed database resource',
146
- });
147
-
148
- const store = {
149
- query(sql: string) {
150
- return sql.length;
151
- },
152
- };
153
- const ctx = resolvedResourceCtx('db.main', store);
154
-
155
- const resolved = db.from(ctx);
156
- expect(resolved.query('select 1')).toBe(8);
157
- });
158
-
159
- test('from(ctx) throws when the resource is missing', () => {
160
- const counter = resource('counter.main', counterResourceSpec);
161
- const ctx = createTrailContext({
162
- abortSignal: new AbortController().signal,
163
- requestId: 'missing-resource',
164
- });
165
-
166
- expect(() => counter.from(ctx)).toThrow(
167
- 'Resource "counter.main" not provisioned in context'
168
- );
169
- });
170
-
171
- test('from(ctx) returns undefined when the resource key exists with an undefined value', () => {
172
- const optional = resource<undefined>('optional.main', {
173
- create: () => Result.ok<undefined>(),
174
- });
175
- const ctx = createTrailContext({
176
- abortSignal: new AbortController().signal,
177
- extensions: { [optional.id]: undefined },
178
- requestId: 'undefined-resource',
179
- });
180
-
181
- expect(optional.from(ctx)).toBeUndefined();
182
- });
183
- });
184
-
185
- describe('resource helpers', () => {
186
- test('isResource identifies resource definitions', () => {
187
- const counter = resource('counter.main', counterResourceSpec);
188
-
189
- expect(isResource(counter)).toBe(true);
190
- expect(isResource({ id: 'counter.main', kind: 'trail' })).toBe(false);
191
- expect(isResource(null)).toBe(false);
192
- });
193
-
194
- test('findDuplicateResourceId returns the first repeated ID', () => {
195
- const first = resource('counter.main', counterResourceSpec);
196
- const duplicate = resource('counter.main', counterResourceSpec);
197
- const other = resource('counter.secondary', counterResourceSpec);
198
-
199
- expect(findDuplicateResourceId([first, other])).toBeUndefined();
200
- expect(findDuplicateResourceId([first, other, duplicate])).toBe(
201
- 'counter.main'
202
- );
203
- });
204
- });
@@ -1,155 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import { Result } from '../result';
4
-
5
- describe('Result', () => {
6
- describe('construction', () => {
7
- test('Result.ok() creates a success result', () => {
8
- const result = Result.ok(42);
9
- expect(result.isOk()).toBe(true);
10
- expect(result.isErr()).toBe(false);
11
- });
12
-
13
- test('Result.err() creates a failure result', () => {
14
- const result = Result.err(new Error('fail'));
15
- expect(result.isOk()).toBe(false);
16
- expect(result.isErr()).toBe(true);
17
- });
18
- });
19
-
20
- describe('type narrowing', () => {
21
- test('isOk() narrows to Ok with value access', () => {
22
- const result: Result<number, Error> = Result.ok(42);
23
- expect(result.isOk()).toBe(true);
24
- expect(result.unwrap()).toBe(42);
25
- });
26
-
27
- test('isErr() narrows to Err with error access', () => {
28
- const error = new Error('boom');
29
- const result: Result<number, Error> = Result.err(error);
30
- expect(result.isErr()).toBe(true);
31
- expect((result as { error: Error }).error).toBe(error);
32
- });
33
- });
34
-
35
- describe('map()', () => {
36
- test('transforms the value of an Ok', () => {
37
- const result = Result.ok(10).map((n) => n * 2);
38
- expect(result.unwrap()).toBe(20);
39
- });
40
-
41
- test('passes through on Err', () => {
42
- const error = new Error('fail');
43
- const result = Result.err<Error>(error).map((_n: number) => 999);
44
- expect(result.isErr()).toBe(true);
45
- expect((result as { error: Error }).error).toBe(error);
46
- });
47
- });
48
-
49
- describe('flatMap()', () => {
50
- test('chains successful results', () => {
51
- const result = Result.ok(10).flatMap((n) => Result.ok(n + 5));
52
- expect(result.unwrap()).toBe(15);
53
- });
54
-
55
- test('short-circuits on error', () => {
56
- const error = new Error('first');
57
- const result = Result.err<Error>(error).flatMap((_n: number) =>
58
- Result.ok(999)
59
- );
60
- expect(result.isErr()).toBe(true);
61
- expect((result as { error: Error }).error).toBe(error);
62
- });
63
-
64
- test('chains into an error', () => {
65
- const result = Result.ok(10).flatMap(() =>
66
- Result.err(new Error('second'))
67
- );
68
- expect(result.isErr()).toBe(true);
69
- });
70
- });
71
-
72
- describe('mapErr()', () => {
73
- test('transforms the error of an Err', () => {
74
- const result = Result.err('bad').mapErr((e) => new Error(e));
75
- expect(result.isErr()).toBe(true);
76
- expect((result as { error: Error }).error).toBeInstanceOf(Error);
77
- expect((result as { error: Error }).error.message).toBe('bad');
78
- });
79
-
80
- test('passes through on Ok', () => {
81
- const result = Result.ok(42).mapErr(() => 'transformed');
82
- expect(result.unwrap()).toBe(42);
83
- });
84
- });
85
-
86
- describe('match()', () => {
87
- test('dispatches to ok handler on success', () => {
88
- const output = Result.ok(5).match({
89
- err: (e) => `error:${e}`,
90
- ok: (v) => `value:${v}`,
91
- });
92
- expect(output).toBe('value:5');
93
- });
94
-
95
- test('dispatches to err handler on failure', () => {
96
- const output = Result.err('oops').match({
97
- err: (e) => `error:${e}`,
98
- ok: (v) => `value:${v}`,
99
- });
100
- expect(output).toBe('error:oops');
101
- });
102
- });
103
-
104
- describe('unwrap()', () => {
105
- test('returns value on Ok', () => {
106
- expect(Result.ok('hello').unwrap()).toBe('hello');
107
- });
108
-
109
- test('throws on Err with Error instance', () => {
110
- const error = new Error('boom');
111
- expect(() => Result.err(error).unwrap()).toThrow('boom');
112
- });
113
-
114
- test('throws wrapped error on Err with non-Error value', () => {
115
- expect(() => Result.err('string error').unwrap()).toThrow('string error');
116
- });
117
- });
118
-
119
- describe('unwrapOr()', () => {
120
- test('returns value on Ok', () => {
121
- expect(Result.ok(42).unwrapOr(0)).toBe(42);
122
- });
123
-
124
- test('returns fallback on Err', () => {
125
- const result: Result<number, Error> = Result.err(new Error('fail'));
126
- expect(result.unwrapOr(0)).toBe(0);
127
- });
128
- });
129
-
130
- describe('Result.combine()', () => {
131
- test('collects all Ok values into an array', () => {
132
- const results = [Result.ok(1), Result.ok(2), Result.ok(3)];
133
- const combined = Result.combine(results);
134
- expect(combined.unwrap()).toEqual([1, 2, 3]);
135
- });
136
-
137
- test('returns first error encountered', () => {
138
- const err1 = new Error('first');
139
- const err2 = new Error('second');
140
- const results: Result<number, Error>[] = [
141
- Result.ok(1),
142
- Result.err(err1),
143
- Result.err(err2),
144
- ];
145
- const combined = Result.combine(results);
146
- expect(combined.isErr()).toBe(true);
147
- expect((combined as { error: Error }).error).toBe(err1);
148
- });
149
-
150
- test('returns Ok with empty array for empty input', () => {
151
- const combined = Result.combine([]);
152
- expect(combined.unwrap()).toEqual([]);
153
- });
154
- });
155
- });