@ontrails/core 1.0.0-beta.1 → 1.0.0-beta.10

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 (83) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +101 -0
  3. package/README.md +53 -11
  4. package/dist/derive.d.ts +1 -1
  5. package/dist/derive.d.ts.map +1 -1
  6. package/dist/derive.js +4 -1
  7. package/dist/derive.js.map +1 -1
  8. package/dist/dispatch.d.ts +27 -0
  9. package/dist/dispatch.d.ts.map +1 -0
  10. package/dist/dispatch.js +34 -0
  11. package/dist/dispatch.js.map +1 -0
  12. package/dist/event.d.ts +2 -2
  13. package/dist/event.d.ts.map +1 -1
  14. package/dist/event.js +1 -1
  15. package/dist/event.js.map +1 -1
  16. package/dist/execute.d.ts +30 -0
  17. package/dist/execute.d.ts.map +1 -0
  18. package/dist/execute.js +64 -0
  19. package/dist/execute.js.map +1 -0
  20. package/dist/index.d.ts +10 -6
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +7 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/patterns/status.d.ts +1 -1
  25. package/dist/result.d.ts.map +1 -1
  26. package/dist/result.js +15 -4
  27. package/dist/result.js.map +1 -1
  28. package/dist/serialization.d.ts.map +1 -1
  29. package/dist/serialization.js +45 -7
  30. package/dist/serialization.js.map +1 -1
  31. package/dist/topo.d.ts +4 -4
  32. package/dist/topo.d.ts.map +1 -1
  33. package/dist/topo.js +12 -16
  34. package/dist/topo.js.map +1 -1
  35. package/dist/trail.d.ts +16 -10
  36. package/dist/trail.d.ts.map +1 -1
  37. package/dist/trail.js +4 -2
  38. package/dist/trail.js.map +1 -1
  39. package/dist/type-utils.d.ts +24 -0
  40. package/dist/type-utils.d.ts.map +1 -0
  41. package/dist/type-utils.js +12 -0
  42. package/dist/type-utils.js.map +1 -0
  43. package/dist/types.d.ts +1 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/validate-topo.d.ts +2 -2
  46. package/dist/validate-topo.d.ts.map +1 -1
  47. package/dist/validate-topo.js +59 -9
  48. package/dist/validate-topo.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/__tests__/context.test.ts +4 -5
  51. package/src/__tests__/derive.test.ts +44 -0
  52. package/src/__tests__/dispatch.test.ts +154 -0
  53. package/src/__tests__/event.test.ts +5 -5
  54. package/src/__tests__/execute.test.ts +208 -0
  55. package/src/__tests__/layer.test.ts +11 -111
  56. package/src/__tests__/serialization.test.ts +166 -1
  57. package/src/__tests__/topo.test.ts +101 -79
  58. package/src/__tests__/trail.test.ts +73 -35
  59. package/src/__tests__/type-utils.test.ts +90 -0
  60. package/src/__tests__/validate-topo.test.ts +97 -20
  61. package/src/derive.ts +12 -2
  62. package/src/dispatch.ts +54 -0
  63. package/src/event.ts +3 -3
  64. package/src/execute.ts +96 -0
  65. package/src/index.ts +22 -18
  66. package/src/result.ts +18 -4
  67. package/src/serialization.ts +56 -11
  68. package/src/topo.ts +18 -23
  69. package/src/trail.ts +24 -13
  70. package/src/type-utils.ts +45 -0
  71. package/src/types.ts +1 -3
  72. package/src/validate-topo.ts +70 -10
  73. package/tsconfig.tsbuildinfo +1 -1
  74. package/dist/hike.d.ts +0 -36
  75. package/dist/hike.d.ts.map +0 -1
  76. package/dist/hike.js +0 -20
  77. package/dist/hike.js.map +0 -1
  78. package/src/__tests__/hike.test.ts +0 -117
  79. package/src/__tests__/job.test.ts +0 -98
  80. package/src/adapters.ts +0 -68
  81. package/src/health.ts +0 -23
  82. package/src/hike.ts +0 -77
  83. package/src/job.ts +0 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/core",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -16,6 +16,6 @@
