@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.4

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 (56) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +8 -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/event.d.ts +2 -2
  9. package/dist/event.d.ts.map +1 -1
  10. package/dist/event.js +1 -1
  11. package/dist/event.js.map +1 -1
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/result.d.ts.map +1 -1
  17. package/dist/result.js +15 -4
  18. package/dist/result.js.map +1 -1
  19. package/dist/serialization.d.ts.map +1 -1
  20. package/dist/serialization.js +45 -7
  21. package/dist/serialization.js.map +1 -1
  22. package/dist/topo.d.ts +2 -4
  23. package/dist/topo.d.ts.map +1 -1
  24. package/dist/topo.js +8 -16
  25. package/dist/topo.js.map +1 -1
  26. package/dist/trail.d.ts +16 -10
  27. package/dist/trail.d.ts.map +1 -1
  28. package/dist/trail.js +4 -2
  29. package/dist/trail.js.map +1 -1
  30. package/dist/validate-topo.d.ts +2 -2
  31. package/dist/validate-topo.d.ts.map +1 -1
  32. package/dist/validate-topo.js +59 -9
  33. package/dist/validate-topo.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/derive.test.ts +44 -0
  36. package/src/__tests__/event.test.ts +5 -5
  37. package/src/__tests__/layer.test.ts +10 -22
  38. package/src/__tests__/serialization.test.ts +166 -1
  39. package/src/__tests__/topo.test.ts +78 -81
  40. package/src/__tests__/trail.test.ts +73 -35
  41. package/src/__tests__/validate-topo.test.ts +97 -20
  42. package/src/derive.ts +12 -2
  43. package/src/event.ts +3 -3
  44. package/src/index.ts +11 -5
  45. package/src/result.ts +18 -4
  46. package/src/serialization.ts +56 -11
  47. package/src/topo.ts +11 -23
  48. package/src/trail.ts +24 -13
  49. package/src/validate-topo.ts +70 -10
  50. package/tsconfig.tsbuildinfo +1 -1
  51. package/dist/hike.d.ts +0 -36
  52. package/dist/hike.d.ts.map +0 -1
  53. package/dist/hike.js +0 -20
  54. package/dist/hike.js.map +0 -1
  55. package/src/__tests__/hike.test.ts +0 -117
  56. package/src/hike.ts +0 -77
