@mandujs/core 0.54.12 → 0.54.14

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 (80) hide show
  1. package/package.json +9 -1
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +49 -1
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +8 -0
  11. package/src/agent/verify.ts +100 -2
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +36 -16
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/__tests__/fast-refresh.test.ts +24 -17
  17. package/src/bundler/analyzer.ts +15 -15
  18. package/src/bundler/build.test.ts +136 -48
  19. package/src/bundler/build.ts +193 -122
  20. package/src/bundler/css.ts +42 -42
  21. package/src/bundler/manifest-schema.ts +21 -21
  22. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  23. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  24. package/src/bundler/types.ts +31 -31
  25. package/src/client/island.ts +79 -79
  26. package/src/config/validate.ts +1 -1
  27. package/src/contract/schema.ts +7 -0
  28. package/src/deploy/inference/context.ts +82 -82
  29. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  30. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  31. package/src/error/formatter.ts +10 -1
  32. package/src/experimental/index.ts +10 -0
  33. package/src/filling/context.ts +17 -17
  34. package/src/filling/filling.ts +22 -1
  35. package/src/filling/index.ts +15 -1
  36. package/src/generator/generate.ts +30 -30
  37. package/src/generator/index.ts +3 -3
  38. package/src/generator/templates.ts +210 -210
  39. package/src/guard/check.ts +9 -9
  40. package/src/guard/config-guard.ts +13 -13
  41. package/src/guard/fs-routes-policy.ts +51 -51
  42. package/src/guard/index.ts +11 -11
  43. package/src/index.ts +0 -10
  44. package/src/internal/index.ts +25 -0
  45. package/src/kitchen/api/file-api.ts +11 -11
  46. package/src/report/index.ts +1 -1
  47. package/src/resource/__tests__/generator.test.ts +6 -6
  48. package/src/resource/__tests__/schema.test.ts +14 -14
  49. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  50. package/src/resource/ddl/emit.ts +146 -146
  51. package/src/resource/generator-schema.ts +11 -11
  52. package/src/resource/generators/slot.ts +72 -72
  53. package/src/resource/schema.ts +21 -21
  54. package/src/router/client-entry.test.ts +84 -41
  55. package/src/router/client-entry.ts +156 -89
  56. package/src/router/fs-routes.test.ts +90 -0
  57. package/src/router/fs-routes.ts +37 -29
  58. package/src/router/fs-scanner.ts +81 -33
  59. package/src/router/fs-types.ts +8 -5
  60. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  61. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  62. package/src/runtime/__tests__/page-render-response.test.ts +164 -103
  63. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  64. package/src/runtime/devtools-adapter.ts +68 -68
  65. package/src/runtime/escape.ts +34 -34
  66. package/src/runtime/image-feature.ts +15 -0
  67. package/src/runtime/observability-lifecycle.ts +290 -290
  68. package/src/runtime/page-render-response.ts +208 -106
  69. package/src/runtime/rate-limit.ts +231 -0
  70. package/src/runtime/request-middleware.ts +31 -31
  71. package/src/runtime/router.test.ts +4 -4
  72. package/src/runtime/router.ts +10 -12
  73. package/src/runtime/scheduler-lifecycle.ts +64 -0
  74. package/src/runtime/server.ts +148 -353
  75. package/src/runtime/ssr.ts +59 -59
  76. package/src/runtime/static-files.ts +289 -289
  77. package/src/runtime/streaming-ssr.ts +22 -22
  78. package/src/spec/schema.ts +4 -3
  79. package/src/watcher/__tests__/watcher.test.ts +59 -59
  80. package/src/watcher/watcher.ts +61 -61
@@ -28,16 +28,55 @@ import {
28
28
  sortRoutesByPriority,
29
29
  getPatternShape,
30
30
  } from "./fs-patterns";