16
16
  "clean": "rm -rf dist *.tsbuildinfo"
17
17
  },
18
18
  "peerDependencies": {
19
- "zod": "catalog:"
19
+ "zod": "^4.3.5"
20
20
  }
21
21
  }
@@ -48,14 +48,13 @@ describe('createTrailContext', () => {
48
48
  expect(ctx.workspaceRoot).toBe('/tmp');
49
49
  });
50
50
 
51
- test('TrailContext is extensible with custom fields', () => {
51
+ test('TrailContext supports extensions for custom fields', () => {
52
52
  const ctx: TrailContext = createTrailContext({
53
- customField: 42,
54
- nested: { key: 'value' },
53
+ extensions: { customField: 42, nested: { key: 'value' } },
55
54
  });
56
55
 
57
- expect(ctx['customField']).toBe(42);
58
- expect(ctx['nested']).toEqual({ key: 'value' });
56
+ expect(ctx.extensions?.['customField']).toBe(42);
57
+ expect(ctx.extensions?.['nested']).toEqual({ key: 'value' });
59
58
  });
60
59
 
61
60
  test('each call generates a unique requestId', () => {
@@ -77,6 +77,50 @@ describe('derive', () => {
77
77
  });
78
78
  });
79
79
 
80
+ describe('array types', () => {
81
+ test('z.array(z.string()) derives string[] field', () => {
82
+ const schema = z.object({ names: z.array(z.string()) });
83
+ const fields = deriveFields(schema);
84
+ expect(fields[0]).toMatchObject({
85
+ name: 'names',
86
+ options: undefined,
87
+ required: true,
88
+ type: 'string[]',
89
+ });
90
+ });
91
+
92
+ test('z.array(z.number()) derives number[] field', () => {
93
+ const schema = z.object({ scores: z.array(z.number()) });
94
+ const fields = deriveFields(schema);
95
+ expect(fields[0]).toMatchObject({
96
+ name: 'scores',
97
+ options: undefined,
98
+ required: true,
99
+ type: 'number[]',
100
+ });
101
+ });
102
+
103
+ test('optional z.array(z.string()) marks not required', () => {
104
+ const schema = z.object({ tags: z.array(z.string()).optional() });
105
+ const fields = deriveFields(schema);
106
+ expect(fields[0]).toMatchObject({
107
+ name: 'tags',
108
+ required: false,
109
+ type: 'string[]',
110
+ });
111
+ });
112
+
113
+ test('z.array(z.string()) with default sets default value', () => {
114
+ const schema = z.object({
115
+ items: z.array(z.string()).default(['a', 'b']),
116
+ });
117
+ const fields = deriveFields(schema);
118
+ expect(fields[0]?.required).toBe(false);
119
+ expect(fields[0]?.type).toBe('string[]');
120
+ expect(fields[0]?.default).toEqual(['a', 'b']);
121
+ });
122
+ });
123
+
80
124
  describe('modifiers', () => {
81
125
  test('.describe() sets label', () => {
82
126
  const schema = z.object({
@@ -0,0 +1,154 @@
1
+ /* oxlint-disable require-await -- trail implementations satisfy async interface without awaiting */
2
+ import { describe, test, expect } from 'bun:test';
3
+
4
+ import { z } from 'zod';
5
+
6
+ import { dispatch } from '../dispatch';
7
+ import { InternalError, NotFoundError, ValidationError } from '../errors';
8
+ import type { Layer } from '../layer';
9
+ import { Result } from '../result';
10
+ import { topo } from '../topo';
11
+ import { trail } from '../trail';
12
+ import type { TrailContext } from '../types';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Fixtures
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const echoTrail = trail('echo', {
19
+ input: z.object({ value: z.string() }),
20
+ output: z.object({ value: z.string() }),
21
+ run: (input) => Result.ok({ value: input.value }),
22
+ });
23
+
24
+ const throwingTrail = trail('throws', {
25
+ input: z.object({}),
26
+ run: () => {
27
+ throw new Error('kaboom');
28
+ },
29
+ });
30
+
31
+ const testTopo = topo('test', { echoTrail, throwingTrail });
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Tests
35
+ // ---------------------------------------------------------------------------
36
+
37
+ describe('dispatch', () => {
38
+ describe('happy path', () => {
39
+ test('dispatches by ID and returns Result.ok with expected value', async () => {
40
+ const result = await dispatch(testTopo, 'echo', { value: 'hello' });
41
+
42
+ expect(result.isOk()).toBe(true);
43
+ expect(result.unwrap()).toEqual({ value: 'hello' });
44
+ });
45
+ });
46
+
47
+ describe('not found', () => {
48
+ test('returns NotFoundError for unknown trail ID', async () => {
49
+ const result = await dispatch(testTopo, 'nonexistent', {});
50
+
51
+ expect(result.isErr()).toBe(true);
52
+ expect(result.error).toBeInstanceOf(NotFoundError);
53
+ expect(result.error.message).toContain('nonexistent');
54
+ expect(result.error.message).toContain('test');
55
+ });
56
+ });
57
+
58
+ describe('validation', () => {
59
+ test('returns ValidationError for invalid input', async () => {
60
+ const result = await dispatch(testTopo, 'echo', { value: 42 });
61
+
62
+ expect(result.isErr()).toBe(true);
63
+ expect(result.error).toBeInstanceOf(ValidationError);
64
+ });
65
+ });
66
+
67
+ describe('layers', () => {
68
+ test('layer composition works through dispatch', async () => {
69
+ const log: string[] = [];
70
+ const layer: Layer = {
71
+ name: 'test-layer',
72
+ wrap(_trail, impl) {
73
+ return async (input, ctx) => {
74
+ log.push('before');
75
+ const r = await impl(input, ctx);
76
+ log.push('after');
77
+ return r;
78
+ };
79
+ },
80
+ };
81
+
82
+ const result = await dispatch(
83
+ testTopo,
84
+ 'echo',
85
+ { value: 'x' },
86
+ { layers: [layer] }
87
+ );
88
+
89
+ expect(result.isOk()).toBe(true);
90
+ expect(log).toEqual(['before', 'after']);
91
+ });
92
+ });
93
+
94
+ describe('context', () => {
95
+ test('context overrides work through dispatch', async () => {
96
+ let capturedCtx: TrailContext | undefined;
97
+ const ctxTrail = trail('ctx-dispatch-test', {
98
+ input: z.object({}),
99
+ run: (_input, ctx) => {
100
+ capturedCtx = ctx;
101
+ return Result.ok(null);
102
+ },
103
+ });
104
+
105
+ const ctxTopo = topo('ctx-test', { ctxTrail });
106
+ await dispatch(
107
+ ctxTopo,
108
+ 'ctx-dispatch-test',
109
+ {},
110
+ { ctx: { requestId: 'override-id' } }
111
+ );
112
+
113
+ expect(capturedCtx?.requestId).toBe('override-id');
114
+ });
115
+
116
+ test('createContext factory works through dispatch', async () => {
117
+ let capturedCtx: TrailContext | undefined;
118
+ const ctxTrail = trail('factory-dispatch-test', {
119
+ input: z.object({}),
120
+ run: (_input, ctx) => {
121
+ capturedCtx = ctx;
122
+ return Result.ok(null);
123
+ },
124
+ });
125
+
126
+ const ctxTopo = topo('factory-test', { ctxTrail });
127
+ const customCtx: TrailContext = {
128
+ cwd: '/custom',
129
+ requestId: 'factory-id',
130
+ signal: new AbortController().signal,
131
+ };
132
+
133
+ await dispatch(
134
+ ctxTopo,
135
+ 'factory-dispatch-test',
136
+ {},
137
+ { createContext: () => customCtx }
138
+ );
139
+
140
+ expect(capturedCtx?.requestId).toBe('factory-id');
141
+ expect(capturedCtx?.cwd).toBe('/custom');
142
+ });
143
+ });
144
+
145
+ describe('error handling', () => {
146
+ test('never throws — exceptions become InternalError', async () => {
147
+ const result = await dispatch(testTopo, 'throws', {});
148
+
149
+ expect(result.isErr()).toBe(true);
150
+ expect(result.error).toBeInstanceOf(InternalError);
151
+ expect(result.error.message).toBe('kaboom');
152
+ });
153
+ });
154
+ });
@@ -12,7 +12,7 @@ const payloadSchema = z.object({
12
12
  const userAction = event('user.action', {
13
13
  description: 'A user performed an action',
14
14
  from: ['auth.login', 'auth.signup'],
15
- markers: { domain: 'auth', priority: 1 },
15
+ metadata: { domain: 'auth', priority: 1 },
16
16
  payload: payloadSchema,
17
17
  });
18
18
 
@@ -45,7 +45,7 @@ describe('event() basics', () => {
45
45
  });
46
46
  });
47
47
 
48
- describe('event() from and markers', () => {
48
+ describe('event() from and metadata', () => {
49
49
  test('preserves from', () => {
50
50
  expect(userAction.from).toEqual(['auth.login', 'auth.signup']);
51
51
  });
@@ -54,8 +54,8 @@ describe('event() from and markers', () => {
54
54
  expect(Object.isFrozen(userAction.from)).toBe(true);
55
55
  });
56
56
 
57
- test('preserves markers', () => {
58
- expect(userAction.markers).toEqual({ domain: 'auth', priority: 1 });
57
+ test('preserves metadata', () => {
58
+ expect(userAction.metadata).toEqual({ domain: 'auth', priority: 1 });
59
59
  });
60
60
 
61
61
  test('optional fields default to undefined', () => {
@@ -64,7 +64,7 @@ describe('event() from and markers', () => {
64
64
  });
65
65
  expect(minimal.description).toBeUndefined();
66
66
  expect(minimal.from).toBeUndefined();
67
- expect(minimal.markers).toBeUndefined();
67
+ expect(minimal.metadata).toBeUndefined();
68
68
  });
69
69
  });
70
70
 
@@ -0,0 +1,208 @@
1
+ /* oxlint-disable require-await -- trail implementations satisfy async interface without awaiting */
2
+ import { describe, test, expect } from 'bun:test';
3
+
4
+ import { z } from 'zod';
5
+
6
+ import { InternalError, ValidationError } from '../errors';
7
+ import { executeTrail } from '../execute';
8
+ import { createTrailContext } from '../context';
9
+ import type { Layer } from '../layer';
10
+ import { Result } from '../result';
11
+ import { trail } from '../trail';
12
+ import type { TrailContext } from '../types';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Fixtures
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const echoTrail = trail('echo', {
19
+ input: z.object({ value: z.string() }),
20
+ output: z.object({ value: z.string() }),
21
+ run: (input) => Result.ok({ value: input.value }),
22
+ });
23
+
24
+ const failingTrail = trail('fails', {
25
+ input: z.object({}),
26
+ run: () => Result.err(new ValidationError('bad input')),
27
+ });
28
+
29
+ const throwingTrail = trail('throws', {
30
+ input: z.object({}),
31
+ run: () => {
32
+ throw new Error('kaboom');
33
+ },
34
+ });
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Tests
38
+ // ---------------------------------------------------------------------------
39
+
40
+ describe('executeTrail', () => {
41
+ describe('happy path', () => {
42
+ test('validates input and executes trail', async () => {
43
+ const result = await executeTrail(echoTrail, { value: 'hello' });
44
+
45
+ expect(result.isOk()).toBe(true);
46
+ expect(result.unwrap()).toEqual({ value: 'hello' });
47
+ });
48
+ });
49
+
50
+ describe('validation', () => {
51
+ test('returns validation error for invalid input', async () => {
52
+ const result = await executeTrail(echoTrail, { value: 42 });
53
+
54
+ expect(result.isErr()).toBe(true);
55
+ expect(result.error).toBeInstanceOf(ValidationError);
56
+ });
57
+ });
58
+
59
+ describe('layers', () => {
60
+ test('composes layers around execution', async () => {
61
+ const log: string[] = [];
62
+ const layer: Layer = {
63
+ name: 'test-layer',
64
+ wrap(_trail, impl) {
65
+ return async (input, ctx) => {
66
+ log.push('before');
67
+ const r = await impl(input, ctx);
68
+ log.push('after');
69
+ return r;
70
+ };
71
+ },
72
+ };
73
+
74
+ const result = await executeTrail(
75
+ echoTrail,
76
+ { value: 'x' },
77
+ { layers: [layer] }
78
+ );
79
+
80
+ expect(result.isOk()).toBe(true);
81
+ expect(log).toEqual(['before', 'after']);
82
+ });
83
+ });
84
+
85
+ describe('context', () => {
86
+ test('accepts context overrides', async () => {
87
+ let capturedCtx: TrailContext | undefined;
88
+ const ctxTrail = trail('ctx-test', {
89
+ input: z.object({}),
90
+ run: (_input, ctx) => {
91
+ capturedCtx = ctx;
92
+ return Result.ok(null);
93
+ },
94
+ });
95
+
96
+ await executeTrail(ctxTrail, {}, { ctx: { requestId: 'override-id' } });
97
+
98
+ expect(capturedCtx?.requestId).toBe('override-id');
99
+ });
100
+
101
+ test('accepts signal override', async () => {
102
+ let capturedSignal: AbortSignal | undefined;
103
+ const sigTrail = trail('sig-test', {
104
+ input: z.object({}),
105
+ run: (_input, ctx) => {
106
+ capturedSignal = ctx.signal;
107
+ return Result.ok(null);
108
+ },
109
+ });
110
+
111
+ const signal = AbortSignal.timeout(9999);
112
+ await executeTrail(sigTrail, {}, { signal });
113
+
114
+ expect(capturedSignal).toBe(signal);
115
+ });
116
+
117
+ test('accepts context factory', async () => {
118
+ let capturedCtx: TrailContext | undefined;
119
+ const ctxTrail = trail('factory-test', {
120
+ input: z.object({}),
121
+ run: (_input, ctx) => {
122
+ capturedCtx = ctx;
123
+ return Result.ok(null);
124
+ },
125
+ });
126
+
127
+ const customCtx: TrailContext = {
128
+ cwd: '/custom',
129
+ requestId: 'factory-id',
130
+ signal: new AbortController().signal,
131
+ };
132
+
133
+ await executeTrail(ctxTrail, {}, { createContext: () => customCtx });
134
+
135
+ expect(capturedCtx?.requestId).toBe('factory-id');
136
+ expect(capturedCtx?.cwd).toBe('/custom');
137
+ });
138
+
139
+ test('context factory + ctx overrides merge correctly', async () => {
140
+ let capturedCtx: TrailContext | undefined;
141
+ const ctxTrail = trail('merge-test', {
142
+ input: z.object({}),
143
+ run: (_input, ctx) => {
144
+ capturedCtx = ctx;
145
+ return Result.ok(null);
146
+ },
147
+ });
148
+
149
+ const baseCtx: TrailContext = {
150
+ cwd: '/factory',
151
+ requestId: 'factory-id',
152
+ signal: new AbortController().signal,
153
+ };
154
+
155
+ await executeTrail(
156
+ ctxTrail,
157
+ {},
158
+ {
159
+ createContext: () => baseCtx,
160
+ ctx: { requestId: 'overridden-id' },
161
+ }
162
+ );
163
+
164
+ expect(capturedCtx?.requestId).toBe('overridden-id');
165
+ expect(capturedCtx?.cwd).toBe('/factory');
166
+ });
167
+
168
+ test('deep-merges extensions from factory and overrides', async () => {
169
+ let captured: TrailContext | undefined;
170
+ const t = trail('ext.test', {
171
+ input: z.object({}),
172
+ output: z.object({}),
173
+ run: (_input, ctx) => {
174
+ captured = ctx;
175
+ return Result.ok({});
176
+ },
177
+ });
178
+ await executeTrail(
179
+ t,
180
+ {},
181
+ {
182
+ createContext: () =>
183
+ createTrailContext({ extensions: { store: 'db' } }),
184
+ ctx: { extensions: { userId: '123' } },
185
+ }
186
+ );
187
+ expect(captured?.extensions).toEqual({ store: 'db', userId: '123' });
188
+ });
189
+ });
190
+
191
+ describe('error handling', () => {
192
+ test('propagates Result.err from run function', async () => {
193
+ const result = await executeTrail(failingTrail, {});
194
+
195
+ expect(result.isErr()).toBe(true);
196
+ expect(result.error).toBeInstanceOf(ValidationError);
197
+ expect(result.error.message).toBe('bad input');
198
+ });
199
+
200
+ test('catches thrown exceptions and returns InternalError', async () => {
201
+ const result = await executeTrail(throwingTrail, {});
202
+
203
+ expect(result.isErr()).toBe(true);
204
+ expect(result.error).toBeInstanceOf(InternalError);
205
+ expect(result.error.message).toBe('kaboom');
206
+ });
207
+ });
208
+ });
@@ -1,17 +1,8 @@
1
- /* oxlint-disable require-await -- adapter mocks and layer wrappers satisfy async interfaces without awaiting */
1
+ /* oxlint-disable require-await -- layer wrappers satisfy async interfaces without awaiting */
2
2
  import { describe, test, expect } from 'bun:test';
3
3
 
4
4
  import { z } from 'zod';
5
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
6
  import { composeLayers } from '../layer';
16
7
  import type { Layer } from '../layer';
17
8
  import { Result } from '../result';
@@ -24,10 +15,10 @@ const stubCtx: TrailContext = {
24
15
  };
25
16
 
26
17
  const echoTrail = trail('echo', {
27
- implementation: (input) => Result.ok({ value: input.value }),
28
18
  input: z.object({ value: z.string() }),
29
- markers: { domain: 'test' },
19
+ metadata: { domain: 'test' },
30
20
  output: z.object({ value: z.string() }),
21
+ run: (input) => Result.ok({ value: input.value }),
31
22
  });
32
23
 
33
24
  // ---------------------------------------------------------------------------
@@ -49,11 +40,7 @@ describe('Layer', () => {
49
40
  },
50
41
  };
51
42
 
52
- const wrapped = composeLayers(
53
- [prefixLayer],
54
- echoTrail,
55
- echoTrail.implementation
56
- );
43
+ const wrapped = composeLayers([prefixLayer], echoTrail, echoTrail.run);
57
44
  const result = await wrapped({ value: 'hello' }, stubCtx);
58
45
 
59
46
  expect(result.isOk()).toBe(true);
@@ -87,11 +74,7 @@ describe('Layer', () => {
87
74
  },
88
75
  };
89
76
 
90
- const wrapped = composeLayers(
91
- [outer, inner],
92
- echoTrail,
93
- echoTrail.implementation
94
- );
77
+ const wrapped = composeLayers([outer, inner], echoTrail, echoTrail.run);
95
78
  await wrapped({ value: 'x' }, stubCtx);
96
79
 
97
80
  expect(log).toEqual([
@@ -110,11 +93,7 @@ describe('Layer', () => {
110
93
  },
111
94
  };
112
95
 
113
- const wrapped = composeLayers(
114
- [shortCircuit],
115
- echoTrail,
116
- echoTrail.implementation
117
- );
96
+ const wrapped = composeLayers([shortCircuit], echoTrail, echoTrail.run);
118
97
  const result = await wrapped({ value: 'hello' }, stubCtx);
119
98
 
120
99
  expect(result.isErr()).toBe(true);
@@ -122,103 +101,24 @@ describe('Layer', () => {
122
101
  expect(err.error.message).toBe('blocked');
123
102
  });
124
103
 
125
- test('layer can inspect trail markers', () => {
104
+ test('layer can inspect trail metadata', () => {
126
105
  let capturedDomain: unknown;
127
106
 
128
107
  const inspectLayer: Layer = {
129
108
  name: 'inspect',
130
109
  wrap(t, impl) {
131
- capturedDomain = t.markers?.['domain'];
110
+ capturedDomain = t.metadata?.['domain'];
132
111
  return impl;
133
112
  },
134
113
  };
135
114
 
136
- composeLayers([inspectLayer], echoTrail, echoTrail.implementation);
115
+ composeLayers([inspectLayer], echoTrail, echoTrail.run);
137
116
 
138
117
  expect(capturedDomain).toBe('test');
139
118
  });
140
119
 
141
120
  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);
121
+ const wrapped = composeLayers([], echoTrail, echoTrail.run);
122
+ expect(wrapped).toBe(echoTrail.run);
223
123
  });
224
124
  });