@@ -1 +1 @@
1
- {"version":3,"file":"trail.d.ts","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trail.d.ts","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"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"}
package/dist/trail.js CHANGED
@@ -5,12 +5,14 @@ export function trail(idOrSpec, maybeSpec) {
5
5
  if (!resolved.spec) {
6
6
  throw new TypeError('trail() requires a spec when an id is provided');
7
7
  }
8
- const { implementation, ...spec } = resolved.spec;
8
+ const { run, follow: rawFollow, intent: rawIntent, ...spec } = resolved.spec;
9
9
  return Object.freeze({
10
10
  ...spec,
11
+ follow: Object.freeze([...(rawFollow ?? [])]),
11
12
  id: resolved.id,
12
- implementation: async (input, ctx) => await implementation(input, ctx),
13
+ intent: rawIntent ?? 'write',
13
14
  kind: 'trail',
15
+ run: async (input, ctx) => await run(input, ctx),
14
16
  });
15
17
  }
16
18
  //# sourceMappingURL=trail.js.map
package/dist/trail.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"trail.js","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"AAqGA,MAAM,UAAU,KAAK,CACnB,QAA8D,EAC9D,SAA2B;IAE3B,MAAM,QAAQ,GACZ,OAAO,QAAQ,KAAK,QAAQ;QAC1B,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE;QACnC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAE1C,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CAAC,gDAAgD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC;IAElD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,IAAI;QACP,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,cAAc,EAAE,KAAK,EAAE,KAAQ,EAAE,GAAiB,EAAE,EAAE,CACpD,MAAM,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC;QAClC,IAAI,EAAE,OAAgB;KACvB,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"trail.js","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"AA+GA,MAAM,UAAU,KAAK,CACnB,QAA8D,EAC9D,SAA2B;IAE3B,MAAM,QAAQ,GACZ,OAAO,QAAQ,KAAK,QAAQ;QAC1B,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE;QACnC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAE1C,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CAAC,gDAAgD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC;IAE7E,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,IAAI;QACP,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7C,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,MAAM,EAAE,SAAS,IAAI,OAAO;QAC5B,IAAI,EAAE,OAAgB;QACtB,GAAG,EAAE,KAAK,EAAE,KAAQ,EAAE,GAAiB,EAAE,EAAE,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC;KAClE,CAAC,CAAC;AACL,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Structural validation for a Topo graph.
3
3
  *
4
- * Checks hike follows references, example input validity, event origin
4
+ * Checks trail follow references, example input validity, event origin
5
5
  * references, and output schema completeness. Returns a Result with all
6
6
  * issues collected into a single ValidationError.
7
7
  */
@@ -16,7 +16,7 @@ export interface TopoIssue {
16
16
  /**
17
17
  * Validate the structural integrity of a Topo graph.
18
18
  *
19
- * Checks follows references, example inputs, event origins, and output
19
+ * Checks follow references, example inputs, event origins, and output
20
20
  * schema presence. Returns `Result.ok()` when no issues are found, or
21
21
  * `Result.err(ValidationError)` with all issues in the error context.
22
22
  */
@@ -1 +1 @@
1
- {"version":3,"file":"validate-topo.d.ts","sourceRoot":"","sources":["../src/validate-topo.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAQtC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAoGD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,IAAI,KAAG,MAAM,CAAC,IAAI,EAAE,eAAe,CAmBrE,CAAC"}
1
+ {"version":3,"file":"validate-topo.d.ts","sourceRoot":"","sources":["../src/validate-topo.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAQtC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAiKD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,IAAI,KAAG,MAAM,CAAC,IAAI,EAAE,eAAe,CAmBrE,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Structural validation for a Topo graph.
3
3
  *
4
- * Checks hike follows references, example input validity, event origin
4
+ * Checks trail follow references, example input validity, event origin
5
5
  * references, and output schema completeness. Returns a Result with all
6
6
  * issues collected into a single ValidationError.
7
7
  */
@@ -11,13 +11,62 @@ import { validateInput } from './validation.js';
11
11
  // ---------------------------------------------------------------------------
12
12
  // Validators
13
13
  // ---------------------------------------------------------------------------
14
- const checkFollows = (hikes, topo) => {
14
+ const WHITE = 0;
15
+ const GRAY = 1;
16
+ const BLACK = 2;
17
+ /** Build an adjacency list and initial color map from trails with follow. */
18
+ const buildFollowGraph = (trails) => {
19
+ const graph = new Map();
20
+ for (const [id, t] of trails) {
21
+ if (t.follow.length > 0) {
22
+ graph.set(id, t.follow);
23
+ }
24
+ }
25
+ const color = new Map();
26
+ for (const id of graph.keys()) {
27
+ color.set(id, WHITE);
28
+ }
29
+ return { color, graph };
30
+ };
31
+ /** Detect multi-node cycles in the trail follow graph via DFS. */
32
+ const detectFollowCycles = (trails) => {
33
+ const issues = [];
34
+ const { color, graph } = buildFollowGraph(trails);
35
+ const dfs = (node, path) => {
36
+ color.set(node, GRAY);
37
+ for (const next of graph.get(node) ?? []) {
38
+ if (!graph.has(next)) {
39
+ continue;
40
+ }
41
+ const c = color.get(next) ?? WHITE;
42
+ if (c === GRAY) {
43
+ const cycle = [...path.slice(path.indexOf(next)), next];
44
+ issues.push({
45
+ message: `Cycle detected: ${cycle.join(' → ')}`,
46
+ rule: 'follow-cycle',
47
+ trailId: next,
48
+ });
49
+ }
50
+ else if (c === WHITE) {
51
+ dfs(next, [...path, next]);
52
+ }
53
+ }
54
+ color.set(node, BLACK);
55
+ };
56
+ for (const id of graph.keys()) {
57
+ if (color.get(id) === WHITE) {
58
+ dfs(id, [id]);
59
+ }
60
+ }
61
+ return issues;
62
+ };
63
+ const checkFollows = (trails, topo) => {
15
64
  const issues = [];
16
- for (const [id, hike] of hikes) {
17
- for (const followId of hike.follows) {
65
+ for (const [id, trail] of trails) {
66
+ for (const followId of trail.follow) {
18
67
  if (followId === id) {
19
68
  issues.push({
20
- message: `Hike follows itself`,
69
+ message: `Trail follows itself`,
21
70
  rule: 'no-self-follow',
22
71
  trailId: id,
23
72
  });
@@ -25,18 +74,19 @@ const checkFollows = (hikes, topo) => {
25
74
  else if (!topo.has(followId)) {
26
75
  issues.push({
27
76
  message: `Follows "${followId}" which is not in the topo`,
28
- rule: 'follows-exist',
77
+ rule: 'follow-exists',
29
78
  trailId: id,
30
79
  });
31
80
  }
32
81
  }
33
82
  }
83
+ issues.push(...detectFollowCycles(trails));
34
84
  return issues;
35
85
  };
36
86
  const checkOneExample = (id, example, inputSchema, hasOutput) => {
37
87
  const issues = [];
38
88
  const result = validateInput(inputSchema, example.input);
39
- if (result.isErr() && example.error === undefined) {
89
+ if (result.isErr() && example.error !== 'ValidationError') {
40
90
  issues.push({
41
91
  message: `Example "${example.name}" input does not parse against schema`,
42
92
  rule: 'example-input-valid',
@@ -88,13 +138,13 @@ const checkEventOrigins = (events, topo) => {
88
138
  /**
89
139
  * Validate the structural integrity of a Topo graph.
90
140
  *
91
- * Checks follows references, example inputs, event origins, and output
141
+ * Checks follow references, example inputs, event origins, and output
92
142
  * schema presence. Returns `Result.ok()` when no issues are found, or
93
143
  * `Result.err(ValidationError)` with all issues in the error context.
94
144
  */
95
145
  export const validateTopo = (topo) => {
96
146
  const issues = [
97
- ...checkFollows(topo.hikes, topo),
147
+ ...checkFollows(topo.trails, topo),
98
148
  ...checkExamples(topo.trails),
99
149
  ...checkEventOrigins(topo.events, topo),
100
150
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"validate-topo.js","sourceRoot":"","sources":["../src/validate-topo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validate-topo.js","sourceRoot":"","sources":["../src/validate-topo.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/core",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -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({
@@ -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
 
@@ -24,10 +24,10 @@ const stubCtx: TrailContext = {
24
24
  };
25
25
 
26
26
  const echoTrail = trail('echo', {
27
- implementation: (input) => Result.ok({ value: input.value }),
28
27
  input: z.object({ value: z.string() }),
29
- markers: { domain: 'test' },
28
+ metadata: { domain: 'test' },
30
29
  output: z.object({ value: z.string() }),
30
+ run: (input) => Result.ok({ value: input.value }),
31
31
  });
32
32
 
33
33
  // ---------------------------------------------------------------------------
@@ -49,11 +49,7 @@ describe('Layer', () => {
49
49
  },
50
50
  };
51
51
 
52
- const wrapped = composeLayers(
53
- [prefixLayer],
54
- echoTrail,
55
- echoTrail.implementation
56
- );
52
+ const wrapped = composeLayers([prefixLayer], echoTrail, echoTrail.run);
57
53
  const result = await wrapped({ value: 'hello' }, stubCtx);
58
54
 
59
55
  expect(result.isOk()).toBe(true);
@@ -87,11 +83,7 @@ describe('Layer', () => {
87
83
  },
88
84
  };
89
85
 
90
- const wrapped = composeLayers(
91
- [outer, inner],
92
- echoTrail,
93
- echoTrail.implementation
94
- );
86
+ const wrapped = composeLayers([outer, inner], echoTrail, echoTrail.run);
95
87
  await wrapped({ value: 'x' }, stubCtx);
96
88
 
97
89
  expect(log).toEqual([
@@ -110,11 +102,7 @@ describe('Layer', () => {
110
102
  },
111
103
  };
112
104
 
113
- const wrapped = composeLayers(
114
- [shortCircuit],
115
- echoTrail,
116
- echoTrail.implementation
117
- );
105
+ const wrapped = composeLayers([shortCircuit], echoTrail, echoTrail.run);
118
106
  const result = await wrapped({ value: 'hello' }, stubCtx);
119
107
 
120
108
  expect(result.isErr()).toBe(true);
@@ -122,25 +110,25 @@ describe('Layer', () => {
122
110
  expect(err.error.message).toBe('blocked');
123
111
  });
124
112
 
125
- test('layer can inspect trail markers', () => {
113
+ test('layer can inspect trail metadata', () => {
126
114
  let capturedDomain: unknown;
127
115
 
128
116
  const inspectLayer: Layer = {
129
117
  name: 'inspect',
130
118
  wrap(t, impl) {
131
- capturedDomain = t.markers?.['domain'];
119
+ capturedDomain = t.metadata?.['domain'];
132
120
  return impl;
133
121
  },
134
122
  };
135
123
 
136
- composeLayers([inspectLayer], echoTrail, echoTrail.implementation);
124
+ composeLayers([inspectLayer], echoTrail, echoTrail.run);
137
125
 
138
126
  expect(capturedDomain).toBe('test');
139
127
  });
140
128
 
141
129
  test('empty layers array returns implementation unchanged', () => {
142
- const wrapped = composeLayers([], echoTrail, echoTrail.implementation);
143
- expect(wrapped).toBe(echoTrail.implementation);
130
+ const wrapped = composeLayers([], echoTrail, echoTrail.run);
131
+ expect(wrapped).toBe(echoTrail.run);
144
132
  });
145
133
  });
146
134
 
@@ -2,13 +2,24 @@ import { describe, test, expect } from 'bun:test';
2
2
 
3
3
  import {
4
4
  ValidationError,
5
+ AmbiguousError,
6
+ AssertionError,
5
7
  NetworkError,
6
8
  RateLimitError,
7
9
  InternalError,
8
10
  TimeoutError,
9
11
  NotFoundError,
12
+ AlreadyExistsError,
13
+ ConflictError,
14
+ PermissionError,
15
+ AuthError,
16
+ CancelledError,
10
17
  } from '../errors.js';
11
- import { serializeError, deserializeError } from '../serialization.js';
18
+ import {
19
+ serializeError,
20
+ deserializeError,
21
+ safeStringify,
22
+ } from '../serialization.js';
12
23
  import { Result } from '../result.js';
13
24
  import type { SerializedError } from '../serialization.js';
14
25
 
@@ -156,6 +167,66 @@ describe('deserializeError', () => {
156
167
  expect(err.category).toBe(category);
157
168
  }
158
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
+ });
159
230
  });
160
231
 
161
232
  // ---------------------------------------------------------------------------
@@ -233,4 +304,98 @@ describe('Result.toJson (safeStringify)', () => {
233
304
  expect(parsed['a']).toBe(1);
234
305
  expect(parsed['self']).toBe('[Circular]');
235
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
+ });
236
401
  });