@ontrails/http 1.0.0-beta.12
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-build.log +1 -0
- package/.turbo/turbo-lint.log +3 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +114 -0
- package/README.md +88 -0
- package/dist/blaze.d.ts +25 -0
- package/dist/blaze.d.ts.map +1 -0
- package/dist/blaze.js +69 -0
- package/dist/blaze.js.map +1 -0
- package/dist/build.d.ts +52 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +121 -0
- package/dist/build.js.map +1 -0
- package/dist/hono/blaze.d.ts +33 -0
- package/dist/hono/blaze.d.ts.map +1 -0
- package/dist/hono/blaze.js +232 -0
- package/dist/hono/blaze.js.map +1 -0
- package/dist/hono/index.d.ts +2 -0
- package/dist/hono/index.d.ts.map +1 -0
- package/dist/hono/index.js +2 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/__tests__/build.test.ts +478 -0
- package/src/build.ts +233 -0
- package/src/hono/__tests__/blaze.test.ts +525 -0
- package/src/hono/blaze.ts +352 -0
- package/src/hono/index.ts +1 -0
- package/src/index.ts +8 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
InternalError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
Result,
|
|
7
|
+
service,
|
|
8
|
+
trail,
|
|
9
|
+
topo,
|
|
10
|
+
} from '@ontrails/core';
|
|
11
|
+
import type { Layer } from '@ontrails/core';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
import { blaze } from '../blaze.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Test trails
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const echoTrail = trail('echo', {
|
|
21
|
+
description: 'Echo a message back',
|
|
22
|
+
input: z.object({ message: z.string() }),
|
|
23
|
+
intent: 'read',
|
|
24
|
+
output: z.object({ reply: z.string() }),
|
|
25
|
+
run: (input) => Result.ok({ reply: input.message }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const createTrail = trail('item.create', {
|
|
29
|
+
description: 'Create an item',
|
|
30
|
+
input: z.object({ name: z.string() }),
|
|
31
|
+
output: z.object({ id: z.string(), name: z.string() }),
|
|
32
|
+
run: (input) => Result.ok({ id: '123', name: input.name }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const deleteTrail = trail('item.delete', {
|
|
36
|
+
description: 'Delete an item',
|
|
37
|
+
input: z.object({ id: z.string() }),
|
|
38
|
+
intent: 'destroy',
|
|
39
|
+
run: (_input) => Result.ok({ deleted: true }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const notFoundTrail = trail('item.get', {
|
|
43
|
+
description: 'Get an item that does not exist',
|
|
44
|
+
input: z.object({ id: z.string() }),
|
|
45
|
+
intent: 'read',
|
|
46
|
+
run: (_input) => Result.err(new NotFoundError('Item not found')),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const internalTrail = trail('crash', {
|
|
50
|
+
description: 'Always fails with internal error',
|
|
51
|
+
input: z.object({}),
|
|
52
|
+
run: () => Result.err(new InternalError('Something broke')),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const dbService = service('db.main', {
|
|
56
|
+
create: () =>
|
|
57
|
+
Result.ok({
|
|
58
|
+
source: 'factory',
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Helpers
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/** Make a request against a Hono test app. */
|
|
67
|
+
const request = (
|
|
68
|
+
app: Awaited<ReturnType<typeof blaze>>,
|
|
69
|
+
method: string,
|
|
70
|
+
path: string,
|
|
71
|
+
body?: Record<string, unknown>,
|
|
72
|
+
headers?: Record<string, string>
|
|
73
|
+
): Promise<Response> => {
|
|
74
|
+
const init: RequestInit = { headers: { ...headers }, method };
|
|
75
|
+
if (body !== undefined) {
|
|
76
|
+
(init.headers as Record<string, string>)['Content-Type'] =
|
|
77
|
+
'application/json';
|
|
78
|
+
init.body = JSON.stringify(body);
|
|
79
|
+
}
|
|
80
|
+
return app.request(path, init);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Make a request with a raw string body. */
|
|
84
|
+
const requestRaw = (
|
|
85
|
+
app: Awaited<ReturnType<typeof blaze>>,
|
|
86
|
+
method: string,
|
|
87
|
+
path: string,
|
|
88
|
+
rawBody: string,
|
|
89
|
+
headers?: Record<string, string>
|
|
90
|
+
): Promise<Response> => {
|
|
91
|
+
const init: RequestInit = {
|
|
92
|
+
body: rawBody,
|
|
93
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
94
|
+
method,
|
|
95
|
+
};
|
|
96
|
+
return app.request(path, init);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Tests
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
describe('blaze (Hono adapter)', () => {
|
|
104
|
+
describe('validation', () => {
|
|
105
|
+
test('blaze throws on invalid topo', async () => {
|
|
106
|
+
const t = trail('broken', {
|
|
107
|
+
follow: ['nonexistent.trail'],
|
|
108
|
+
input: z.object({}),
|
|
109
|
+
output: z.object({}),
|
|
110
|
+
run: () => Result.ok({}),
|
|
111
|
+
});
|
|
112
|
+
const app = topo('test', { t });
|
|
113
|
+
await expect(blaze(app, { serve: false })).rejects.toThrow(/validation/i);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('blaze skips validation when validate: false', async () => {
|
|
117
|
+
const t = trail('broken', {
|
|
118
|
+
follow: ['nonexistent.trail'],
|
|
119
|
+
input: z.object({}),
|
|
120
|
+
output: z.object({}),
|
|
121
|
+
run: () => Result.ok({}),
|
|
122
|
+
});
|
|
123
|
+
const app = topo('test', { t });
|
|
124
|
+
await expect(
|
|
125
|
+
blaze(app, { serve: false, validate: false })
|
|
126
|
+
).resolves.toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('GET handler', () => {
|
|
131
|
+
test('returns 200 with data on success', async () => {
|
|
132
|
+
const app = topo('testapp', { echoTrail });
|
|
133
|
+
const hono = await blaze(app, { serve: false });
|
|
134
|
+
|
|
135
|
+
const res = await request(hono, 'GET', '/echo?message=hello');
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
|
|
138
|
+
const json = await res.json();
|
|
139
|
+
expect(json).toEqual({ data: { reply: 'hello' } });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('returns 400 on invalid input', async () => {
|
|
143
|
+
const app = topo('testapp', { echoTrail });
|
|
144
|
+
const hono = await blaze(app, { serve: false });
|
|
145
|
+
|
|
146
|
+
const res = await request(hono, 'GET', '/echo');
|
|
147
|
+
expect(res.status).toBe(400);
|
|
148
|
+
|
|
149
|
+
const json = await res.json();
|
|
150
|
+
expect(json.error).toBeDefined();
|
|
151
|
+
expect(json.error.category).toBe('validation');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('POST handler', () => {
|
|
156
|
+
test('returns 200 with data on success', async () => {
|
|
157
|
+
const app = topo('testapp', { createTrail });
|
|
158
|
+
const hono = await blaze(app, { serve: false });
|
|
159
|
+
|
|
160
|
+
const res = await request(hono, 'POST', '/item/create', {
|
|
161
|
+
name: 'Widget',
|
|
162
|
+
});
|
|
163
|
+
expect(res.status).toBe(200);
|
|
164
|
+
|
|
165
|
+
const json = await res.json();
|
|
166
|
+
expect(json).toEqual({ data: { id: '123', name: 'Widget' } });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('returns 400 on invalid input', async () => {
|
|
170
|
+
const app = topo('testapp', { createTrail });
|
|
171
|
+
const hono = await blaze(app, { serve: false });
|
|
172
|
+
|
|
173
|
+
const res = await request(hono, 'POST', '/item/create', {});
|
|
174
|
+
expect(res.status).toBe(400);
|
|
175
|
+
|
|
176
|
+
const json = await res.json();
|
|
177
|
+
expect(json.error.category).toBe('validation');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('POST with empty input schema succeeds without a body', async () => {
|
|
181
|
+
const emptyWriteTrail = trail('empty.write', {
|
|
182
|
+
input: z.object({}),
|
|
183
|
+
intent: 'write',
|
|
184
|
+
output: z.object({ ok: z.boolean() }),
|
|
185
|
+
run: () => Result.ok({ ok: true }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const app = topo('testapp', { emptyWriteTrail });
|
|
189
|
+
const hono = await blaze(app, { serve: false });
|
|
190
|
+
|
|
191
|
+
// No body, no Content-Type header — mirrors a client that obeys the
|
|
192
|
+
// OpenAPI spec (no requestBody declared for empty-input POST routes).
|
|
193
|
+
const res = await hono.request('/empty/write', { method: 'POST' });
|
|
194
|
+
expect(res.status).toBe(200);
|
|
195
|
+
|
|
196
|
+
const json = await res.json();
|
|
197
|
+
expect(json).toEqual({ data: { ok: true } });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('DELETE handler', () => {
|
|
202
|
+
test('returns 200 with data on success', async () => {
|
|
203
|
+
const app = topo('testapp', { deleteTrail });
|
|
204
|
+
const hono = await blaze(app, { serve: false });
|
|
205
|
+
|
|
206
|
+
const res = await request(hono, 'DELETE', '/item/delete', {
|
|
207
|
+
id: 'abc',
|
|
208
|
+
});
|
|
209
|
+
expect(res.status).toBe(200);
|
|
210
|
+
|
|
211
|
+
const json = await res.json();
|
|
212
|
+
expect(json).toEqual({ data: { deleted: true } });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('error mapping', () => {
|
|
217
|
+
test('NotFoundError maps to 404', async () => {
|
|
218
|
+
const app = topo('testapp', { notFoundTrail });
|
|
219
|
+
const hono = await blaze(app, { serve: false });
|
|
220
|
+
|
|
221
|
+
const res = await request(hono, 'GET', '/item/get?id=missing');
|
|
222
|
+
expect(res.status).toBe(404);
|
|
223
|
+
|
|
224
|
+
const json = await res.json();
|
|
225
|
+
expect(json.error.category).toBe('not_found');
|
|
226
|
+
expect(json.error.message).toBe('Item not found');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('InternalError maps to 500', async () => {
|
|
230
|
+
const app = topo('testapp', { internalTrail });
|
|
231
|
+
const hono = await blaze(app, { serve: false });
|
|
232
|
+
|
|
233
|
+
const res = await request(hono, 'POST', '/crash', {});
|
|
234
|
+
expect(res.status).toBe(500);
|
|
235
|
+
|
|
236
|
+
const json = await res.json();
|
|
237
|
+
expect(json.error.category).toBe('internal');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('thrown exceptions map to 500', async () => {
|
|
241
|
+
const throwTrail = trail('throw', {
|
|
242
|
+
input: z.object({}),
|
|
243
|
+
run: () => {
|
|
244
|
+
throw new Error('unexpected crash');
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const app = topo('testapp', { throwTrail });
|
|
249
|
+
const hono = await blaze(app, { serve: false });
|
|
250
|
+
|
|
251
|
+
const res = await request(hono, 'POST', '/throw', {});
|
|
252
|
+
expect(res.status).toBe(500);
|
|
253
|
+
|
|
254
|
+
const json = await res.json();
|
|
255
|
+
expect(json.error.message).toBe('unexpected crash');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('layers', () => {
|
|
260
|
+
test('layers compose around trail execution', async () => {
|
|
261
|
+
const calls: string[] = [];
|
|
262
|
+
|
|
263
|
+
const testLayer: Layer = {
|
|
264
|
+
name: 'test-layer',
|
|
265
|
+
wrap(_trail, impl) {
|
|
266
|
+
return async (input, ctx) => {
|
|
267
|
+
calls.push('before');
|
|
268
|
+
const result = await impl(input, ctx);
|
|
269
|
+
calls.push('after');
|
|
270
|
+
return result;
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const app = topo('testapp', { echoTrail });
|
|
276
|
+
const hono = await blaze(app, { layers: [testLayer], serve: false });
|
|
277
|
+
|
|
278
|
+
const res = await request(hono, 'GET', '/echo?message=hi');
|
|
279
|
+
expect(res.status).toBe(200);
|
|
280
|
+
expect(calls).toEqual(['before', 'after']);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('malformed JSON body', () => {
|
|
285
|
+
test('returns 400 for invalid JSON in POST body', async () => {
|
|
286
|
+
const app = topo('testapp', { createTrail });
|
|
287
|
+
const hono = await blaze(app, { serve: false });
|
|
288
|
+
|
|
289
|
+
const res = await requestRaw(hono, 'POST', '/item/create', '{invalid');
|
|
290
|
+
expect(res.status).toBe(400);
|
|
291
|
+
|
|
292
|
+
const json = await res.json();
|
|
293
|
+
expect(json.error.message).toBe('Invalid JSON in request body');
|
|
294
|
+
expect(json.error.code).toBe('ValidationError');
|
|
295
|
+
expect(json.error.category).toBe('validation');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('returns 400 for invalid JSON in DELETE body', async () => {
|
|
299
|
+
const app = topo('testapp', { deleteTrail });
|
|
300
|
+
const hono = await blaze(app, { serve: false });
|
|
301
|
+
|
|
302
|
+
const res = await requestRaw(hono, 'DELETE', '/item/delete', 'not-json');
|
|
303
|
+
expect(res.status).toBe(400);
|
|
304
|
+
|
|
305
|
+
const json = await res.json();
|
|
306
|
+
expect(json.error.message).toBe('Invalid JSON in request body');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('query param parsing', () => {
|
|
311
|
+
test('numeric-looking string is preserved as string', async () => {
|
|
312
|
+
const stringIdTrail = trail('lookup', {
|
|
313
|
+
input: z.object({ id: z.string() }),
|
|
314
|
+
intent: 'read',
|
|
315
|
+
run: (input) => Result.ok({ id: input.id }),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const app = topo('testapp', { stringIdTrail });
|
|
319
|
+
const hono = await blaze(app, { serve: false });
|
|
320
|
+
|
|
321
|
+
const res = await request(hono, 'GET', '/lookup?id=00123');
|
|
322
|
+
expect(res.status).toBe(200);
|
|
323
|
+
|
|
324
|
+
const json = await res.json();
|
|
325
|
+
expect(json.data.id).toBe('00123');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('repeated keys become arrays', async () => {
|
|
329
|
+
const tagsTrail = trail('tags', {
|
|
330
|
+
input: z.object({ tags: z.array(z.string()) }),
|
|
331
|
+
intent: 'read',
|
|
332
|
+
run: (input) => Result.ok({ tags: input.tags }),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const app = topo('testapp', { tagsTrail });
|
|
336
|
+
const hono = await blaze(app, { serve: false });
|
|
337
|
+
|
|
338
|
+
const res = await request(hono, 'GET', '/tags?tags=a&tags=b');
|
|
339
|
+
expect(res.status).toBe(200);
|
|
340
|
+
|
|
341
|
+
const json = await res.json();
|
|
342
|
+
expect(json.data.tags).toEqual(['a', 'b']);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('single value is wrapped in array when schema expects z.array()', async () => {
|
|
346
|
+
const tagsTrail = trail('tags.single', {
|
|
347
|
+
input: z.object({ tags: z.array(z.string()) }),
|
|
348
|
+
intent: 'read',
|
|
349
|
+
run: (input) => Result.ok({ tags: input.tags }),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const app = topo('testapp', { tagsTrail });
|
|
353
|
+
const hono = await blaze(app, { serve: false });
|
|
354
|
+
|
|
355
|
+
const res = await request(hono, 'GET', '/tags/single?tags=foo');
|
|
356
|
+
expect(res.status).toBe(200);
|
|
357
|
+
|
|
358
|
+
const json = await res.json();
|
|
359
|
+
expect(json.data.tags).toEqual(['foo']);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('single value stays scalar when schema expects a string', async () => {
|
|
363
|
+
const nameTrail = trail('name.check', {
|
|
364
|
+
input: z.object({ name: z.string() }),
|
|
365
|
+
intent: 'read',
|
|
366
|
+
run: (input) => Result.ok({ name: input.name }),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const app = topo('testapp', { nameTrail });
|
|
370
|
+
const hono = await blaze(app, { serve: false });
|
|
371
|
+
|
|
372
|
+
const res = await request(hono, 'GET', '/name/check?name=bar');
|
|
373
|
+
expect(res.status).toBe(200);
|
|
374
|
+
|
|
375
|
+
const json = await res.json();
|
|
376
|
+
expect(json.data.name).toBe('bar');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('optional array field with single value is wrapped in array', async () => {
|
|
380
|
+
const optArrayTrail = trail('opt.array', {
|
|
381
|
+
input: z.object({ ids: z.array(z.string()).optional() }),
|
|
382
|
+
intent: 'read',
|
|
383
|
+
run: (input) => Result.ok({ ids: input.ids }),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const app = topo('testapp', { optArrayTrail });
|
|
387
|
+
const hono = await blaze(app, { serve: false });
|
|
388
|
+
|
|
389
|
+
const res = await request(hono, 'GET', '/opt/array?ids=one');
|
|
390
|
+
expect(res.status).toBe(200);
|
|
391
|
+
|
|
392
|
+
const json = await res.json();
|
|
393
|
+
expect(json.data.ids).toEqual(['one']);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('AbortSignal', () => {
|
|
398
|
+
test('passes request AbortSignal to trail context', async () => {
|
|
399
|
+
let capturedSignal: AbortSignal | undefined;
|
|
400
|
+
|
|
401
|
+
const signalTrail = trail('signal.check', {
|
|
402
|
+
input: z.object({}),
|
|
403
|
+
intent: 'read',
|
|
404
|
+
output: z.object({ ok: z.boolean() }),
|
|
405
|
+
run: (_input, ctx) => {
|
|
406
|
+
capturedSignal = ctx.signal;
|
|
407
|
+
return Result.ok({ ok: true });
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const app = topo('testapp', { signalTrail });
|
|
412
|
+
const hono = await blaze(app, { serve: false });
|
|
413
|
+
|
|
414
|
+
const res = await request(hono, 'GET', '/signal/check');
|
|
415
|
+
expect(res.status).toBe(200);
|
|
416
|
+
expect(capturedSignal).toBeInstanceOf(AbortSignal);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('signal is aborted when request is cancelled', async () => {
|
|
420
|
+
let capturedSignal: AbortSignal | undefined;
|
|
421
|
+
|
|
422
|
+
const signalTrail = trail('signal.aborted', {
|
|
423
|
+
input: z.object({}),
|
|
424
|
+
intent: 'read',
|
|
425
|
+
output: z.object({ ok: z.boolean() }),
|
|
426
|
+
run: (_input, ctx) => {
|
|
427
|
+
capturedSignal = ctx.signal;
|
|
428
|
+
return Result.ok({ ok: true });
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const app = topo('testapp', { signalTrail });
|
|
433
|
+
const hono = await blaze(app, { serve: false });
|
|
434
|
+
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
controller.abort();
|
|
437
|
+
|
|
438
|
+
// Pass the pre-aborted signal directly in the Request.
|
|
439
|
+
// Hono's fetch propagates Request.signal into c.req.raw.signal.
|
|
440
|
+
const res = await hono.fetch(
|
|
441
|
+
new Request('http://localhost/signal/aborted', {
|
|
442
|
+
method: 'GET',
|
|
443
|
+
signal: controller.signal,
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
expect(res.status).toBe(200);
|
|
447
|
+
expect(capturedSignal?.aborted).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('context', () => {
|
|
452
|
+
test('X-Request-ID header is used for requestId', async () => {
|
|
453
|
+
let capturedRequestId: string | undefined;
|
|
454
|
+
|
|
455
|
+
const ctxTrail = trail('ctx.check', {
|
|
456
|
+
input: z.object({}),
|
|
457
|
+
intent: 'read',
|
|
458
|
+
run: (_input, ctx) => {
|
|
459
|
+
capturedRequestId = ctx.requestId;
|
|
460
|
+
return Result.ok({ ok: true });
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const app = topo('testapp', { ctxTrail });
|
|
465
|
+
const hono = await blaze(app, { serve: false });
|
|
466
|
+
|
|
467
|
+
const res = await request(hono, 'GET', '/ctx/check', undefined, {
|
|
468
|
+
'X-Request-ID': 'custom-req-123',
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(res.status).toBe(200);
|
|
472
|
+
expect(capturedRequestId).toBe('custom-req-123');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('custom createContext is used when provided', async () => {
|
|
476
|
+
let contextUsed = false;
|
|
477
|
+
|
|
478
|
+
const ctxTrail = trail('ctx.custom', {
|
|
479
|
+
input: z.object({}),
|
|
480
|
+
intent: 'read',
|
|
481
|
+
run: (_input, ctx) => {
|
|
482
|
+
contextUsed = ctx.extensions?.['custom'] === true;
|
|
483
|
+
return Result.ok({ ok: true });
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const app = topo('testapp', { ctxTrail });
|
|
488
|
+
const hono = await blaze(app, {
|
|
489
|
+
createContext: () => ({
|
|
490
|
+
extensions: { custom: true },
|
|
491
|
+
requestId: 'test-id',
|
|
492
|
+
signal: new AbortController().signal,
|
|
493
|
+
}),
|
|
494
|
+
serve: false,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const res = await request(hono, 'GET', '/ctx/custom');
|
|
498
|
+
expect(res.status).toBe(200);
|
|
499
|
+
expect(contextUsed).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('service overrides reach the trail through blaze()', async () => {
|
|
503
|
+
const serviceTrail = trail('service.check', {
|
|
504
|
+
input: z.object({}),
|
|
505
|
+
intent: 'read',
|
|
506
|
+
output: z.object({ source: z.string() }),
|
|
507
|
+
run: (_input, ctx) =>
|
|
508
|
+
Result.ok({ source: dbService.from(ctx).source as string }),
|
|
509
|
+
services: [dbService],
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const app = topo('testapp', { dbService, serviceTrail });
|
|
513
|
+
const hono = await blaze(app, {
|
|
514
|
+
serve: false,
|
|
515
|
+
services: { 'db.main': { source: 'override' } },
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const res = await request(hono, 'GET', '/service/check');
|
|
519
|
+
expect(res.status).toBe(200);
|
|
520
|
+
|
|
521
|
+
const json = await res.json();
|
|
522
|
+
expect(json.data.source).toBe('override');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
});
|