@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +8 -11
- package/dist/derive.d.ts +1 -1
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +4 -1
- package/dist/derive.js.map +1 -1
- package/dist/event.d.ts +2 -2
- package/dist/event.d.ts.map +1 -1
- package/dist/event.js +1 -1
- package/dist/event.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +15 -4
- package/dist/result.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +45 -7
- package/dist/serialization.js.map +1 -1
- package/dist/topo.d.ts +2 -4
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +8 -16
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +16 -10
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +4 -2
- package/dist/trail.js.map +1 -1
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +59 -9
- package/dist/validate-topo.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/derive.test.ts +44 -0
- package/src/__tests__/event.test.ts +5 -5
- package/src/__tests__/layer.test.ts +10 -22
- package/src/__tests__/serialization.test.ts +166 -1
- package/src/__tests__/topo.test.ts +78 -81
- package/src/__tests__/trail.test.ts +73 -35
- package/src/__tests__/validate-topo.test.ts +97 -20
- package/src/derive.ts +12 -2
- package/src/event.ts +3 -3
- package/src/index.ts +11 -5
- package/src/result.ts +18 -4
- package/src/serialization.ts +56 -11
- package/src/topo.ts +11 -23
- package/src/trail.ts +24 -13
- package/src/validate-topo.ts +70 -10
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/src/__tests__/hike.test.ts +0 -117
- package/src/hike.ts +0 -77
package/dist/trail.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trail.d.ts","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,EAAE,CAAC;IAChC,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAMD,yDAAyD;AACzD,MAAM,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC3C,qEAAqE;IACrE,QAAQ,CAAC,
|
|
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 {
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/dist/validate-topo.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural validation for a Topo graph.
|
|
3
3
|
*
|
|
4
|
-
* Checks
|
|
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
|
|
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;
|
|
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"}
|
package/dist/validate-topo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural validation for a Topo graph.
|
|
3
3
|
*
|
|
4
|
-
* Checks
|
|
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
|
|
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,
|
|
17
|
-
for (const followId of
|
|
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: `
|
|
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: '
|
|
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
|
|
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
|
|
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.
|
|
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":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-topo.js","sourceRoot":"","sources":["../src/validate-topo.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
58
|
-
expect(userAction.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
119
|
+
capturedDomain = t.metadata?.['domain'];
|
|
132
120
|
return impl;
|
|
133
121
|
},
|
|
134
122
|
};
|
|
135
123
|
|
|
136
|
-
composeLayers([inspectLayer], echoTrail, echoTrail.
|
|
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.
|
|
143
|
-
expect(wrapped).toBe(echoTrail.
|
|
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 {
|
|
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
|
});
|