@goharvest/simforge 0.4.7 → 0.5.2

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.
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import * as baml from "./baml.js";
3
- import { Simforge, SimforgeError } from "./client";
3
+ import { getCurrentSpan, Simforge } from "./client";
4
4
  // Mock fetch globally
5
5
  globalThis.fetch = vi.fn();
6
6
  // Mock BAML execution
@@ -16,9 +16,30 @@ describe("Simforge Client", () => {
16
16
  const client = new Simforge({ apiKey: "test-key" });
17
17
  expect(client).toBeInstanceOf(Simforge);
18
18
  });
19
- it("should create client even with empty apiKey (validation happens at call time)", () => {
19
+ it("should auto-disable tracing with empty apiKey and log warning", () => {
20
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
20
21
  const client = new Simforge({ apiKey: "" });
21
22
  expect(client).toBeInstanceOf(Simforge);
23
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("apiKey is empty"));
24
+ // Should behave as disabled — withSpan returns the original function
25
+ const fn = client.withSpan("test-key", () => "result");
26
+ expect(fn()).toBe("result");
27
+ warnSpy.mockRestore();
28
+ });
29
+ it("should auto-disable tracing with whitespace apiKey", () => {
30
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
31
+ const client = new Simforge({ apiKey: " " });
32
+ const fn = client.withSpan("test-key", () => "result");
33
+ expect(fn()).toBe("result");
34
+ warnSpy.mockRestore();
35
+ });
36
+ it("should not warn when explicitly disabled with empty apiKey", () => {
37
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
38
+ const client = new Simforge({ apiKey: "", enabled: false });
39
+ expect(warnSpy).not.toHaveBeenCalled();
40
+ const fn = client.withSpan("test-key", () => "result");
41
+ expect(fn()).toBe("result");
42
+ warnSpy.mockRestore();
22
43
  });
23
44
  it("should use custom serviceUrl", () => {
24
45
  const client = new Simforge({
@@ -41,14 +62,7 @@ describe("Simforge Client", () => {
41
62
  });
42
63
  expect(client).toBeInstanceOf(Simforge);
43
64
  });
44
- it("should accept executeLocally flag", () => {
45
- const client = new Simforge({
46
- apiKey: "test-key",
47
- executeLocally: false,
48
- });
49
- expect(client).toBeInstanceOf(Simforge);
50
- });
51
- describe("call method - local execution", () => {
65
+ describe("call method", () => {
52
66
  beforeEach(() => {
53
67
  // Reset BAML mock
54
68
  vi.mocked(baml.runFunctionWithBaml).mockReset();
@@ -118,7 +132,6 @@ function ExtractName(text: string) -> Name {
118
132
  });
119
133
  const client = new Simforge({
120
134
  apiKey: "test-key",
121
- executeLocally: true,
122
135
  envVars: { OPENAI_API_KEY: "test-openai-key" },
123
136
  });
124
137
  const result = await client.call("ExtractName", {
@@ -151,7 +164,6 @@ function ExtractName(text: string) -> Name {
151
164
  vi.mocked(baml.runFunctionWithBaml).mockRejectedValueOnce(new Error("Invalid argument: Expected type String, got Number(12345)"));
152
165
  const client = new Simforge({
153
166
  apiKey: "test-key",
154
- executeLocally: true,
155
167
  });
156
168
  await expect(
157
169
  // biome-ignore lint/suspicious/noExplicitAny: Testing wrong type handling
@@ -175,7 +187,6 @@ function ExtractName(text: string) -> Name {
175
187
  vi.mocked(baml.runFunctionWithBaml).mockRejectedValueOnce(new Error("Missing required parameter: text"));
176
188
  const client = new Simforge({
177
189
  apiKey: "test-key",
178
- executeLocally: true,
179
190
  });
180
191
  await expect(client.call("ExtractName", {})).rejects.toThrow("Missing required parameter");
181
192
  });
@@ -232,7 +243,6 @@ function ExtractPerson(text: string) -> Person {}
232
243
  });
233
244
  const client = new Simforge({
234
245
  apiKey: "test-key",
235
- executeLocally: true,
236
246
  });
237
247
  const result = (await client.call("ExtractPerson", {
238
248
  text: "John Doe, 30, john@example.com, 123 Main St, New York, 10001",
@@ -262,7 +272,6 @@ function ExtractPerson(text: string) -> Person {}
262
272
  });
263
273
  const client = new Simforge({
264
274
  apiKey: "test-key",
265
- executeLocally: true,
266
275
  });
267
276
  await expect(client.call("ExtractName", { text: "test" })).rejects.toThrow("has no prompt");
268
277
  });
@@ -295,7 +304,6 @@ function ExtractPerson(text: string) -> Person {}
295
304
  });
296
305
  const client = new Simforge({
297
306
  apiKey: "test-key",
298
- executeLocally: true,
299
307
  envVars: {
300
308
  OPENAI_API_KEY: "test-key",
301
309
  },
@@ -309,50 +317,6 @@ function ExtractPerson(text: string) -> Person {}
309
317
  });
310
318
  });
311
319
  });
312
- describe("call method - server-side execution", () => {
313
- it("should make successful API call", async () => {
314
- const mockResponse = "success";
315
- globalThis.fetch.mockResolvedValueOnce({
316
- ok: true,
317
- json: async () => ({ result: mockResponse }),
318
- });
319
- const client = new Simforge({
320
- apiKey: "test-key",
321
- executeLocally: false,
322
- });
323
- const result = await client.call("testMethod", { input: "test" });
324
- expect(result).toEqual(mockResponse);
325
- expect(globalThis.fetch).toHaveBeenCalledWith(expect.stringContaining("/api/sdk/call"), expect.objectContaining({
326
- method: "POST",
327
- headers: expect.objectContaining({
328
- Authorization: "Bearer test-key",
329
- "Content-Type": "application/json",
330
- }),
331
- }));
332
- });
333
- it("should handle API errors", async () => {
334
- ;
335
- globalThis.fetch.mockResolvedValueOnce({
336
- ok: false,
337
- status: 400,
338
- json: async () => ({ error: "Bad request" }),
339
- });
340
- const client = new Simforge({
341
- apiKey: "test-key",
342
- executeLocally: false,
343
- });
344
- await expect(client.call("testMethod", {})).rejects.toThrow(SimforgeError);
345
- });
346
- it("should handle network errors", async () => {
347
- ;
348
- globalThis.fetch.mockRejectedValueOnce(new Error("Network error"));
349
- const client = new Simforge({
350
- apiKey: "test-key",
351
- executeLocally: false,
352
- });
353
- await expect(client.call("testMethod", {})).rejects.toThrow(SimforgeError);
354
- });
355
- });
356
320
  describe("trace creation with source field", () => {
357
321
  it("should include source='typescript-sdk' by default when creating traces", async () => {
358
322
  const mockFetch = vi.mocked(fetch);
@@ -387,7 +351,6 @@ function ExtractPerson(text: string) -> Person {}
387
351
  });
388
352
  const client = new Simforge({
389
353
  apiKey: "test-key",
390
- executeLocally: true,
391
354
  });
392
355
  await client.call("testMethod", {});
393
356
  // Verify trace creation was called with source field
@@ -440,7 +403,6 @@ function ExtractPerson(text: string) -> Person {}
440
403
  });
441
404
  const client = new Simforge({
442
405
  apiKey: "test-key",
443
- executeLocally: true,
444
406
  });
445
407
  await client.call("testMethod", {});
446
408
  // Verify trace creation includes both source and rawCollector
@@ -450,5 +412,838 @@ function ExtractPerson(text: string) -> Person {}
450
412
  expect(traceBody.rawCollector).toEqual(mockCollector);
451
413
  });
