@blokjs/shared 0.2.1 → 0.4.0
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/__tests__/unit/BlokError.test.ts +294 -0
- package/__tests__/unit/NodeBase.test.ts +4 -1
- package/__tests__/unit/utils/Mapper.test.ts +299 -31
- package/__tests__/unit/utils/MapperResolutionError.test.ts +64 -0
- package/dist/BlokError.d.ts +196 -0
- package/dist/BlokError.js +328 -0
- package/dist/BlokError.js.map +1 -0
- package/dist/NodeBase.d.ts +96 -2
- package/dist/NodeBase.js +120 -2
- package/dist/NodeBase.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types/Context.d.ts +91 -0
- package/dist/types/StateContext.d.ts +21 -0
- package/dist/types/StateContext.js +2 -0
- package/dist/types/StateContext.js.map +1 -0
- package/dist/types/VarsContext.d.ts +15 -2
- package/dist/utils/Mapper.d.ts +111 -2
- package/dist/utils/Mapper.js +256 -23
- package/dist/utils/Mapper.js.map +1 -1
- package/dist/utils/MapperResolutionError.d.ts +84 -0
- package/dist/utils/MapperResolutionError.js +61 -0
- package/dist/utils/MapperResolutionError.js.map +1 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -5
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import BlokError, {
|
|
3
|
+
DEFAULT_HTTP_STATUS,
|
|
4
|
+
DEFAULT_RETRYABLE,
|
|
5
|
+
ErrorCategory,
|
|
6
|
+
ErrorSeverity,
|
|
7
|
+
type NodeErrorPayload,
|
|
8
|
+
} from "../../src/BlokError";
|
|
9
|
+
import GlobalError from "../../src/GlobalError";
|
|
10
|
+
|
|
11
|
+
describe("BlokError", () => {
|
|
12
|
+
describe("factory methods", () => {
|
|
13
|
+
const factories = [
|
|
14
|
+
{ name: "validation", method: BlokError.validation, expected: ErrorCategory.VALIDATION },
|
|
15
|
+
{ name: "configuration", method: BlokError.configuration, expected: ErrorCategory.CONFIGURATION },
|
|
16
|
+
{ name: "dependency", method: BlokError.dependency, expected: ErrorCategory.DEPENDENCY },
|
|
17
|
+
{ name: "timeout", method: BlokError.timeout, expected: ErrorCategory.TIMEOUT },
|
|
18
|
+
{ name: "permission", method: BlokError.permission, expected: ErrorCategory.PERMISSION },
|
|
19
|
+
{ name: "rateLimit", method: BlokError.rateLimit, expected: ErrorCategory.RATE_LIMIT },
|
|
20
|
+
{ name: "notFound", method: BlokError.notFound, expected: ErrorCategory.NOT_FOUND },
|
|
21
|
+
{ name: "conflict", method: BlokError.conflict, expected: ErrorCategory.CONFLICT },
|
|
22
|
+
{ name: "cancelled", method: BlokError.cancelled, expected: ErrorCategory.CANCELLED },
|
|
23
|
+
{ name: "internal", method: BlokError.internal, expected: ErrorCategory.INTERNAL },
|
|
24
|
+
{ name: "protocol", method: BlokError.protocol, expected: ErrorCategory.PROTOCOL },
|
|
25
|
+
{ name: "data", method: BlokError.data, expected: ErrorCategory.DATA },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const { name, method, expected } of factories) {
|
|
29
|
+
it(`${name}() creates an error with the right category`, () => {
|
|
30
|
+
const err = method({ code: "TEST", message: "test" });
|
|
31
|
+
expect(err.category).toBe(expected);
|
|
32
|
+
expect(err).toBeInstanceOf(BlokError);
|
|
33
|
+
expect(err).toBeInstanceOf(GlobalError);
|
|
34
|
+
expect(err).toBeInstanceOf(Error);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it("each factory applies the default http_status for its category", () => {
|
|
39
|
+
for (const { method, expected } of factories) {
|
|
40
|
+
const err = method({ code: "TEST", message: "test" });
|
|
41
|
+
expect(err.httpStatus).toBe(DEFAULT_HTTP_STATUS[expected]);
|
|
42
|
+
expect(err.context.code).toBe(DEFAULT_HTTP_STATUS[expected]);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("each factory applies the default retryable hint for its category", () => {
|
|
47
|
+
for (const { method, expected } of factories) {
|
|
48
|
+
const err = method({ code: "TEST", message: "test" });
|
|
49
|
+
expect(err.retryable).toBe(DEFAULT_RETRYABLE[expected]);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("defaults severity to ERROR", () => {
|
|
54
|
+
const err = BlokError.dependency({ code: "X", message: "x" });
|
|
55
|
+
expect(err.severity).toBe(ErrorSeverity.ERROR);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("constructor options", () => {
|
|
60
|
+
it("preserves all human-readable fields", () => {
|
|
61
|
+
const err = BlokError.dependency({
|
|
62
|
+
code: "POSTGRES_CONNECT_TIMEOUT",
|
|
63
|
+
message: "Could not connect to Postgres within 5s",
|
|
64
|
+
description: "Tried host=localhost port=5432; timeout=5000ms",
|
|
65
|
+
remediation: "Check DATABASE_URL env var and network reachability",
|
|
66
|
+
docUrl: "https://blok.dev/errors/POSTGRES_CONNECT_TIMEOUT",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(err.errorCode).toBe("POSTGRES_CONNECT_TIMEOUT");
|
|
70
|
+
expect(err.message).toBe("Could not connect to Postgres within 5s");
|
|
71
|
+
expect(err.description).toContain("port=5432");
|
|
72
|
+
expect(err.remediation).toContain("DATABASE_URL");
|
|
73
|
+
expect(err.docUrl).toContain("POSTGRES_CONNECT_TIMEOUT");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("allows overriding httpStatus", () => {
|
|
77
|
+
const err = BlokError.dependency({ code: "X", message: "x", httpStatus: 503 });
|
|
78
|
+
expect(err.httpStatus).toBe(503);
|
|
79
|
+
expect(err.context.code).toBe(503);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("allows overriding retryable + retryAfterMs", () => {
|
|
83
|
+
const err = BlokError.dependency({
|
|
84
|
+
code: "X",
|
|
85
|
+
message: "x",
|
|
86
|
+
retryable: false,
|
|
87
|
+
retryAfterMs: 0,
|
|
88
|
+
});
|
|
89
|
+
expect(err.retryable).toBe(false);
|
|
90
|
+
expect(err.retryAfterMs).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("allows overriding severity", () => {
|
|
94
|
+
const err = BlokError.timeout({
|
|
95
|
+
code: "X",
|
|
96
|
+
message: "x",
|
|
97
|
+
severity: ErrorSeverity.FATAL,
|
|
98
|
+
});
|
|
99
|
+
expect(err.severity).toBe(ErrorSeverity.FATAL);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("captures structured details", () => {
|
|
103
|
+
const err = BlokError.validation({
|
|
104
|
+
code: "VALIDATION_FAILED",
|
|
105
|
+
message: "schema mismatch",
|
|
106
|
+
details: { issues: [{ path: "email", message: "invalid" }] },
|
|
107
|
+
});
|
|
108
|
+
expect(err.details).toEqual({ issues: [{ path: "email", message: "invalid" }] });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("captures contextSnapshot", () => {
|
|
112
|
+
const snapshot = { inputs: { foo: "bar" }, varsKeys: ["a", "b"] };
|
|
113
|
+
const err = BlokError.internal({
|
|
114
|
+
code: "OOPS",
|
|
115
|
+
message: "oops",
|
|
116
|
+
contextSnapshot: snapshot,
|
|
117
|
+
});
|
|
118
|
+
expect(err.contextSnapshot).toEqual(snapshot);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("cause chain", () => {
|
|
123
|
+
it("flattens a single Error cause", () => {
|
|
124
|
+
const inner = new Error("connect ECONNREFUSED");
|
|
125
|
+
const outer = BlokError.dependency({
|
|
126
|
+
code: "DB_DOWN",
|
|
127
|
+
message: "Database unreachable",
|
|
128
|
+
cause: inner,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(outer.causes).toHaveLength(1);
|
|
132
|
+
expect(outer.causes[0].message).toBe("connect ECONNREFUSED");
|
|
133
|
+
expect(outer.causes[0].category).toBe(ErrorCategory.INTERNAL);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("flattens a BlokError cause without re-wrapping its own causes", () => {
|
|
137
|
+
const root = BlokError.timeout({ code: "DNS_TIMEOUT", message: "dns timed out" });
|
|
138
|
+
const middle = BlokError.dependency({ code: "DB_DOWN", message: "db unreachable", cause: root });
|
|
139
|
+
const top = BlokError.internal({ code: "REQUEST_FAILED", message: "request failed", cause: middle });
|
|
140
|
+
|
|
141
|
+
expect(top.causes).toHaveLength(2);
|
|
142
|
+
expect(top.causes[0].code).toBe("DB_DOWN");
|
|
143
|
+
expect(top.causes[1].code).toBe("DNS_TIMEOUT");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns an empty causes array when no cause is provided", () => {
|
|
147
|
+
const err = BlokError.internal({ code: "X", message: "x" });
|
|
148
|
+
expect(err.causes).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("does not infinite-loop on circular causes", () => {
|
|
152
|
+
const a = new Error("a");
|
|
153
|
+
const b = new Error("b");
|
|
154
|
+
(a as Error & { cause?: Error }).cause = b;
|
|
155
|
+
(b as Error & { cause?: Error }).cause = a;
|
|
156
|
+
|
|
157
|
+
expect(() => BlokError.internal({ code: "CYCLE", message: "circular", cause: a })).not.toThrow();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("GlobalError compatibility", () => {
|
|
162
|
+
it("populates context.code with httpStatus", () => {
|
|
163
|
+
const err = BlokError.notFound({ code: "USER_NOT_FOUND", message: "no user" });
|
|
164
|
+
expect(err.context.code).toBe(404);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("populates context.name with node when provided", () => {
|
|
168
|
+
const err = BlokError.dependency({ code: "X", message: "x", node: "fetch-user" });
|
|
169
|
+
expect(err.context.name).toBe("fetch-user");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("populates context.json with the full payload", () => {
|
|
173
|
+
const err = BlokError.dependency({
|
|
174
|
+
code: "DB_DOWN",
|
|
175
|
+
message: "db down",
|
|
176
|
+
description: "host unreachable",
|
|
177
|
+
remediation: "check network",
|
|
178
|
+
});
|
|
179
|
+
const payload = err.context.json as unknown as NodeErrorPayload;
|
|
180
|
+
expect(payload.code).toBe("DB_DOWN");
|
|
181
|
+
expect(payload.category).toBe(ErrorCategory.DEPENDENCY);
|
|
182
|
+
expect(payload.description).toBe("host unreachable");
|
|
183
|
+
expect(payload.remediation).toBe("check network");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("toJSON / fromJSON round-trip", () => {
|
|
188
|
+
it("preserves every field", () => {
|
|
189
|
+
const inner = BlokError.timeout({
|
|
190
|
+
code: "INNER_TIMEOUT",
|
|
191
|
+
message: "inner timed out",
|
|
192
|
+
});
|
|
193
|
+
const original = BlokError.dependency({
|
|
194
|
+
code: "DB_DOWN",
|
|
195
|
+
message: "Database unreachable",
|
|
196
|
+
description: "Tried host=db port=5432",
|
|
197
|
+
remediation: "Check DATABASE_URL",
|
|
198
|
+
docUrl: "https://blok.dev/errors/DB_DOWN",
|
|
199
|
+
cause: inner,
|
|
200
|
+
retryable: true,
|
|
201
|
+
retryAfterMs: 5000,
|
|
202
|
+
details: { sqlState: "08001" },
|
|
203
|
+
contextSnapshot: { inputs: { query: "SELECT 1" } },
|
|
204
|
+
httpStatus: 503,
|
|
205
|
+
severity: ErrorSeverity.WARN,
|
|
206
|
+
node: "store-tutorial",
|
|
207
|
+
sdk: "blok-python3",
|
|
208
|
+
sdkVersion: "1.0.0",
|
|
209
|
+
runtimeKind: "runtime.python3",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const json = original.toJSON();
|
|
213
|
+
const restored = BlokError.fromJSON(json);
|
|
214
|
+
|
|
215
|
+
expect(restored.errorCode).toBe(original.errorCode);
|
|
216
|
+
expect(restored.category).toBe(original.category);
|
|
217
|
+
expect(restored.severity).toBe(original.severity);
|
|
218
|
+
expect(restored.message).toBe(original.message);
|
|
219
|
+
expect(restored.description).toBe(original.description);
|
|
220
|
+
expect(restored.remediation).toBe(original.remediation);
|
|
221
|
+
expect(restored.docUrl).toBe(original.docUrl);
|
|
222
|
+
expect(restored.retryable).toBe(original.retryable);
|
|
223
|
+
expect(restored.retryAfterMs).toBe(original.retryAfterMs);
|
|
224
|
+
expect(restored.details).toEqual(original.details);
|
|
225
|
+
expect(restored.contextSnapshot).toEqual(original.contextSnapshot);
|
|
226
|
+
expect(restored.httpStatus).toBe(original.httpStatus);
|
|
227
|
+
expect(restored.nodeName).toBe(original.nodeName);
|
|
228
|
+
expect(restored.sdk).toBe(original.sdk);
|
|
229
|
+
expect(restored.sdkVersion).toBe(original.sdkVersion);
|
|
230
|
+
expect(restored.runtimeKind).toBe(original.runtimeKind);
|
|
231
|
+
expect(restored.causes).toEqual(original.causes);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("toJSON produces an ISO timestamp for `at`", () => {
|
|
235
|
+
const err = BlokError.internal({ code: "X", message: "x" });
|
|
236
|
+
const json = err.toJSON();
|
|
237
|
+
expect(json.at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
238
|
+
expect(new Date(json.at).getTime()).not.toBeNaN();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("fromUnknown", () => {
|
|
243
|
+
it("passes BlokError through unchanged", () => {
|
|
244
|
+
const original = BlokError.dependency({ code: "X", message: "x" });
|
|
245
|
+
const wrapped = BlokError.fromUnknown(original);
|
|
246
|
+
expect(wrapped).toBe(original);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("wraps a plain Error as INTERNAL with UNCAUGHT_<NAME> code", () => {
|
|
250
|
+
const err = new TypeError("bad type");
|
|
251
|
+
const wrapped = BlokError.fromUnknown(err);
|
|
252
|
+
expect(wrapped.category).toBe(ErrorCategory.INTERNAL);
|
|
253
|
+
expect(wrapped.errorCode).toBe("UNCAUGHT_TYPEERROR");
|
|
254
|
+
expect(wrapped.message).toBe("bad type");
|
|
255
|
+
expect(wrapped.causes).toHaveLength(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("wraps a string as INTERNAL/UNCAUGHT_ERROR", () => {
|
|
259
|
+
const wrapped = BlokError.fromUnknown("oops");
|
|
260
|
+
expect(wrapped.category).toBe(ErrorCategory.INTERNAL);
|
|
261
|
+
expect(wrapped.errorCode).toBe("UNCAUGHT_ERROR");
|
|
262
|
+
expect(wrapped.message).toBe("oops");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("wraps a non-Error value as INTERNAL/UNCAUGHT_ERROR with stringified message", () => {
|
|
266
|
+
const wrapped = BlokError.fromUnknown({ weird: true });
|
|
267
|
+
expect(wrapped.category).toBe(ErrorCategory.INTERNAL);
|
|
268
|
+
expect(wrapped.errorCode).toBe("UNCAUGHT_ERROR");
|
|
269
|
+
expect(wrapped.message).toContain("weird");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("preserves a pre-existing GlobalError code and json", () => {
|
|
273
|
+
const ge = new GlobalError("legacy");
|
|
274
|
+
ge.setCode(403);
|
|
275
|
+
ge.setJson({ origin: "auth" });
|
|
276
|
+
const wrapped = BlokError.fromUnknown(ge);
|
|
277
|
+
expect(wrapped.httpStatus).toBe(403);
|
|
278
|
+
expect(wrapped.details).toEqual({ origin: "auth" });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("uses ctx overrides for node/sdk/sdkVersion/runtimeKind", () => {
|
|
282
|
+
const wrapped = BlokError.fromUnknown(new Error("x"), {
|
|
283
|
+
node: "step-1",
|
|
284
|
+
sdk: "blok-go",
|
|
285
|
+
sdkVersion: "1.0.0",
|
|
286
|
+
runtimeKind: "runtime.go",
|
|
287
|
+
});
|
|
288
|
+
expect(wrapped.nodeName).toBe("step-1");
|
|
289
|
+
expect(wrapped.sdk).toBe("blok-go");
|
|
290
|
+
expect(wrapped.sdkVersion).toBe("1.0.0");
|
|
291
|
+
expect(wrapped.runtimeKind).toBe("runtime.go");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -46,7 +46,10 @@ describe("NodeBase", () => {
|
|
|
46
46
|
expect(n.name).toBe("");
|
|
47
47
|
expect(n.active).toBe(true);
|
|
48
48
|
expect(n.stop).toBe(false);
|
|
49
|
-
|
|
49
|
+
// set_var defaults to undefined (NOT false). false short-circuits
|
|
50
|
+
// PersistenceHelper.applyStepOutput and silently disables v2's
|
|
51
|
+
// default-store rule for every step that didn't explicitly set it.
|
|
52
|
+
expect(n.set_var).toBeUndefined();
|
|
50
53
|
expect(n.contentType).toBe("");
|
|
51
54
|
});
|
|
52
55
|
});
|