31
- import { mark, measure } from "../perf";
32
- import { METADATA_ROUTES } from "../routes/types";
33
- import {
34
- hasUseClientDirective,
35
- resolveRouteLevelClientEntryPath,
36
- } from "./client-entry";
37
-
38
- // ═══════════════════════════════════════════════════════════════════════════
39
- // Scanner Class
40
- // ═══════════════════════════════════════════════════════════════════════════
31
+ import { mark, measure } from "../perf";
32
+ import { METADATA_ROUTES } from "../routes/types";
33
+ import type { HydrationConfig } from "../spec/schema";
34
+ import {
35
+ hasUseClientDirective,
36
+ resolveRouteLevelClientEntry,
37
+ } from "./client-entry";
38
+
39
+ const HYDRATION_STRATEGIES = new Set(["none", "island", "full", "progressive"]);
40
+ const HYDRATION_PRIORITIES = new Set(["immediate", "visible", "idle", "interaction"]);
41
+
42
+ function parsePageHydrationConfig(source: string): HydrationConfig | undefined {
43
+ const stringMatch = source.match(
44
+ /export\s+const\s+hydration\s*(?::[^=]+)?=\s*["'](none|island|full|progressive)["']/m,
45
+ );
46
+ if (stringMatch?.[1]) {
47
+ return {
48
+ strategy: stringMatch[1] as HydrationConfig["strategy"],
49
+ priority: "visible",
50
+ preload: false,
51
+ };
52
+ }
53
+
54
+ const objectMatch = source.match(
55
+ /export\s+const\s+hydration\s*(?::[^=]+)?=\s*\{([\s\S]*?)\}\s*;?/m,
56
+ );
57
+ const body = objectMatch?.[1];
58
+ if (!body) return undefined;
59
+
60
+ const strategyMatch = body.match(/\bstrategy\s*:\s*["']([^"']+)["']/);
61
+ const strategy = strategyMatch?.[1];
62
+ if (!strategy || !HYDRATION_STRATEGIES.has(strategy)) return undefined;
63
+
64
+ const priorityMatch = body.match(/\bpriority\s*:\s*["']([^"']+)["']/);
65
+ const priority = priorityMatch?.[1];
66
+ const preloadMatch = body.match(/\bpreload\s*:\s*(true|false)\b/);
67
+
68
+ return {
69
+ strategy: strategy as HydrationConfig["strategy"],
70
+ priority: HYDRATION_PRIORITIES.has(priority ?? "")
71
+ ? (priority as HydrationConfig["priority"])
72
+ : "visible",
73
+ preload: preloadMatch?.[1] === "true",
74
+ };
75
+ }
76
+
77
+ // ═══════════════════════════════════════════════════════════════════════════
78
+ // Scanner Class
79
+ // ═══════════════════════════════════════════════════════════════════════════
41
80
 
42
81
  /**
43
82
  * FS Routes 스캐너
@@ -364,17 +403,22 @@ export class FSScanner {
364
403
  const dirPath = this.getDirPath(file.relativePath);
365
404
  const islands = islandMap.get(dirPath);
366
405
 
367
- // clientModule 결정: island 파일 또는 "use client"가 있는 page 자체
368
- let clientModule: string | undefined;
369
- let pageFileContent: string | null = null;
370
-
371
- if (file.type === "page") {
372
- try {
373
- pageFileContent = await Bun.file(file.absolutePath).text();
374
- } catch {
375
- pageFileContent = null;
376
- }
377
- }
406
+ // clientModule 결정: island 파일 또는 "use client"가 있는 page 자체
407
+ let clientModule: string | undefined;
408
+ let clientExportName: string | undefined;
409
+ let hydration: HydrationConfig | undefined;
410
+ let pageFileContent: string | null = null;
411
+
412
+ if (file.type === "page") {
413
+ try {
414
+ pageFileContent = await Bun.file(file.absolutePath).text();
415
+ } catch {
416
+ pageFileContent = null;
417
+ }
418
+ if (pageFileContent) {
419
+ hydration = parsePageHydrationConfig(pageFileContent);
420
+ }
421
+ }
378
422
 
379
423
  if (islands?.[0]) {
380
424
  // 우선순위: 명시적 island 파일
@@ -395,12 +439,14 @@ export class FSScanner {
395
439
  } else if (file.type === "page" && pageFileContent) {
396
440
  // page 파일 자체에서 "use client" 확인
397
441
  const hasUseClient = hasUseClientDirective(pageFileContent);
398
- if (hasUseClient) {
399
- clientModule = modulePath;
400
- } else {
401
- clientModule = await resolveRouteLevelClientEntryPath(rootDir, modulePath, pageFileContent) ?? undefined;
402
- }
403
- }
442
+ if (hasUseClient) {
443
+ clientModule = modulePath;
444
+ } else {
445
+ const routeClientEntry = await resolveRouteLevelClientEntry(rootDir, modulePath, pageFileContent);
446
+ clientModule = routeClientEntry?.modulePath;
447
+ clientExportName = routeClientEntry?.exportName;
448
+ }
449
+ }
404
450
 
405
451
  // 로딩/에러/404 모듈 찾기 — nearest-ancestor resolution.
406
452
  // Phase 18.β: `not-found.tsx` joins `loading.tsx`/`error.tsx` in
@@ -415,12 +461,14 @@ export class FSScanner {
415
461
  segments: file.segments,
416
462
  pattern,
417
463
  kind: file.type === "page" ? "page" : "api",
418
- module: modulePath,
419
- componentModule: file.type === "page" ? modulePath : undefined,
420
- clientModule,
421
- layoutChain,
422
- loadingModule,
423
- errorModule,
464
+ module: modulePath,
465
+ componentModule: file.type === "page" ? modulePath : undefined,
466
+ clientModule,
467
+ clientExportName,
468
+ hydration,
469
+ layoutChain,
470
+ loadingModule,
471
+ errorModule,
424
472
  notFoundModule,
425
473
  sourceFile: file.absolutePath,
426
474
  };
@@ -117,11 +117,14 @@ export interface FSRouteConfig {
117
117
  /** API 핸들러 모듈 경로 */
118
118
  module: string;
119
119
 
120
- /** Island 컴포넌트 모듈 경로 */
121
- clientModule?: string;
122
-
123
- /** 적용할 레이아웃 체인 */
124
- layoutChain: string[];
120
+ /** Island 컴포넌트 모듈 경로 */
121
+ clientModule?: string;
122
+
123
+ /** Named export used by the route-level client module, if not default. */
124
+ clientExportName?: string;
125
+
126
+ /** 적용할 레이아웃 체인 */
127
+ layoutChain: string[];
125
128
 
126
129
  /** 로딩 UI 모듈 경로 */
127
130
  loadingModule?: string;
@@ -1,68 +1,68 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import type { RoutesManifest } from "../../spec/schema";
3
- import {
4
- createRuntimeDevtoolsAdapter,
5
- shouldRecordRuntimeRequest,
6
- type RuntimeDevtoolsAdapter,
7
- } from "../devtools-adapter";
8
-
9
- const manifest: RoutesManifest = {
10
- version: 1,
11
- routes: [],
12
- };
13
-
14
- describe("runtime devtools adapter", () => {
15
- let adapter: RuntimeDevtoolsAdapter | null = null;
16
-
17
- afterEach(() => {
18
- adapter?.stop();
19
- adapter = null;
20
- });
21
-
22
- it("stays disabled outside dev mode", async () => {
23
- adapter = createRuntimeDevtoolsAdapter({
24
- isDev: false,
25
- rootDir: process.cwd(),
26
- manifest,
27
- guardConfig: null,
28
- });
29
-
30
- expect(adapter.kitchen).toBeNull();
31
- expect(adapter.dashboardPath).toBeNull();
32
- const response = await adapter.handleRequest(
33
- new Request("http://localhost:3000/__kitchen"),
34
- "/__kitchen"
35
- );
36
- expect(response).toBeNull();
37
- });
38
-
39
- it("dispatches Kitchen requests in dev mode", async () => {
40
- adapter = createRuntimeDevtoolsAdapter({
41
- isDev: true,
42
- rootDir: process.cwd(),
43
- manifest,
44
- guardConfig: null,
45
- });
46
-
47
- expect(adapter.kitchen).not.toBeNull();
48
- expect(adapter.dashboardPath).toBe("/__kitchen");
49
- await expect(
50
- adapter.handleRequest(new Request("http://localhost:3000/api/ping"), "/api/ping")
51
- ).resolves.toBeNull();
52
-
53
- const response = await adapter.handleRequest(
54
- new Request("http://localhost:3000/__kitchen"),
55
- "/__kitchen"
56
- );
57
- expect(response?.status).toBe(200);
58
- expect(await response?.text()).toContain("Mandu Kitchen");
59
- });
60
-
61
- it("keeps framework-internal request paths out of the Kitchen request log", () => {
62
- expect(shouldRecordRuntimeRequest("/api/ping")).toBe(true);
63
- expect(shouldRecordRuntimeRequest("/_mandu/heap")).toBe(true);
64
- expect(shouldRecordRuntimeRequest("/.mandu/client/runtime.js")).toBe(false);
65
- expect(shouldRecordRuntimeRequest("/__kitchen/api/events")).toBe(false);
66
- expect(shouldRecordRuntimeRequest("/__mandu/events/recent")).toBe(false);
67
- });
68
- });
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import type { RoutesManifest } from "../../spec/schema";
3
+ import {
4
+ createRuntimeDevtoolsAdapter,
5
+ shouldRecordRuntimeRequest,
6
+ type RuntimeDevtoolsAdapter,
7
+ } from "../devtools-adapter";
8
+
9
+ const manifest: RoutesManifest = {
10
+ version: 1,
11
+ routes: [],
12
+ };
13
+
14
+ describe("runtime devtools adapter", () => {
15
+ let adapter: RuntimeDevtoolsAdapter | null = null;
16
+
17
+ afterEach(() => {
18
+ adapter?.stop();
19
+ adapter = null;
20
+ });
21
+
22
+ it("stays disabled outside dev mode", async () => {
23
+ adapter = createRuntimeDevtoolsAdapter({
24
+ isDev: false,
25
+ rootDir: process.cwd(),
26
+ manifest,
27
+ guardConfig: null,
28
+ });
29
+
30
+ expect(adapter.kitchen).toBeNull();
31
+ expect(adapter.dashboardPath).toBeNull();
32
+ const response = await adapter.handleRequest(
33
+ new Request("http://localhost:3000/__kitchen"),
34
+ "/__kitchen"
35
+ );
36
+ expect(response).toBeNull();
37
+ });
38
+
39
+ it("dispatches Kitchen requests in dev mode", async () => {
40
+ adapter = createRuntimeDevtoolsAdapter({
41
+ isDev: true,
42
+ rootDir: process.cwd(),
43
+ manifest,
44
+ guardConfig: null,
45
+ });
46
+
47
+ expect(adapter.kitchen).not.toBeNull();
48
+ expect(adapter.dashboardPath).toBe("/__kitchen");
49
+ await expect(
50
+ adapter.handleRequest(new Request("http://localhost:3000/api/ping"), "/api/ping")
51
+ ).resolves.toBeNull();
52
+
53
+ const response = await adapter.handleRequest(
54
+ new Request("http://localhost:3000/__kitchen"),
55
+ "/__kitchen"
56
+ );
57
+ expect(response?.status).toBe(200);
58
+ expect(await response?.text()).toContain("Mandu Kitchen");
59
+ });
60
+
61
+ it("keeps framework-internal request paths out of the Kitchen request log", () => {
62
+ expect(shouldRecordRuntimeRequest("/api/ping")).toBe(true);
63
+ expect(shouldRecordRuntimeRequest("/_mandu/heap")).toBe(true);
64
+ expect(shouldRecordRuntimeRequest("/.mandu/client/runtime.js")).toBe(false);
65
+ expect(shouldRecordRuntimeRequest("/__kitchen/api/events")).toBe(false);
66
+ expect(shouldRecordRuntimeRequest("/__mandu/events/recent")).toBe(false);
67
+ });
68
+ });
@@ -1,103 +1,103 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import {
3
- INTERNAL_EVENTS_ENDPOINT,
4
- createRuntimeObservabilityLifecycle,
5
- } from "../observability-lifecycle";
6
- import { eventBus } from "../../observability/event-bus";
7
- import {
8
- resetTracer,
9
- type Span,
10
- type SpanExporter,
11
- } from "../../observability/tracing";
12
-
13
- class CaptureExporter implements SpanExporter {
14
- readonly spans: Span[] = [];
15
- export(spans: Span[]): void {
16
- this.spans.push(...spans);
17
- }
18
- }
19
-
20
- afterEach(() => {
21
- resetTracer();
22
- });
23
-
24
- describe("runtime observability lifecycle", () => {
25
- test("serves heap snapshots with perf data when exposed", async () => {
26
- const lifecycle = createRuntimeObservabilityLifecycle({ isDev: true });
27
- const response = lifecycle.handleEndpoint(
28
- new Request("http://localhost/_mandu/heap"),
29
- "/_mandu/heap"
30
- );
31
-
32
- expect(response?.status).toBe(200);
33
- const body = await response!.json() as {
34
- process: { heapUsed: number };
35
- perf: unknown;
36
- };
37
- expect(body.process.heapUsed).toBeGreaterThan(0);
38
- expect(body.perf).toBeDefined();
39
- });
40
-
41
- test("keeps metrics hidden in production unless explicitly enabled", () => {
42
- const hidden = createRuntimeObservabilityLifecycle({ isDev: false });
43
- const shown = createRuntimeObservabilityLifecycle({
44
- isDev: false,
45
- metricsEndpoint: true,
46
- });
47
-
48
- expect(hidden.handleEndpoint(new Request("http://localhost/_mandu/metrics"), "/_mandu/metrics")).toBeNull();
49
- expect(shown.handleEndpoint(new Request("http://localhost/_mandu/metrics"), "/_mandu/metrics")?.status).toBe(200);
50
- });
51
-
52
- test("serves recent EventBus snapshots from the lifecycle endpoint", async () => {
53
- const lifecycle = createRuntimeObservabilityLifecycle({ isDev: true });
54
- const source = `observability-lifecycle-${Date.now()}`;
55
- eventBus.emit({
56
- type: "http",
57
- severity: "info",
58
- source,
59
- message: "GET /observed 200",
60
- });
61
-
62
- const response = lifecycle.handleEndpoint(
63
- new Request(`http://localhost${INTERNAL_EVENTS_ENDPOINT}/recent?source=${source}`),
64
- `${INTERNAL_EVENTS_ENDPOINT}/recent`
65
- );
66
-
67
- expect(response?.status).toBe(200);
68
- const body = await response!.json() as {
69
- events: Array<{ source: string; message: string }>;
70
- };
71
- expect(body.events.some((event) => event.source === source)).toBe(true);
72
- });
73
-
74
- test("wraps requests in a root tracing span", async () => {
75
- const exporter = new CaptureExporter();
76
- const lifecycle = createRuntimeObservabilityLifecycle({
77
- isDev: false,
78
- tracing: {
79
- enabled: true,
80
- customExporter: exporter,
81
- },
82
- });
83
- const incomingTraceId = "4bf92f3577b34da6a3ce929d0e0e4736";
84
- const incomingSpanId = "00f067aa0ba902b7";
85
- const response = await lifecycle.runRequest(
86
- new Request("https://example.test/users", {
87
- headers: {
88
- traceparent: `00-${incomingTraceId}-${incomingSpanId}-01`,
89
- },
90
- }),
91
- Date.now(),
92
- "corr-1",
93
- async () => new Response("created", { status: 201 })
94
- );
95
-
96
- expect(response.status).toBe(201);
97
- expect(exporter.spans).toHaveLength(1);
98
- expect(exporter.spans[0].traceId).toBe(incomingTraceId);
99
- expect(exporter.spans[0].parentSpanId).toBe(incomingSpanId);
100
- expect(exporter.spans[0].attributes["http.status_code"]).toBe(201);
101
- expect(exporter.spans[0].status).toBe("ok");
102
- });
103
- });
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ INTERNAL_EVENTS_ENDPOINT,
4
+ createRuntimeObservabilityLifecycle,
5
+ } from "../observability-lifecycle";
6
+ import { eventBus } from "../../observability/event-bus";
7
+ import {
8
+ resetTracer,
9
+ type Span,
10
+ type SpanExporter,
11
+ } from "../../observability/tracing";
12
+
13
+ class CaptureExporter implements SpanExporter {
14
+ readonly spans: Span[] = [];
15
+ export(spans: Span[]): void {
16
+ this.spans.push(...spans);
17
+ }
18
+ }
19
+
20
+ afterEach(() => {
21
+ resetTracer();
22
+ });
23
+
24
+ describe("runtime observability lifecycle", () => {
25
+ test("serves heap snapshots with perf data when exposed", async () => {
26
+ const lifecycle = createRuntimeObservabilityLifecycle({ isDev: true });
27
+ const response = lifecycle.handleEndpoint(
28
+ new Request("http://localhost/_mandu/heap"),
29
+ "/_mandu/heap"
30
+ );
31
+
32
+ expect(response?.status).toBe(200);
33
+ const body = await response!.json() as {
34
+ process: { heapUsed: number };
35
+ perf: unknown;
36
+ };
37
+ expect(body.process.heapUsed).toBeGreaterThan(0);
38
+ expect(body.perf).toBeDefined();
39
+ });
40
+
41
+ test("keeps metrics hidden in production unless explicitly enabled", () => {
42
+ const hidden = createRuntimeObservabilityLifecycle({ isDev: false });
43
+ const shown = createRuntimeObservabilityLifecycle({
44
+ isDev: false,
45
+ metricsEndpoint: true,
46
+ });
47
+
48
+ expect(hidden.handleEndpoint(new Request("http://localhost/_mandu/metrics"), "/_mandu/metrics")).toBeNull();
49
+ expect(shown.handleEndpoint(new Request("http://localhost/_mandu/metrics"), "/_mandu/metrics")?.status).toBe(200);
50
+ });
51
+
52
+ test("serves recent EventBus snapshots from the lifecycle endpoint", async () => {
53
+ const lifecycle = createRuntimeObservabilityLifecycle({ isDev: true });
54
+ const source = `observability-lifecycle-${Date.now()}`;
55
+ eventBus.emit({
56
+ type: "http",
57
+ severity: "info",
58
+ source,
59
+ message: "GET /observed 200",
60
+ });
61
+
62
+ const response = lifecycle.handleEndpoint(
63
+ new Request(`http://localhost${INTERNAL_EVENTS_ENDPOINT}/recent?source=${source}`),
64
+ `${INTERNAL_EVENTS_ENDPOINT}/recent`
65
+ );
66
+
67
+ expect(response?.status).toBe(200);
68
+ const body = await response!.json() as {
69
+ events: Array<{ source: string; message: string }>;
70
+ };
71
+ expect(body.events.some((event) => event.source === source)).toBe(true);
72
+ });
73
+
74
+ test("wraps requests in a root tracing span", async () => {
75
+ const exporter = new CaptureExporter();
76
+ const lifecycle = createRuntimeObservabilityLifecycle({
77
+ isDev: false,
78
+ tracing: {
79
+ enabled: true,
80
+ customExporter: exporter,
81
+ },
82
+ });
83
+ const incomingTraceId = "4bf92f3577b34da6a3ce929d0e0e4736";
84
+ const incomingSpanId = "00f067aa0ba902b7";
85
+ const response = await lifecycle.runRequest(
86
+ new Request("https://example.test/users", {
87
+ headers: {
88
+ traceparent: `00-${incomingTraceId}-${incomingSpanId}-01`,
89
+ },
90
+ }),
91
+ Date.now(),
92
+ "corr-1",
93
+ async () => new Response("created", { status: 201 })
94
+ );
95
+
96
+ expect(response.status).toBe(201);
97
+ expect(exporter.spans).toHaveLength(1);
98
+ expect(exporter.spans[0].traceId).toBe(incomingTraceId);
99
+ expect(exporter.spans[0].parentSpanId).toBe(incomingSpanId);
100
+ expect(exporter.spans[0].attributes["http.status_code"]).toBe(201);
101
+ expect(exporter.spans[0].status).toBe("ok");
102
+ });
103
+ });