452
414
  });
415
+ describe("withSpan", () => {
416
+ it("should wrap an async function and trace its execution", async () => {
417
+ const mockFetch = vi.mocked(fetch);
418
+ // Mock wrapper trace creation
419
+ mockFetch.mockResolvedValueOnce({
420
+ ok: true,
421
+ status: 200,
422
+ json: async () => ({ traceId: "wrapper-trace-123" }),
423
+ });
424
+ const client = new Simforge({ apiKey: "test-key" });
425
+ const originalFn = async (name, count) => {
426
+ return { greeting: `Hello ${name}!`, count };
427
+ };
428
+ const wrappedFn = client.withSpan("greeting-service", originalFn);
429
+ const result = await wrappedFn("World", 42);
430
+ // Verify the function returns the correct result
431
+ expect(result).toEqual({ greeting: "Hello World!", count: 42 });
432
+ // Verify wrapper trace was created
433
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
434
+ expect(traceCall).toBeDefined();
435
+ const traceBody = JSON.parse(traceCall[1].body);
436
+ expect(traceBody.traceFunctionKey).toBe("greeting-service");
437
+ expect(traceBody.source).toBe("typescript-sdk-function");
438
+ // Inputs and result are in externalSpan.span_data
439
+ expect(traceBody.rawSpan.span_data.input).toEqual(["World", 42]);
440
+ expect(traceBody.rawSpan.span_data.output).toEqual({
441
+ greeting: "Hello World!",
442
+ count: 42,
443
+ });
444
+ // No meta needed for basic types
445
+ expect(traceBody.rawSpan.span_data.input_meta).toBeUndefined();
446
+ expect(traceBody.rawSpan.span_data.output_meta).toBeUndefined();
447
+ });
448
+ it("should wrap a sync function and trace its execution", async () => {
449
+ const mockFetch = vi.mocked(fetch);
450
+ // Mock wrapper trace creation
451
+ mockFetch.mockResolvedValueOnce({
452
+ ok: true,
453
+ status: 200,
454
+ json: async () => ({ traceId: "wrapper-trace-456" }),
455
+ });
456
+ const client = new Simforge({ apiKey: "test-key" });
457
+ const originalFn = (a, b) => a + b;
458
+ const wrappedFn = client.withSpan("calculator", originalFn);
459
+ const result = wrappedFn(5, 3);
460
+ // Verify the function returns the correct result
461
+ expect(result).toBe(8);
462
+ // Wait for the background trace to be sent
463
+ await new Promise((resolve) => setTimeout(resolve, 10));
464
+ // Verify wrapper trace was created
465
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
466
+ expect(traceCall).toBeDefined();
467
+ const traceBody = JSON.parse(traceCall[1].body);
468
+ expect(traceBody.traceFunctionKey).toBe("calculator");
469
+ // Result is in externalSpan.span_data.output
470
+ expect(traceBody.rawSpan.span_data.output).toBe(8);
471
+ });
472
+ it("should preserve the original function's return type", async () => {
473
+ const mockFetch = vi.mocked(fetch);
474
+ mockFetch.mockResolvedValueOnce({
475
+ ok: true,
476
+ status: 200,
477
+ json: async () => ({ traceId: "wrapper-trace-789" }),
478
+ });
479
+ const client = new Simforge({ apiKey: "test-key" });
480
+ const getUser = async (id) => ({
481
+ id,
482
+ name: "Test User",
483
+ });
484
+ const wrappedGetUser = client.withSpan("user-service", getUser);
485
+ // TypeScript should know the return type is Promise<User>
486
+ const user = await wrappedGetUser("user-123");
487
+ expect(user.id).toBe("user-123");
488
+ expect(user.name).toBe("Test User");
489
+ });
490
+ it("should handle string results without double-serialization", async () => {
491
+ const mockFetch = vi.mocked(fetch);
492
+ mockFetch.mockResolvedValueOnce({
493
+ ok: true,
494
+ status: 200,
495
+ json: async () => ({ traceId: "wrapper-trace-str" }),
496
+ });
497
+ const client = new Simforge({ apiKey: "test-key" });
498
+ const getString = async () => "Hello, World!";
499
+ const wrappedFn = client.withSpan("string-service", getString);
500
+ const result = await wrappedFn();
501
+ expect(result).toBe("Hello, World!");
502
+ // Wait for trace
503
+ await new Promise((resolve) => setTimeout(resolve, 10));
504
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
505
+ const traceBody = JSON.parse(traceCall[1].body);
506
+ // Result is in externalSpan.span_data.output
507
+ expect(traceBody.rawSpan.span_data.output).toBe("Hello, World!");
508
+ });
509
+ it("should handle functions with no arguments", async () => {
510
+ const mockFetch = vi.mocked(fetch);
511
+ mockFetch.mockResolvedValueOnce({
512
+ ok: true,
513
+ status: 200,
514
+ json: async () => ({ traceId: "wrapper-trace-noargs" }),
515
+ });
516
+ const client = new Simforge({ apiKey: "test-key" });
517
+ const getTimestamp = async () => Date.now();
518
+ const wrappedFn = client.withSpan("timestamp-service", getTimestamp);
519
+ const result = await wrappedFn();
520
+ expect(typeof result).toBe("number");
521
+ // Wait for trace
522
+ await new Promise((resolve) => setTimeout(resolve, 10));
523
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
524
+ const traceBody = JSON.parse(traceCall[1].body);
525
+ // Empty array for no-arg functions
526
+ expect(traceBody.rawSpan.span_data.input).toEqual([]);
527
+ });
528
+ it("should include superjson meta for special types like Date", async () => {
529
+ const mockFetch = vi.mocked(fetch);
530
+ mockFetch.mockResolvedValueOnce({
531
+ ok: true,
532
+ status: 200,
533
+ json: async () => ({ traceId: "wrapper-trace-date" }),
534
+ });
535
+ const client = new Simforge({ apiKey: "test-key" });
536
+ const testDate = new Date("2024-01-15T10:30:00.000Z");
537
+ const getDateResult = async (inputDate) => ({
538
+ timestamp: inputDate,
539
+ formatted: inputDate.toISOString(),
540
+ });
541
+ const wrappedFn = client.withSpan("date-service", getDateResult);
542
+ const result = await wrappedFn(testDate);
543
+ expect(result.timestamp).toEqual(testDate);
544
+ // Wait for trace
545
+ await new Promise((resolve) => setTimeout(resolve, 10));
546
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
547
+ const traceBody = JSON.parse(traceCall[1].body);
548
+ // Result should have JSON data and meta for Date type
549
+ expect(traceBody.rawSpan.span_data.output.timestamp).toBe("2024-01-15T10:30:00.000Z");
550
+ expect(traceBody.rawSpan.span_data.output.formatted).toBe("2024-01-15T10:30:00.000Z");
551
+ expect(traceBody.rawSpan.span_data.output_meta).toBeDefined();
552
+ expect(traceBody.rawSpan.span_data.output_meta.values.timestamp).toEqual([
553
+ "Date",
554
+ ]);
555
+ // Inputs should also have meta for the Date argument (array format)
556
+ expect(traceBody.rawSpan.span_data.input[0]).toBe("2024-01-15T10:30:00.000Z");
557
+ expect(traceBody.rawSpan.span_data.input_meta).toBeDefined();
558
+ expect(traceBody.rawSpan.span_data.input_meta.values["0"]).toEqual([
559
+ "Date",
560
+ ]);
561
+ });
562
+ it("should not throw if trace creation fails", async () => {
563
+ const mockFetch = vi.mocked(fetch);
564
+ const consoleErrorSpy = vi
565
+ .spyOn(console, "error")
566
+ .mockImplementation(() => { });
567
+ // Mock trace creation to fail
568
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
569
+ const client = new Simforge({ apiKey: "test-key" });
570
+ const originalFn = async () => "success";
571
+ const wrappedFn = client.withSpan("failing-trace", originalFn);
572
+ // Function should still return normally
573
+ const result = await wrappedFn();
574
+ expect(result).toBe("success");
575
+ // Allow microtasks to run so the promise rejection can be caught
576
+ await new Promise((resolve) => queueMicrotask(resolve));
577
+ await new Promise((resolve) => queueMicrotask(resolve));
578
+ // Wait for console.error to be called - this ensures the .catch() handler has run
579
+ await vi.waitFor(() => {
580
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create external span");
581
+ }, { timeout: 1000 });
582
+ consoleErrorSpy.mockRestore();
583
+ });
584
+ it("should share trace ID for nested withSpan calls", async () => {
585
+ const mockFetch = vi.mocked(fetch);
586
+ // Mock responses for both spans
587
+ mockFetch.mockResolvedValue({
588
+ ok: true,
589
+ status: 200,
590
+ json: async () => ({ traceId: "shared-trace" }),
591
+ });
592
+ const client = new Simforge({ apiKey: "test-key" });
593
+ const innerFn = async () => "inner result";
594
+ const outerFn = async () => {
595
+ const wrappedInner = client.withSpan("inner-span", innerFn);
596
+ return wrappedInner();
597
+ };
598
+ const wrappedOuter = client.withSpan("outer-span", outerFn);
599
+ await wrappedOuter();
600
+ // Wait for background traces
601
+ await new Promise((resolve) => setTimeout(resolve, 50));
602
+ // Get all span creation calls
603
+ const spanCalls = mockFetch.mock.calls.filter((call) => call[0].toString().includes("/sdk/externalSpans"));
604
+ expect(spanCalls.length).toBe(2);
605
+ const spanBodies = spanCalls.map((call) => JSON.parse(call[1].body));
606
+ // Both spans should share the same trace ID
607
+ const traceId = spanBodies[0].rawSpan.trace_id;
608
+ expect(spanBodies[1].rawSpan.trace_id).toBe(traceId);
609
+ });
610
+ it("should include parent_id for child spans", async () => {
611
+ const mockFetch = vi.mocked(fetch);
612
+ mockFetch.mockResolvedValue({
613
+ ok: true,
614
+ status: 200,
615
+ json: async () => ({ traceId: "nested-trace" }),
616
+ });
617
+ const client = new Simforge({ apiKey: "test-key" });
618
+ const innerFn = async () => "inner";
619
+ const outerFn = async () => {
620
+ const wrappedInner = client.withSpan("inner-span", innerFn);
621
+ return wrappedInner();
622
+ };
623
+ const wrappedOuter = client.withSpan("outer-span", outerFn);
624
+ await wrappedOuter();
625
+ // Wait for background traces
626
+ await new Promise((resolve) => setTimeout(resolve, 50));
627
+ const spanCalls = mockFetch.mock.calls.filter((call) => call[0].toString().includes("/sdk/externalSpans"));
628
+ const spanBodies = spanCalls.map((call) => JSON.parse(call[1].body));
629
+ // Find the inner and outer spans
630
+ const innerSpan = spanBodies.find((b) => b.traceFunctionKey === "inner-span");
631
+ const outerSpan = spanBodies.find((b) => b.traceFunctionKey === "outer-span");
632
+ // Inner span should have parent_id pointing to outer span
633
+ expect(innerSpan.rawSpan.parent_id).toBe(outerSpan.rawSpan.id);
634
+ // Outer span (root) should not have parent_id
635
+ expect(outerSpan.rawSpan.parent_id).toBeUndefined();
636
+ });
637
+ it("should generate new trace ID for independent calls", async () => {
638
+ const mockFetch = vi.mocked(fetch);
639
+ mockFetch.mockResolvedValue({
640
+ ok: true,
641
+ status: 200,
642
+ json: async () => ({ traceId: "independent-trace" }),
643
+ });
644
+ const client = new Simforge({ apiKey: "test-key" });
645
+ const fn1 = async () => "result1";
646
+ const fn2 = async () => "result2";
647
+ const wrapped1 = client.withSpan("span-1", fn1);
648
+ const wrapped2 = client.withSpan("span-2", fn2);
649
+ await wrapped1();
650
+ await wrapped2();
651
+ // Wait for background traces
652
+ await new Promise((resolve) => setTimeout(resolve, 50));
653
+ const spanCalls = mockFetch.mock.calls.filter((call) => call[0].toString().includes("/sdk/externalSpans"));
654
+ const spanBodies = spanCalls.map((call) => JSON.parse(call[1].body));
655
+ // Each independent call should have a different trace ID
656
+ expect(spanBodies[0].rawSpan.trace_id).not.toBe(spanBodies[1].rawSpan.trace_id);
657
+ });
658
+ it("should include function name in span data", async () => {
659
+ const mockFetch = vi.mocked(fetch);
660
+ mockFetch.mockResolvedValueOnce({
661
+ ok: true,
662
+ status: 200,
663
+ json: async () => ({ traceId: "func-name-trace" }),
664
+ });
665
+ const client = new Simforge({ apiKey: "test-key" });
666
+ async function myNamedFunction(x) {
667
+ return x * 2;
668
+ }
669
+ const wrappedFn = client.withSpan("math-service", myNamedFunction);
670
+ await wrappedFn(5);
671
+ // Wait for background trace
672
+ await new Promise((resolve) => setTimeout(resolve, 10));
673
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
674
+ const traceBody = JSON.parse(traceCall[1].body);
675
+ expect(traceBody.rawSpan.span_data.function_name).toBe("myNamedFunction");
676
+ });
677
+ it("should handle anonymous functions without function name", async () => {
678
+ const mockFetch = vi.mocked(fetch);
679
+ mockFetch.mockResolvedValueOnce({
680
+ ok: true,
681
+ status: 200,
682
+ json: async () => ({ traceId: "anon-func-trace" }),
683
+ });
684
+ const client = new Simforge({ apiKey: "test-key" });
685
+ const wrappedFn = client.withSpan("anon-service", async (x) => x * 2);
686
+ await wrappedFn(5);
687
+ // Wait for background trace
688
+ await new Promise((resolve) => setTimeout(resolve, 10));
689
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
690
+ const traceBody = JSON.parse(traceCall[1].body);
691
+ // Anonymous arrow functions don't have a name, so function_name should be undefined
692
+ expect(traceBody.rawSpan.span_data.function_name).toBeUndefined();
693
+ });
694
+ it("should use function name as span name when function has a name", async () => {
695
+ const mockFetch = vi.mocked(fetch);
696
+ mockFetch.mockResolvedValueOnce({
697
+ ok: true,
698
+ status: 200,
699
+ json: async () => ({ traceId: "named-span-trace" }),
700
+ });
701
+ const client = new Simforge({ apiKey: "test-key" });
702
+ async function processOrder(id) {
703
+ return `processed-${id}`;
704
+ }
705
+ const wrappedFn = client.withSpan("order-service", processOrder);
706
+ await wrappedFn("123");
707
+ // Wait for background trace
708
+ await new Promise((resolve) => setTimeout(resolve, 10));
709
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
710
+ const traceBody = JSON.parse(traceCall[1].body);
711
+ // Span name should be the function name, not the trace function key
712
+ expect(traceBody.rawSpan.span_data.name).toBe("processOrder");
713
+ expect(traceBody.rawSpan.span_data.function_name).toBe("processOrder");
714
+ });
715
+ it("should fall back to trace function key as span name for anonymous functions", async () => {
716
+ const mockFetch = vi.mocked(fetch);
717
+ mockFetch.mockResolvedValueOnce({
718
+ ok: true,
719
+ status: 200,
720
+ json: async () => ({ traceId: "anon-span-trace" }),
721
+ });
722
+ const client = new Simforge({ apiKey: "test-key" });
723
+ const wrappedFn = client.withSpan("anonymous-service", async (x) => x * 2);
724
+ await wrappedFn(5);
725
+ // Wait for background trace
726
+ await new Promise((resolve) => setTimeout(resolve, 10));
727
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
728
+ const traceBody = JSON.parse(traceCall[1].body);
729
+ // Span name should fall back to trace function key for anonymous functions
730
+ expect(traceBody.rawSpan.span_data.name).toBe("anonymous-service");
731
+ expect(traceBody.rawSpan.span_data.function_name).toBeUndefined();
732
+ });
733
+ it("should use explicit name option over function name", async () => {
734
+ const mockFetch = vi.mocked(fetch);
735
+ mockFetch.mockResolvedValueOnce({
736
+ ok: true,
737
+ status: 200,
738
+ json: async () => ({ traceId: "explicit-name-trace" }),
739
+ });
740
+ const client = new Simforge({ apiKey: "test-key" });
741
+ async function myFunction(id) {
742
+ return `result-${id}`;
743
+ }
744
+ const wrappedFn = client.withSpan("my-service", { name: "CustomSpanName" }, myFunction);
745
+ await wrappedFn("123");
746
+ // Wait for background trace
747
+ await new Promise((resolve) => setTimeout(resolve, 10));
748
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
749
+ const traceBody = JSON.parse(traceCall[1].body);
750
+ // Span name should be the explicit name, not the function name
751
+ expect(traceBody.rawSpan.span_data.name).toBe("CustomSpanName");
752
+ // function_name should still be the actual function name
753
+ expect(traceBody.rawSpan.span_data.function_name).toBe("myFunction");
754
+ });
755
+ it("should use explicit name option for anonymous functions", async () => {
756
+ const mockFetch = vi.mocked(fetch);
757
+ mockFetch.mockResolvedValueOnce({
758
+ ok: true,
759
+ status: 200,
760
+ json: async () => ({ traceId: "anon-explicit-name-trace" }),
761
+ });
762
+ const client = new Simforge({ apiKey: "test-key" });
763
+ const wrappedFn = client.withSpan("anon-service", { name: "AnonymousHandler" }, async (x) => x * 2);
764
+ await wrappedFn(5);
765
+ // Wait for background trace
766
+ await new Promise((resolve) => setTimeout(resolve, 10));
767
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
768
+ const traceBody = JSON.parse(traceCall[1].body);
769
+ // Span name should be the explicit name
770
+ expect(traceBody.rawSpan.span_data.name).toBe("AnonymousHandler");
771
+ // No function_name for anonymous functions
772
+ expect(traceBody.rawSpan.span_data.function_name).toBeUndefined();
773
+ });
774
+ it("should still send span on error", async () => {
775
+ const mockFetch = vi.mocked(fetch);
776
+ mockFetch.mockResolvedValue({
777
+ ok: true,
778
+ status: 200,
779
+ json: async () => ({ traceId: "error-trace" }),
780
+ });
781
+ const client = new Simforge({ apiKey: "test-key" });
782
+ const failingFn = async () => {
783
+ throw new Error("Test error");
784
+ };
785
+ const wrappedFn = client.withSpan("failing-fn", failingFn);
786
+ await expect(wrappedFn()).rejects.toThrow("Test error");
787
+ // Wait for background traces
788
+ await new Promise((resolve) => setTimeout(resolve, 50));
789
+ const spanCalls = mockFetch.mock.calls.filter((call) => call[0].toString().includes("/sdk/externalSpans"));
790
+ expect(spanCalls.length).toBe(1);
791
+ const spanBody = JSON.parse(spanCalls[0][1].body);
792
+ expect(spanBody.rawSpan.span_data.error).toBe("Test error");
793
+ });
794
+ it("should default type to 'custom' when no options provided", async () => {
795
+ const mockFetch = vi.mocked(fetch);
796
+ mockFetch.mockResolvedValueOnce({
797
+ ok: true,
798
+ status: 200,
799
+ json: async () => ({ traceId: "default-type-trace" }),
800
+ });
801
+ const client = new Simforge({ apiKey: "test-key" });
802
+ const fn = async () => "result";
803
+ const wrappedFn = client.withSpan("test-service", fn);
804
+ await wrappedFn();
805
+ // Wait for background trace
806
+ await new Promise((resolve) => setTimeout(resolve, 10));
807
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
808
+ const traceBody = JSON.parse(traceCall[1].body);
809
+ expect(traceBody.rawSpan.span_data.type).toBe("custom");
810
+ });
811
+ it("should use provided type from options", async () => {
812
+ const mockFetch = vi.mocked(fetch);
813
+ mockFetch.mockResolvedValueOnce({
814
+ ok: true,
815
+ status: 200,
816
+ json: async () => ({ traceId: "function-type-trace" }),
817
+ });
818
+ const client = new Simforge({ apiKey: "test-key" });
819
+ const fn = async () => "result";
820
+ const wrappedFn = client.withSpan("tool-service", { type: "function" }, fn);
821
+ await wrappedFn();
822
+ // Wait for background trace
823
+ await new Promise((resolve) => setTimeout(resolve, 10));
824
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
825
+ const traceBody = JSON.parse(traceCall[1].body);
826
+ expect(traceBody.rawSpan.span_data.type).toBe("function");
827
+ });
828
+ it("should support all span types", async () => {
829
+ const mockFetch = vi.mocked(fetch);
830
+ const client = new Simforge({ apiKey: "test-key" });
831
+ const spanTypes = [
832
+ "llm",
833
+ "agent",
834
+ "function",
835
+ "guardrail",
836
+ "handoff",
837
+ "custom",
838
+ ];
839
+ for (const spanType of spanTypes) {
840
+ mockFetch.mockClear();
841
+ mockFetch.mockResolvedValueOnce({
842
+ ok: true,
843
+ status: 200,
844
+ json: async () => ({ traceId: `${spanType}-trace` }),
845
+ });
846
+ const fn = async () => spanType;
847
+ const wrappedFn = client.withSpan(`${spanType}-service`, { type: spanType }, fn);
848
+ await wrappedFn();
849
+ // Wait for background trace
850
+ await new Promise((resolve) => setTimeout(resolve, 10));
851
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
852
+ const traceBody = JSON.parse(traceCall[1].body);
853
+ expect(traceBody.rawSpan.span_data.type).toBe(spanType);
854
+ }
855
+ });
856
+ it("should work with type option and sync functions", async () => {
857
+ const mockFetch = vi.mocked(fetch);
858
+ mockFetch.mockResolvedValueOnce({
859
+ ok: true,
860
+ status: 200,
861
+ json: async () => ({ traceId: "sync-type-trace" }),
862
+ });
863
+ const client = new Simforge({ apiKey: "test-key" });
864
+ const fn = (a, b) => a + b;
865
+ const wrappedFn = client.withSpan("calculator", { type: "function" }, fn);
866
+ const result = wrappedFn(5, 3);
867
+ expect(result).toBe(8);
868
+ // Wait for background trace
869
+ await new Promise((resolve) => setTimeout(resolve, 10));
870
+ const traceCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
871
+ const traceBody = JSON.parse(traceCall[1].body);
872
+ expect(traceBody.rawSpan.span_data.type).toBe("function");
873
+ expect(traceBody.rawSpan.span_data.input).toEqual([5, 3]);
874
+ expect(traceBody.rawSpan.span_data.output).toBe(8);
875
+ });
876
+ });
877
+ describe("enabled option", () => {
878
+ it("should default enabled to true", () => {
879
+ const mockFetch = vi.mocked(fetch);
880
+ mockFetch.mockResolvedValueOnce({
881
+ ok: true,
882
+ status: 200,
883
+ json: async () => ({ traceId: "enabled-default-trace" }),
884
+ });
885
+ const client = new Simforge({ apiKey: "test-key" });
886
+ const fn = async () => "result";
887
+ const wrappedFn = client.withSpan("test-service", fn);
888
+ // If enabled defaults to true, the wrapped function should send spans
889
+ // We can verify by checking that it's not the same reference as the original
890
+ expect(wrappedFn).not.toBe(fn);
891
+ });
892
+ it("should return unwrapped function when enabled is false", () => {
893
+ const client = new Simforge({ apiKey: "test-key", enabled: false });
894
+ const fn = async () => "result";
895
+ const wrappedFn = client.withSpan("test-service", fn);
896
+ // When disabled, withSpan should return the original function
897
+ expect(wrappedFn).toBe(fn);
898
+ });
899
+ it("should not send spans when enabled is false", async () => {
900
+ const mockFetch = vi.mocked(fetch);
901
+ mockFetch.mockClear();
902
+ const client = new Simforge({ apiKey: "test-key", enabled: false });
903
+ const fn = async (x) => x * 2;
904
+ const wrappedFn = client.withSpan("test-service", fn);
905
+ const result = await wrappedFn(5);
906
+ expect(result).toBe(10);
907
+ // Wait for any potential background traces
908
+ await new Promise((resolve) => setTimeout(resolve, 50));
909
+ // No fetch calls should have been made
910
+ const spanCalls = mockFetch.mock.calls.filter((call) => call[0].toString().includes("/sdk/externalSpans"));
911
+ expect(spanCalls.length).toBe(0);
912
+ });
913
+ it("should return unwrapped function via getFunction when enabled is false", () => {
914
+ const client = new Simforge({ apiKey: "test-key", enabled: false });
915
+ const func = client.getFunction("my-function");
916
+ const fn = async () => "result";
917
+ const wrappedFn = func.withSpan(fn);
918
+ // When disabled, should return the original function
919
+ expect(wrappedFn).toBe(fn);
920
+ });
921
+ it("should return unwrapped function with options when enabled is false", () => {
922
+ const client = new Simforge({ apiKey: "test-key", enabled: false });
923
+ const fn = async () => "result";
924
+ const wrappedFn = client.withSpan("test-service", { type: "function" }, fn);
925
+ expect(wrappedFn).toBe(fn);
926
+ });
927
+ });
928
+ describe("metadata", () => {
929
+ it("metadata is included in span_data when provided", async () => {
930
+ const mockFetch = vi.mocked(fetch);
931
+ mockFetch.mockResolvedValue({
932
+ ok: true,
933
+ status: 200,
934
+ json: async () => ({ traceId: "metadata-trace" }),
935
+ });
936
+ const client = new Simforge({ apiKey: "test-key" });
937
+ async function processOrder(id) {
938
+ return { id };
939
+ }
940
+ const wrapped = client.withSpan("my-service", {
941
+ type: "function",
942
+ metadata: { user_id: "u-123", region: "us-east" },
943
+ }, processOrder);
944
+ await wrapped("123");
945
+ await new Promise((resolve) => setTimeout(resolve, 10));
946
+ const spanCall = mockFetch.mock.calls.find((c) => String(c[0]).includes("/sdk/externalSpans"));
947
+ expect(spanCall).toBeDefined();
948
+ const traceBody = JSON.parse(spanCall[1]?.body ?? "{}");
949
+ const spanData = traceBody.rawSpan
950
+ .span_data;
951
+ expect(spanData.metadata).toEqual({ user_id: "u-123", region: "us-east" });
952
+ });
953
+ it("metadata is omitted from span_data when not provided", async () => {
954
+ const mockFetch = vi.mocked(fetch);
955
+ mockFetch.mockResolvedValue({
956
+ ok: true,
957
+ status: 200,
958
+ json: async () => ({ traceId: "no-metadata-trace" }),
959
+ });
960
+ const client = new Simforge({ apiKey: "test-key" });
961
+ async function myFn() {
962
+ return "result";
963
+ }
964
+ const wrapped = client.withSpan("my-service", myFn);
965
+ await wrapped();
966
+ await new Promise((resolve) => setTimeout(resolve, 10));
967
+ const spanCall = mockFetch.mock.calls.find((c) => String(c[0]).includes("/sdk/externalSpans"));
968
+ expect(spanCall).toBeDefined();
969
+ const traceBody = JSON.parse(spanCall[1]?.body ?? "{}");
970
+ const spanData = traceBody.rawSpan
971
+ .span_data;
972
+ expect(spanData.metadata).toBeUndefined();
973
+ });
974
+ it("metadata works with getFunction fluent API", async () => {
975
+ const mockFetch = vi.mocked(fetch);
976
+ mockFetch.mockResolvedValue({
977
+ ok: true,
978
+ status: 200,
979
+ json: async () => ({ traceId: "fluent-metadata-trace" }),
980
+ });
981
+ const client = new Simforge({ apiKey: "test-key" });
982
+ const service = client.getFunction("order-processing");
983
+ async function processOrder(id) {
984
+ return { id };
985
+ }
986
+ const wrapped = service.withSpan({
987
+ type: "function",
988
+ metadata: { env: "staging", version: "1.2.3" },
989
+ }, processOrder);
990
+ await wrapped("123");
991
+ await new Promise((resolve) => setTimeout(resolve, 10));
992
+ const spanCall = mockFetch.mock.calls.find((c) => String(c[0]).includes("/sdk/externalSpans"));
993
+ expect(spanCall).toBeDefined();
994
+ const traceBody = JSON.parse(spanCall[1]?.body ?? "{}");
995
+ const spanData = traceBody.rawSpan
996
+ .span_data;
997
+ expect(spanData.metadata).toEqual({ env: "staging", version: "1.2.3" });
998
+ });
999
+ });
1000
+ describe("runtime metadata (getCurrentSpan)", () => {
1001
+ it("sets metadata at runtime from inside a span", async () => {
1002
+ const mockFetch = vi.mocked(fetch);
1003
+ mockFetch.mockResolvedValue({
1004
+ ok: true,
1005
+ status: 200,
1006
+ json: async () => ({ traceId: "runtime-meta-trace" }),
1007
+ });
1008
+ const client = new Simforge({ apiKey: "test-key" });
1009
+ async function processOrder(id) {
1010
+ getCurrentSpan()?.setMetadata({
1011
+ request_id: "req-789",
1012
+ user_id: "u-456",
1013
+ });
1014
+ return { id };
1015
+ }
1016
+ const wrapped = client.withSpan("my-service", { type: "function" }, processOrder);
1017
+ await wrapped("123");
1018
+ await new Promise((resolve) => setTimeout(resolve, 10));
1019
+ const spanCall = mockFetch.mock.calls.find((c) => String(c[0]).includes("/sdk/externalSpans"));
1020
+ expect(spanCall).toBeDefined();
1021
+ const traceBody = JSON.parse(spanCall[1]?.body ?? "{}");
1022
+ const spanData = traceBody.rawSpan
1023
+ .span_data;
1024
+ expect(spanData.metadata).toEqual({
1025
+ request_id: "req-789",
1026
+ user_id: "u-456",
1027
+ });
1028
+ });
1029
+ it("merges runtime metadata with definition-time metadata (runtime wins)", async () => {
1030
+ const mockFetch = vi.mocked(fetch);
1031
+ mockFetch.mockResolvedValue({
1032
+ ok: true,
1033
+ status: 200,
1034
+ json: async () => ({ traceId: "merge-meta-trace" }),
1035
+ });
1036
+ const client = new Simforge({ apiKey: "test-key" });
1037
+ async function processOrder(id) {
1038
+ getCurrentSpan()?.setMetadata({
1039
+ region: "eu-west",
1040
+ request_id: "req-789",
1041
+ });
1042
+ return { id };
1043
+ }
1044
+ const wrapped = client.withSpan("my-service", { type: "function", metadata: { user_id: "u-123", region: "us-east" } }, processOrder);
1045
+ await wrapped("123");
1046
+ await new Promise((resolve) => setTimeout(resolve, 10));
1047
+ const spanCall = mockFetch.mock.calls.find((c) => String(c[0]).includes("/sdk/externalSpans"));
1048
+ expect(spanCall).toBeDefined();
1049
+ const traceBody = JSON.parse(spanCall[1]?.body ?? "{}");
1050
+ const spanData = traceBody.rawSpan
1051
+ .span_data;
1052
+ expect(spanData.metadata).toEqual({
1053
+ user_id: "u-123",
1054
+ region: "eu-west",
1055
+ request_id: "req-789",
1056
+ });
1057
+ });
1058
+ it("getCurrentSpan returns null outside of a span", () => {
1059
+ expect(getCurrentSpan()).toBeNull();
1060
+ });
1061
+ it("works with getFunction fluent API", async () => {
1062
+ const mockFetch = vi.mocked(fetch);
1063
+ mockFetch.mockResolvedValue({
1064
+ ok: true,
1065
+ status: 200,
1066
+ json: async () => ({ traceId: "fluent-runtime-meta" }),
1067
+ });
1068
+ const client = new Simforge({ apiKey: "test-key" });
1069
+ const service = client.getFunction("order-processing");
1070
+ async function processOrder(id) {
1071
+ getCurrentSpan()?.setMetadata({ computed_at: "2024-01-01" });
1072
+ return { id };
1073
+ }
1074
+ const wrapped = service.withSpan({ type: "function" }, processOrder);
1075
+ await wrapped("123");
1076
+ await new Promise((resolve) => setTimeout(resolve, 10));
1077
+ const spanCall = mockFetch.mock.calls.find((c) => String(c[0]).includes("/sdk/externalSpans"));
1078
+ expect(spanCall).toBeDefined();
1079
+ const traceBody = JSON.parse(spanCall[1]?.body ?? "{}");
1080
+ const spanData = traceBody.rawSpan
1081
+ .span_data;
1082
+ expect(spanData.metadata).toEqual({ computed_at: "2024-01-01" });
1083
+ });
1084
+ });
1085
+ describe("error resilience", () => {
1086
+ it("should not crash the host app when span sending throws synchronously", async () => {
1087
+ const mockFetch = vi.mocked(fetch);
1088
+ // Make fetch throw synchronously (simulating serialization failure)
1089
+ mockFetch.mockImplementation(() => {
1090
+ throw new Error("Serialization error");
1091
+ });
1092
+ const client = new Simforge({ apiKey: "test-key" });
1093
+ const fn = async (x) => x * 2;
1094
+ const wrappedFn = client.withSpan("resilient-service", fn);
1095
+ // Function should still return normally
1096
+ const result = await wrappedFn(5);
1097
+ expect(result).toBe(10);
1098
+ });
1099
+ it("should propagate user errors even when SDK has internal errors", async () => {
1100
+ const mockFetch = vi.mocked(fetch);
1101
+ mockFetch.mockImplementation(() => {
1102
+ throw new Error("SDK internal error");
1103
+ });
1104
+ const client = new Simforge({ apiKey: "test-key" });
1105
+ const failingFn = async () => {
1106
+ throw new Error("User error");
1107
+ };
1108
+ const wrappedFn = client.withSpan("error-service", failingFn);
1109
+ await expect(wrappedFn()).rejects.toThrow("User error");
1110
+ });
1111
+ it("should not crash with sync functions when span sending throws", () => {
1112
+ const mockFetch = vi.mocked(fetch);
1113
+ mockFetch.mockImplementation(() => {
1114
+ throw new Error("Serialization error");
1115
+ });
1116
+ const client = new Simforge({ apiKey: "test-key" });
1117
+ const fn = (a, b) => a + b;
1118
+ const wrappedFn = client.withSpan("sync-resilient", fn);
1119
+ const result = wrappedFn(5, 3);
1120
+ expect(result).toBe(8);
1121
+ });
1122
+ it("should propagate user errors from sync functions even when SDK has internal errors", () => {
1123
+ const mockFetch = vi.mocked(fetch);
1124
+ mockFetch.mockImplementation(() => {
1125
+ throw new Error("SDK internal error");
1126
+ });
1127
+ const client = new Simforge({ apiKey: "test-key" });
1128
+ const failingFn = () => {
1129
+ throw new Error("Sync user error");
1130
+ };
1131
+ const wrappedFn = client.withSpan("sync-error-service", failingFn);
1132
+ expect(() => wrappedFn()).toThrow("Sync user error");
1133
+ });
1134
+ });
1135
+ describe("getFunction", () => {
1136
+ it("should return a SimforgeFunction instance", () => {
1137
+ const client = new Simforge({ apiKey: "test-key" });
1138
+ const func = client.getFunction("my-function");
1139
+ expect(func).toBeDefined();
1140
+ expect(typeof func.withSpan).toBe("function");
1141
+ });
1142
+ it("should wrap functions with the bound traceFunctionKey", async () => {
1143
+ const mockFetch = vi.mocked(fetch);
1144
+ mockFetch.mockResolvedValueOnce({
1145
+ ok: true,
1146
+ status: 200,
1147
+ json: async () => ({ traceId: "getfunc-trace" }),
1148
+ });
1149
+ const client = new Simforge({ apiKey: "test-key" });
1150
+ const func = client.getFunction("bound-key");
1151
+ const originalFn = async (x) => x * 2;
1152
+ const wrappedFn = func.withSpan(originalFn);
1153
+ const result = await wrappedFn(5);
1154
+ expect(result).toBe(10);
1155
+ // Wait for background trace
1156
+ await new Promise((resolve) => setTimeout(resolve, 50));
1157
+ const spanCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
1158
+ const spanBody = JSON.parse(spanCall[1].body);
1159
+ expect(spanBody.traceFunctionKey).toBe("bound-key");
1160
+ });
1161
+ it("should allow wrapping multiple functions with the same key", async () => {
1162
+ const mockFetch = vi.mocked(fetch);
1163
+ mockFetch.mockResolvedValue({
1164
+ ok: true,
1165
+ status: 200,
1166
+ json: async () => ({ traceId: "multi-trace" }),
1167
+ });
1168
+ const client = new Simforge({ apiKey: "test-key" });
1169
+ const func = client.getFunction("shared-key");
1170
+ const fn1 = async () => "one";
1171
+ const fn2 = async () => "two";
1172
+ const wrapped1 = func.withSpan(fn1);
1173
+ const wrapped2 = func.withSpan(fn2);
1174
+ await wrapped1();
1175
+ await wrapped2();
1176
+ // Wait for background traces
1177
+ await new Promise((resolve) => setTimeout(resolve, 50));
1178
+ const spanCalls = mockFetch.mock.calls.filter((call) => call[0].toString().includes("/sdk/externalSpans"));
1179
+ const spanBodies = spanCalls.map((call) => JSON.parse(call[1].body));
1180
+ // Both should have the same traceFunctionKey
1181
+ expect(spanBodies[0].traceFunctionKey).toBe("shared-key");
1182
+ expect(spanBodies[1].traceFunctionKey).toBe("shared-key");
1183
+ });
1184
+ it("should default type to 'custom' when no options provided via getFunction", async () => {
1185
+ const mockFetch = vi.mocked(fetch);
1186
+ mockFetch.mockResolvedValueOnce({
1187
+ ok: true,
1188
+ status: 200,
1189
+ json: async () => ({ traceId: "func-default-type" }),
1190
+ });
1191
+ const client = new Simforge({ apiKey: "test-key" });
1192
+ const func = client.getFunction("default-type-key");
1193
+ const originalFn = async () => "result";
1194
+ const wrappedFn = func.withSpan(originalFn);
1195
+ await wrappedFn();
1196
+ // Wait for background trace
1197
+ await new Promise((resolve) => setTimeout(resolve, 50));
1198
+ const spanCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
1199
+ const spanBody = JSON.parse(spanCall[1].body);
1200
+ expect(spanBody.rawSpan.span_data.type).toBe("custom");
1201
+ });
1202
+ it("should use provided type from options via getFunction", async () => {
1203
+ const mockFetch = vi.mocked(fetch);
1204
+ mockFetch.mockResolvedValueOnce({
1205
+ ok: true,
1206
+ status: 200,
1207
+ json: async () => ({ traceId: "func-agent-type" }),
1208
+ });
1209
+ const client = new Simforge({ apiKey: "test-key" });
1210
+ const func = client.getFunction("agent-key");
1211
+ const originalFn = async () => "agent result";
1212
+ const wrappedFn = func.withSpan({ type: "agent" }, originalFn);
1213
+ await wrappedFn();
1214
+ // Wait for background trace
1215
+ await new Promise((resolve) => setTimeout(resolve, 50));
1216
+ const spanCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
1217
+ const spanBody = JSON.parse(spanCall[1].body);
1218
+ expect(spanBody.rawSpan.span_data.type).toBe("agent");
1219
+ });
1220
+ it("should support guardrail type via getFunction", async () => {
1221
+ const mockFetch = vi.mocked(fetch);
1222
+ mockFetch.mockResolvedValueOnce({
1223
+ ok: true,
1224
+ status: 200,
1225
+ json: async () => ({ traceId: "func-guardrail-type" }),
1226
+ });
1227
+ const client = new Simforge({ apiKey: "test-key" });
1228
+ const func = client.getFunction("safety-check");
1229
+ const checkContent = async (content) => ({
1230
+ safe: true,
1231
+ content,
1232
+ });
1233
+ const wrappedFn = func.withSpan({ type: "guardrail" }, checkContent);
1234
+ const result = await wrappedFn("test content");
1235
+ expect(result).toEqual({ safe: true, content: "test content" });
1236
+ // Wait for background trace
1237
+ await new Promise((resolve) => setTimeout(resolve, 50));
1238
+ const spanCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("/sdk/externalSpans"));
1239
+ const spanBody = JSON.parse(spanCall[1].body);
1240
+ expect(spanBody.rawSpan.span_data.type).toBe("guardrail");
1241
+ expect(spanBody.rawSpan.span_data.input).toEqual(["test content"]);
1242
+ expect(spanBody.rawSpan.span_data.output).toEqual({
1243
+ safe: true,
1244
+ content: "test content",
1245
+ });
1246
+ });
1247
+ });
453
1248
  });
454
1249
  //# sourceMappingURL=client.test.js.map