@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.32

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 (249) hide show
  1. package/CHANGELOG.md +323 -8
  2. package/README.md +114 -33
  3. package/package.json +11 -1
  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/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +250 -19
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/error-projection.ts +51 -0
  19. package/src/errors.ts +335 -48
  20. package/src/execute.ts +1577 -0
  21. package/src/fire.ts +1169 -0
  22. package/src/glob.ts +81 -0
  23. package/src/index.ts +526 -35
  24. package/src/internal/fork-ctx.ts +69 -0
  25. package/src/layer-projection.ts +193 -0
  26. package/src/layer.ts +43 -6
  27. package/src/observe.ts +361 -0
  28. package/src/path-scope.ts +66 -0
  29. package/src/path-security.ts +1 -1
  30. package/src/permits.ts +12 -0
  31. package/src/redaction/patterns.ts +6 -3
  32. package/src/resilience.ts +11 -11
  33. package/src/resource-config.ts +792 -0
  34. package/src/resource.ts +194 -0
  35. package/src/result.ts +18 -0
  36. package/src/run.ts +76 -0
  37. package/src/schedule-runtime.ts +689 -0
  38. package/src/schedule.ts +326 -0
  39. package/src/serialization.ts +92 -55
  40. package/src/signal-diagnostics.ts +633 -0
  41. package/src/signal-ref.ts +87 -0
  42. package/src/signal.ts +104 -0
  43. package/src/store/accessor-protocol.ts +56 -0
  44. package/src/store/index.ts +4 -0
  45. package/src/structured-examples.ts +248 -0
  46. package/src/surface-derivation.ts +91 -0
  47. package/src/surface-filter.ts +101 -0
  48. package/src/surface-versioning.ts +42 -0
  49. package/src/topo.ts +769 -67
  50. package/src/tracing.ts +345 -0
  51. package/src/trail-id-glob.ts +15 -0
  52. package/src/trail.ts +1168 -30
  53. package/src/trails/derive-trail.ts +842 -0
  54. package/src/trails/index.ts +9 -0
  55. package/src/trails/ingest.ts +139 -0
  56. package/src/trails-db.ts +206 -0
  57. package/src/transport-error-map.ts +174 -0
  58. package/src/type-utils.ts +87 -0
  59. package/src/types.ts +254 -12
  60. package/src/validate-established-topo.ts +69 -0
  61. package/src/validate-topo.ts +414 -56
  62. package/src/validation.ts +152 -4
  63. package/src/version-marker.ts +716 -0
  64. package/src/version-resolution.ts +308 -0
  65. package/src/version-runtime.ts +120 -0
  66. package/src/webhook.ts +278 -0
  67. package/src/workspace.ts +162 -1
  68. package/src/zod-wrappers.ts +72 -0
  69. package/.turbo/turbo-build.log +0 -1
  70. package/.turbo/turbo-lint.log +0 -3
  71. package/.turbo/turbo-typecheck.log +0 -1
  72. package/dist/adapters.d.ts +0 -39
  73. package/dist/adapters.d.ts.map +0 -1
  74. package/dist/adapters.js +0 -2
  75. package/dist/adapters.js.map +0 -1
  76. package/dist/blob-ref.d.ts +0 -20
  77. package/dist/blob-ref.d.ts.map +0 -1
  78. package/dist/blob-ref.js +0 -22
  79. package/dist/blob-ref.js.map +0 -1
  80. package/dist/branded.d.ts +0 -36
  81. package/dist/branded.d.ts.map +0 -1
  82. package/dist/branded.js +0 -89
  83. package/dist/branded.js.map +0 -1
  84. package/dist/collections.d.ts +0 -31
  85. package/dist/collections.d.ts.map +0 -1
  86. package/dist/collections.js +0 -60
  87. package/dist/collections.js.map +0 -1
  88. package/dist/context.d.ts +0 -10
  89. package/dist/context.d.ts.map +0 -1
  90. package/dist/context.js +0 -15
  91. package/dist/context.js.map +0 -1
  92. package/dist/derive.d.ts +0 -33
  93. package/dist/derive.d.ts.map +0 -1
  94. package/dist/derive.js +0 -125
  95. package/dist/derive.js.map +0 -1
  96. package/dist/errors.d.ts +0 -83
  97. package/dist/errors.d.ts.map +0 -1
  98. package/dist/errors.js +0 -142
  99. package/dist/errors.js.map +0 -1
  100. package/dist/event.d.ts +0 -45
  101. package/dist/event.d.ts.map +0 -1
  102. package/dist/event.js +0 -17
  103. package/dist/event.js.map +0 -1
  104. package/dist/fetch.d.ts +0 -15
  105. package/dist/fetch.d.ts.map +0 -1
  106. package/dist/fetch.js +0 -102
  107. package/dist/fetch.js.map +0 -1
  108. package/dist/guards.d.ts +0 -17
  109. package/dist/guards.d.ts.map +0 -1
  110. package/dist/guards.js +0 -25
  111. package/dist/guards.js.map +0 -1
  112. package/dist/health.d.ts +0 -18
  113. package/dist/health.d.ts.map +0 -1
  114. package/dist/health.js +0 -5
  115. package/dist/health.js.map +0 -1
  116. package/dist/hike.d.ts +0 -36
  117. package/dist/hike.d.ts.map +0 -1
  118. package/dist/hike.js +0 -20
  119. package/dist/hike.js.map +0 -1
  120. package/dist/index.d.ts +0 -36
  121. package/dist/index.d.ts.map +0 -1
  122. package/dist/index.js +0 -40
  123. package/dist/index.js.map +0 -1
  124. package/dist/job.d.ts +0 -24
  125. package/dist/job.d.ts.map +0 -1
  126. package/dist/job.js +0 -17
  127. package/dist/job.js.map +0 -1
  128. package/dist/layer.d.ts +0 -17
  129. package/dist/layer.d.ts.map +0 -1
  130. package/dist/layer.js +0 -21
  131. package/dist/layer.js.map +0 -1
  132. package/dist/path-security.d.ts +0 -28
  133. package/dist/path-security.d.ts.map +0 -1
  134. package/dist/path-security.js +0 -63
  135. package/dist/path-security.js.map +0 -1
  136. package/dist/patterns/bulk.d.ts +0 -15
  137. package/dist/patterns/bulk.d.ts.map +0 -1
  138. package/dist/patterns/bulk.js +0 -14
  139. package/dist/patterns/bulk.js.map +0 -1
  140. package/dist/patterns/change.d.ts +0 -10
  141. package/dist/patterns/change.d.ts.map +0 -1
  142. package/dist/patterns/change.js +0 -10
  143. package/dist/patterns/change.js.map +0 -1
  144. package/dist/patterns/date-range.d.ts +0 -10
  145. package/dist/patterns/date-range.d.ts.map +0 -1
  146. package/dist/patterns/date-range.js +0 -10
  147. package/dist/patterns/date-range.js.map +0 -1
  148. package/dist/patterns/index.d.ts +0 -9
  149. package/dist/patterns/index.d.ts.map +0 -1
  150. package/dist/patterns/index.js +0 -9
  151. package/dist/patterns/index.js.map +0 -1
  152. package/dist/patterns/pagination.d.ts +0 -18
  153. package/dist/patterns/pagination.d.ts.map +0 -1
  154. package/dist/patterns/pagination.js +0 -18
  155. package/dist/patterns/pagination.js.map +0 -1
  156. package/dist/patterns/progress.d.ts +0 -11
  157. package/dist/patterns/progress.d.ts.map +0 -1
  158. package/dist/patterns/progress.js +0 -11
  159. package/dist/patterns/progress.js.map +0 -1
  160. package/dist/patterns/sorting.d.ts +0 -13
  161. package/dist/patterns/sorting.d.ts.map +0 -1
  162. package/dist/patterns/sorting.js +0 -10
  163. package/dist/patterns/sorting.js.map +0 -1
  164. package/dist/patterns/status.d.ts +0 -15
  165. package/dist/patterns/status.d.ts.map +0 -1
  166. package/dist/patterns/status.js +0 -9
  167. package/dist/patterns/status.js.map +0 -1
  168. package/dist/patterns/timestamps.d.ts +0 -10
  169. package/dist/patterns/timestamps.d.ts.map +0 -1
  170. package/dist/patterns/timestamps.js +0 -10
  171. package/dist/patterns/timestamps.js.map +0 -1
  172. package/dist/redaction/index.d.ts +0 -4
  173. package/dist/redaction/index.d.ts.map +0 -1
  174. package/dist/redaction/index.js +0 -3
  175. package/dist/redaction/index.js.map +0 -1
  176. package/dist/redaction/patterns.d.ts +0 -9
  177. package/dist/redaction/patterns.d.ts.map +0 -1
  178. package/dist/redaction/patterns.js +0 -39
  179. package/dist/redaction/patterns.js.map +0 -1
  180. package/dist/redaction/redactor.d.ts +0 -27
  181. package/dist/redaction/redactor.d.ts.map +0 -1
  182. package/dist/redaction/redactor.js +0 -89
  183. package/dist/redaction/redactor.js.map +0 -1
  184. package/dist/resilience.d.ts +0 -34
  185. package/dist/resilience.d.ts.map +0 -1
  186. package/dist/resilience.js +0 -164
  187. package/dist/resilience.js.map +0 -1
  188. package/dist/result.d.ts +0 -57
  189. package/dist/result.d.ts.map +0 -1
  190. package/dist/result.js +0 -156
  191. package/dist/result.js.map +0 -1
  192. package/dist/serialization.d.ts +0 -27
  193. package/dist/serialization.d.ts.map +0 -1
  194. package/dist/serialization.js +0 -153
  195. package/dist/serialization.js.map +0 -1
  196. package/dist/topo.d.ts +0 -18
  197. package/dist/topo.d.ts.map +0 -1
  198. package/dist/topo.js +0 -80
  199. package/dist/topo.js.map +0 -1
  200. package/dist/trail.d.ts +0 -83
  201. package/dist/trail.d.ts.map +0 -1
  202. package/dist/trail.js +0 -16
  203. package/dist/trail.js.map +0 -1
  204. package/dist/types.d.ts +0 -46
  205. package/dist/types.d.ts.map +0 -1
  206. package/dist/types.js +0 -2
  207. package/dist/types.js.map +0 -1
  208. package/dist/validate-topo.d.ts +0 -24
  209. package/dist/validate-topo.d.ts.map +0 -1
  210. package/dist/validate-topo.js +0 -156
  211. package/dist/validate-topo.js.map +0 -1
  212. package/dist/validation.d.ts +0 -27
  213. package/dist/validation.d.ts.map +0 -1
  214. package/dist/validation.js +0 -134
  215. package/dist/validation.js.map +0 -1
  216. package/dist/workspace.d.ts +0 -25
  217. package/dist/workspace.d.ts.map +0 -1
  218. package/dist/workspace.js +0 -57
  219. package/dist/workspace.js.map +0 -1
  220. package/src/__tests__/blob-ref.test.ts +0 -103
  221. package/src/__tests__/branded.test.ts +0 -148
  222. package/src/__tests__/collections.test.ts +0 -126
  223. package/src/__tests__/context.test.ts +0 -66
  224. package/src/__tests__/derive.test.ts +0 -203
  225. package/src/__tests__/errors.test.ts +0 -309
  226. package/src/__tests__/event.test.ts +0 -82
  227. package/src/__tests__/fetch.test.ts +0 -217
  228. package/src/__tests__/guards.test.ts +0 -102
  229. package/src/__tests__/hike.test.ts +0 -117
  230. package/src/__tests__/job.test.ts +0 -98
  231. package/src/__tests__/layer.test.ts +0 -224
  232. package/src/__tests__/path-security.test.ts +0 -114
  233. package/src/__tests__/patterns.test.ts +0 -273
  234. package/src/__tests__/redaction.test.ts +0 -244
  235. package/src/__tests__/resilience.test.ts +0 -246
  236. package/src/__tests__/result.test.ts +0 -155
  237. package/src/__tests__/serialization.test.ts +0 -401
  238. package/src/__tests__/topo.test.ts +0 -220
  239. package/src/__tests__/trail.test.ts +0 -179
  240. package/src/__tests__/validate-topo.test.ts +0 -280
  241. package/src/__tests__/validation.test.ts +0 -283
  242. package/src/__tests__/workspace.test.ts +0 -183
  243. package/src/adapters.ts +0 -68
  244. package/src/event.ts +0 -77
  245. package/src/health.ts +0 -23
  246. package/src/hike.ts +0 -77
  247. package/src/job.ts +0 -20
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -1,117 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { Result } from '../result';
6
- import { hike } from '../hike';
7
- import type { TrailContext } from '../types';
8
-
9
- const stubCtx: TrailContext = {
10
- requestId: 'test-123',
11
- signal: AbortSignal.timeout(5000),
12
- };
13
-
14
- describe('hike()', () => {
15
- const inputSchema = z.object({ userId: z.string() });
16
- const outputSchema = z.object({ profile: z.string() });
17
-
18
- const fetchProfile = hike('fetch-profile', {
19
- description: 'Fetch a user profile',
20
- follows: ['authenticate', 'validate-session'],
21
- implementation: (input) =>
22
- Result.ok({ profile: `Profile for ${input.userId}` }),
23
- input: inputSchema,
24
- output: outputSchema,
25
- });
26
-
27
- describe('basics', () => {
28
- test("returns kind 'hike'", () => {
29
- expect(fetchProfile.kind).toBe('hike');
30
- });
31
-
32
- test('returns correct id', () => {
33
- expect(fetchProfile.id).toBe('fetch-profile');
34
- });
35
-
36
- test('preserves follows array', () => {
37
- expect(fetchProfile.follows).toEqual([
38
- 'authenticate',
39
- 'validate-session',
40
- ]);
41
- });
42
-
43
- test('follows array is frozen', () => {
44
- expect(Object.isFrozen(fetchProfile.follows)).toBe(true);
45
- });
46
- });
47
-
48
- describe('trail compatibility', () => {
49
- test('extends Trail — has input schema', () => {
50
- const parsed = fetchProfile.input.safeParse({ userId: 'u-1' });
51
- expect(parsed.success).toBe(true);
52
- });
53
-
54
- test('extends Trail — has output schema', () => {
55
- expect(fetchProfile.output).toBeDefined();
56
- });
57
-
58
- test('extends Trail — implementation is callable', async () => {
59
- const result = await fetchProfile.implementation(
60
- { userId: 'u-1' },
61
- stubCtx
62
- );
63
- expect(result.isOk()).toBe(true);
64
- expect(result.unwrap()).toEqual({ profile: 'Profile for u-1' });
65
- });
66
-
67
- test('preserves description', () => {
68
- expect(fetchProfile.description).toBe('Fetch a user profile');
69
- });
70
-
71
- test('result object is frozen', () => {
72
- expect(Object.isFrozen(fetchProfile)).toBe(true);
73
- });
74
- });
75
-
76
- test('markers are preserved', () => {
77
- const withMarkers = hike('tagged-hike', {
78
- follows: ['setup'],
79
- implementation: () => Result.ok(),
80
- input: z.object({}),
81
- markers: { domain: 'auth' },
82
- });
83
- expect(withMarkers.markers).toEqual({ domain: 'auth' });
84
- });
85
-
86
- describe('single-object overload', () => {
87
- test('accepts spec with id property', () => {
88
- const r = hike({
89
- follows: ['entity.add', 'entity.relate'],
90
- id: 'entity.onboard',
91
- implementation: () => Result.ok(),
92
- input: z.object({}),
93
- });
94
- expect(r.id).toBe('entity.onboard');
95
- expect(r.kind).toBe('hike');
96
- expect(r.follows).toEqual(['entity.add', 'entity.relate']);
97
- });
98
-
99
- test('sync implementations are normalized to an awaitable runtime function', async () => {
100
- const r = hike({
101
- follows: ['entity.add'],
102
- id: 'entity.check',
103
- implementation: (input: { userId: string }) =>
104
- Result.ok({ profile: input.userId }),
105
- input: inputSchema,
106
- output: outputSchema,
107
- });
108
-
109
- const promise = r.implementation({ userId: 'u-2' }, stubCtx);
110
- expect(promise).toBeInstanceOf(Promise);
111
-
112
- const result = await promise;
113
- expect(result.isOk()).toBe(true);
114
- expect(result.unwrap()).toEqual({ profile: 'u-2' });
115
- });
116
- });
117
- });
@@ -1,98 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { jobOutputSchema } from '../job';
6
-
7
- describe('jobOutputSchema', () => {
8
- test('parses a valid completed job output', () => {
9
- const input = {
10
- completedAt: '2026-03-25T00:01:00Z',
11
- current: 10,
12
- jobId: 'job-123',
13
- percentage: 100,
14
- result: { rows: 42 },
15
- startedAt: '2026-03-25T00:00:00Z',
16
- status: 'completed',
17
- total: 10,
18
- };
19
-
20
- const parsed = jobOutputSchema.parse(input);
21
-
22
- expect(parsed.jobId).toBe('job-123');
23
- expect(parsed.status).toBe('completed');
24
- expect(parsed.current).toBe(10);
25
- expect(parsed.total).toBe(10);
26
- });
27
-
28
- test('parses a minimal pending job output', () => {
29
- const input = {
30
- current: 0,
31
- jobId: 'job-456',
32
- status: 'pending',
33
- total: 100,
34
- };
35
-
36
- const parsed = jobOutputSchema.parse(input);
37
-
38
- expect(parsed.jobId).toBe('job-456');
39
- expect(parsed.status).toBe('pending');
40
- expect(parsed.result).toBeUndefined();
41
- expect(parsed.error).toBeUndefined();
42
- });
43
-
44
- test('parses a failed job with error', () => {
45
- const input = {
46
- current: 3,
47
- error: 'connection reset',
48
- jobId: 'job-789',
49
- status: 'failed',
50
- total: 10,
51
- };
52
-
53
- const parsed = jobOutputSchema.parse(input);
54
-
55
- expect(parsed.status).toBe('failed');
56
- expect(parsed.error).toBe('connection reset');
57
- });
58
-
59
- test('rejects when jobId is missing', () => {
60
- const input = {
61
- current: 1,
62
- status: 'running',
63
- total: 5,
64
- };
65
-
66
- expect(() => jobOutputSchema.parse(input)).toThrow();
67
- });
68
-
69
- test('rejects an invalid status value', () => {
70
- const input = {
71
- current: 0,
72
- jobId: 'job-bad',
73
- status: 'unknown',
74
- total: 0,
75
- };
76
-
77
- expect(() => jobOutputSchema.parse(input)).toThrow();
78
- });
79
-
80
- test('composes statusFields and progressFields correctly', () => {
81
- const { shape } = jobOutputSchema;
82
-
83
- // Status field from statusFields()
84
- expect(shape.status).toBeInstanceOf(z.ZodEnum);
85
-
86
- // Progress fields from progressFields()
87
- expect(shape.current).toBeInstanceOf(z.ZodNumber);
88
- expect(shape.total).toBeInstanceOf(z.ZodNumber);
89
- expect(shape.percentage).toBeDefined();
90
-
91
- // Job-specific fields
92
- expect(shape.jobId).toBeInstanceOf(z.ZodString);
93
- expect(shape.error).toBeDefined();
94
- expect(shape.result).toBeDefined();
95
- expect(shape.startedAt).toBeDefined();
96
- expect(shape.completedAt).toBeDefined();
97
- });
98
- });
@@ -1,224 +0,0 @@
1
- /* oxlint-disable require-await -- adapter mocks and layer wrappers satisfy async interfaces without awaiting */
2
- import { describe, test, expect } from 'bun:test';
3
-
4
- import { z } from 'zod';
5
-
6
- import type {
7
- IndexAdapter,
8
- StorageAdapter,
9
- CacheAdapter,
10
- SearchOptions,
11
- SearchResult,
12
- StorageOptions,
13
- } from '../adapters';
14
- import type { HealthStatus, HealthResult, HealthCheck } from '../health';
15
- import { composeLayers } from '../layer';
16
- import type { Layer } from '../layer';
17
- import { Result } from '../result';
18
- import { trail } from '../trail';
19
- import type { TrailContext } from '../types';
20
-
21
- const stubCtx: TrailContext = {
22
- requestId: 'test-layer',
23
- signal: AbortSignal.timeout(5000),
24
- };
25
-
26
- const echoTrail = trail('echo', {
27
- implementation: (input) => Result.ok({ value: input.value }),
28
- input: z.object({ value: z.string() }),
29
- markers: { domain: 'test' },
30
- output: z.object({ value: z.string() }),
31
- });
32
-
33
- // ---------------------------------------------------------------------------
34
- // Layer tests
35
- // ---------------------------------------------------------------------------
36
-
37
- describe('Layer', () => {
38
- test('single layer wraps implementation', async () => {
39
- const prefixLayer: Layer = {
40
- name: 'prefix',
41
- wrap(_trail, impl) {
42
- return async (input, ctx) => {
43
- const result = await impl(input, ctx);
44
- return result.map((out) => ({
45
- ...out,
46
- value: `prefixed:${(out as { value: string }).value}`,
47
- }));
48
- };
49
- },
50
- };
51
-
52
- const wrapped = composeLayers(
53
- [prefixLayer],
54
- echoTrail,
55
- echoTrail.implementation
56
- );
57
- const result = await wrapped({ value: 'hello' }, stubCtx);
58
-
59
- expect(result.isOk()).toBe(true);
60
- expect(result.unwrap()).toEqual({ value: 'prefixed:hello' });
61
- });
62
-
63
- test('multiple layers compose in outermost-first order', async () => {
64
- const log: string[] = [];
65
-
66
- const outer: Layer = {
67
- name: 'outer',
68
- wrap(_trail, impl) {
69
- return async (input, ctx) => {
70
- log.push('outer:before');
71
- const r = await impl(input, ctx);
72
- log.push('outer:after');
73
- return r;
74
- };
75
- },
76
- };
77
-
78
- const inner: Layer = {
79
- name: 'inner',
80
- wrap(_trail, impl) {
81
- return async (input, ctx) => {
82
- log.push('inner:before');
83
- const r = await impl(input, ctx);
84
- log.push('inner:after');
85
- return r;
86
- };
87
- },
88
- };
89
-
90
- const wrapped = composeLayers(
91
- [outer, inner],
92
- echoTrail,
93
- echoTrail.implementation
94
- );
95
- await wrapped({ value: 'x' }, stubCtx);
96
-
97
- expect(log).toEqual([
98
- 'outer:before',
99
- 'inner:before',
100
- 'inner:after',
101
- 'outer:after',
102
- ]);
103
- });
104
-
105
- test('layer can short-circuit without calling inner implementation', async () => {
106
- const shortCircuit: Layer = {
107
- name: 'short-circuit',
108
- wrap() {
109
- return async () => Result.err(new Error('blocked'));
110
- },
111
- };
112
-
113
- const wrapped = composeLayers(
114
- [shortCircuit],
115
- echoTrail,
116
- echoTrail.implementation
117
- );
118
- const result = await wrapped({ value: 'hello' }, stubCtx);
119
-
120
- expect(result.isErr()).toBe(true);
121
- const err = result as unknown as { error: Error };
122
- expect(err.error.message).toBe('blocked');
123
- });
124
-
125
- test('layer can inspect trail markers', () => {
126
- let capturedDomain: unknown;
127
-
128
- const inspectLayer: Layer = {
129
- name: 'inspect',
130
- wrap(t, impl) {
131
- capturedDomain = t.markers?.['domain'];
132
- return impl;
133
- },
134
- };
135
-
136
- composeLayers([inspectLayer], echoTrail, echoTrail.implementation);
137
-
138
- expect(capturedDomain).toBe('test');
139
- });
140
-
141
- test('empty layers array returns implementation unchanged', () => {
142
- const wrapped = composeLayers([], echoTrail, echoTrail.implementation);
143
- expect(wrapped).toBe(echoTrail.implementation);
144
- });
145
- });
146
-
147
- // ---------------------------------------------------------------------------
148
- // Health types (compile-time verification)
149
- // ---------------------------------------------------------------------------
150
-
151
- describe('health types', () => {
152
- test('HealthResult satisfies the interface', () => {
153
- const check: HealthCheck = {
154
- latency: 12,
155
- message: 'ok',
156
- status: 'healthy',
157
- };
158
- const result: HealthResult = {
159
- checks: { db: check },
160
- status: 'healthy',
161
- uptime: 3600,
162
- version: '1.0.0',
163
- };
164
-
165
- expect(result.status).toBe('healthy');
166
- expect(result.checks['db']?.status).toBe('healthy');
167
- });
168
-
169
- test('HealthStatus union is exhaustive', () => {
170
- const statuses: HealthStatus[] = ['healthy', 'degraded', 'unhealthy'];
171
- expect(statuses).toHaveLength(3);
172
- });
173
- });
174
-
175
- // ---------------------------------------------------------------------------
176
- // Adapter types (compile-time verification)
177
- // ---------------------------------------------------------------------------
178
-
179
- describe('adapter types', () => {
180
- test('IndexAdapter mock satisfies the interface', () => {
181
- const mock: IndexAdapter = {
182
- index: async () => Result.ok(),
183
- remove: async () => Result.ok(),
184
- search: async () => Result.ok([]),
185
- };
186
- expect(mock.index).toBeDefined();
187
- expect(mock.search).toBeDefined();
188
- expect(mock.remove).toBeDefined();
189
- });
190
-
191
- test('StorageAdapter mock satisfies the interface', () => {
192
- const mock: StorageAdapter = {
193
- delete: async () => Result.ok(),
194
- get: async () => Result.ok('value'),
195
- has: async () => Result.ok(true),
196
- set: async () => Result.ok(),
197
- };
198
- expect(mock.has).toBeDefined();
199
- });
200
-
201
- test('CacheAdapter mock satisfies the interface', () => {
202
- const mock: CacheAdapter = {
203
- clear: async () => Result.ok(),
204
- delete: async () => Result.ok(),
205
- get: async () => Result.ok(),
206
- set: async () => Result.ok(),
207
- };
208
- expect(mock.clear).toBeDefined();
209
- });
210
-
211
- test('SearchOptions and SearchResult satisfy their shapes', () => {
212
- const opts: SearchOptions = { filters: { tag: 'a' }, limit: 10, offset: 0 };
213
- const hit: SearchResult = {
214
- document: { title: 'x' },
215
- id: '1',
216
- score: 0.95,
217
- };
218
- const sopts: StorageOptions = { ttl: 5000 };
219
-
220
- expect(opts.limit).toBe(10);
221
- expect(hit.score).toBe(0.95);
222
- expect(sopts.ttl).toBe(5000);
223
- });
224
- });
@@ -1,114 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { resolve, join } from 'node:path';
3
-
4
- import { PermissionError } from '../errors.js';
5
- import { securePath, isPathSafe, resolveSafePath } from '../path-security.js';
6
-
7
- // ---------------------------------------------------------------------------
8
- // securePath
9
- // ---------------------------------------------------------------------------
10
-
11
- describe('securePath', () => {
12
- const base = '/project/workspace';
13
-
14
- test('resolves a simple relative path', () => {
15
- const result = securePath(base, 'src/index.ts');
16
- expect(result.isOk()).toBe(true);
17
- expect(result.unwrap()).toBe(resolve(base, 'src/index.ts'));
18
- });
19
-
20
- test('resolves nested paths', () => {
21
- const result = securePath(base, 'src/../lib/utils.ts');
22
- expect(result.isOk()).toBe(true);
23
- expect(result.unwrap()).toBe(resolve(base, 'lib/utils.ts'));
24
- });
25
-
26
- test('accepts the base directory itself', () => {
27
- const result = securePath(base, '.');
28
- expect(result.isOk()).toBe(true);
29
- expect(result.unwrap()).toBe(resolve(base));
30
- });
31
-
32
- test('rejects path traversal with ..', () => {
33
- const result = securePath(base, '../../etc/passwd');
34
- expect(result.isErr()).toBe(true);
35
- const err = result as unknown as { error: PermissionError };
36
- expect(err.error).toBeInstanceOf(PermissionError);
37
- expect(err.error.message).toContain('Path traversal detected');
38
- });
39
-
40
- test('rejects absolute paths outside base', () => {
41
- const result = securePath(base, '/etc/passwd');
42
- expect(result.isErr()).toBe(true);
43
- const err = result as unknown as { error: PermissionError };
44
- expect(err.error).toBeInstanceOf(PermissionError);
45
- });
46
-
47
- test('rejects sneaky traversal (subdir/../../..)', () => {
48
- const result = securePath(base, 'src/../../..');
49
- expect(result.isErr()).toBe(true);
50
- });
51
-
52
- test('allows paths that mention .. but stay inside', () => {
53
- const result = securePath(base, 'src/../src/file.ts');
54
- expect(result.isOk()).toBe(true);
55
- expect(result.unwrap()).toBe(resolve(base, 'src/file.ts'));
56
- });
57
- });
58
-
59
- // ---------------------------------------------------------------------------
60
- // isPathSafe
61
- // ---------------------------------------------------------------------------
62
-
63
- describe('isPathSafe', () => {
64
- const base = '/project/workspace';
65
-
66
- test('returns true for child paths', () => {
67
- expect(isPathSafe(base, 'src/index.ts')).toBe(true);
68
- });
69
-
70
- test('returns true for the base directory', () => {
71
- expect(isPathSafe(base, '.')).toBe(true);
72
- });
73
-
74
- test('returns false for traversal', () => {
75
- expect(isPathSafe(base, '../secret')).toBe(false);
76
- });
77
-
78
- test('returns false for absolute paths outside base', () => {
79
- expect(isPathSafe(base, '/tmp/evil')).toBe(false);
80
- });
81
- });
82
-
83
- // ---------------------------------------------------------------------------
84
- // resolveSafePath
85
- // ---------------------------------------------------------------------------
86
-
87
- describe('resolveSafePath', () => {
88
- const base = '/project/workspace';
89
-
90
- test('joins multiple segments safely', () => {
91
- const result = resolveSafePath(base, 'src', 'lib', 'utils.ts');
92
- expect(result.isOk()).toBe(true);
93
- expect(result.unwrap()).toBe(join(resolve(base), 'src', 'lib', 'utils.ts'));
94
- });
95
-
96
- test('rejects when segments escape the base', () => {
97
- const result = resolveSafePath(base, 'src', '../../../etc');
98
- expect(result.isErr()).toBe(true);
99
- const err = result as unknown as { error: PermissionError };
100
- expect(err.error).toBeInstanceOf(PermissionError);
101
- });
102
-
103
- test('handles single segment', () => {
104
- const result = resolveSafePath(base, 'file.txt');
105
- expect(result.isOk()).toBe(true);
106
- expect(result.unwrap()).toBe(resolve(base, 'file.txt'));
107
- });
108
-
109
- test('handles empty segments', () => {
110
- const result = resolveSafePath(base);
111
- expect(result.isOk()).toBe(true);
112
- expect(result.unwrap()).toBe(resolve(base));
113
- });
114
- });