kernl 0.12.4 → 0.12.6
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 -1
- package/CHANGELOG.md +12 -0
- package/README.md +29 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/kernl/kernl.d.ts +6 -0
- package/dist/kernl/kernl.d.ts.map +1 -1
- package/dist/kernl/kernl.js +19 -0
- package/dist/kernl/types.d.ts +6 -0
- package/dist/kernl/types.d.ts.map +1 -1
- package/dist/lib/env.d.ts +2 -2
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/http.js +1 -5
- package/dist/mcp/sse.d.ts.map +1 -1
- package/dist/mcp/sse.js +1 -5
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -5
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +0 -1
- package/dist/thread/thread.d.ts +5 -4
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +91 -22
- package/dist/thread/types.d.ts +5 -0
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/tracing/__tests__/composite.test.d.ts +2 -0
- package/dist/tracing/__tests__/composite.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/composite.test.js +146 -0
- package/dist/tracing/__tests__/dispatch.test.d.ts +2 -0
- package/dist/tracing/__tests__/dispatch.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/dispatch.test.js +160 -0
- package/dist/tracing/__tests__/helpers.d.ts +69 -0
- package/dist/tracing/__tests__/helpers.d.ts.map +1 -0
- package/dist/tracing/__tests__/helpers.js +109 -0
- package/dist/tracing/__tests__/integration.test.d.ts +2 -0
- package/dist/tracing/__tests__/integration.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/integration.test.js +675 -0
- package/dist/tracing/__tests__/span.test.d.ts +2 -0
- package/dist/tracing/__tests__/span.test.d.ts.map +1 -0
- package/dist/tracing/__tests__/span.test.js +188 -0
- package/dist/tracing/dispatch.d.ts +43 -0
- package/dist/tracing/dispatch.d.ts.map +1 -0
- package/dist/tracing/dispatch.js +70 -0
- package/dist/tracing/index.d.ts +8 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +6 -0
- package/dist/tracing/span.d.ts +69 -0
- package/dist/tracing/span.d.ts.map +1 -0
- package/dist/tracing/span.js +64 -0
- package/dist/tracing/subscriber.d.ts +53 -0
- package/dist/tracing/subscriber.d.ts.map +1 -0
- package/dist/tracing/subscriber.js +1 -0
- package/dist/tracing/subscribers/composite.d.ts +26 -0
- package/dist/tracing/subscribers/composite.d.ts.map +1 -0
- package/dist/tracing/subscribers/composite.js +96 -0
- package/dist/tracing/subscribers/console.d.ts +22 -0
- package/dist/tracing/subscribers/console.d.ts.map +1 -0
- package/dist/tracing/subscribers/console.js +82 -0
- package/dist/tracing/types.d.ts +77 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +1 -0
- package/package.json +5 -1
- package/src/agent.ts +2 -0
- package/src/index.ts +1 -0
- package/src/kernl/kernl.ts +21 -0
- package/src/kernl/types.ts +7 -0
- package/src/mcp/http.ts +1 -9
- package/src/mcp/sse.ts +1 -10
- package/src/mcp/stdio.ts +1 -10
- package/src/task.ts +0 -1
- package/src/thread/thread.ts +111 -24
- package/src/thread/types.ts +5 -0
- package/src/tracing/__tests__/composite.test.ts +218 -0
- package/src/tracing/__tests__/dispatch.test.ts +222 -0
- package/src/tracing/__tests__/helpers.ts +138 -0
- package/src/tracing/__tests__/integration.test.ts +808 -0
- package/src/tracing/__tests__/span.test.ts +250 -0
- package/src/tracing/dispatch.ts +114 -0
- package/src/tracing/index.ts +39 -0
- package/src/tracing/span.ts +115 -0
- package/src/tracing/subscriber.ts +62 -0
- package/src/tracing/subscribers/composite.ts +102 -0
- package/src/tracing/subscribers/console.ts +101 -0
- package/src/tracing/types.ts +115 -0
- package/dist/trace/processor.d.ts +0 -1
- package/dist/trace/processor.d.ts.map +0 -1
- package/dist/trace/processor.js +0 -1
- package/dist/trace/traces.d.ts +0 -1
- package/dist/trace/traces.d.ts.map +0 -1
- package/dist/trace/traces.js +0 -73
- package/dist/trace/utils.d.ts +0 -22
- package/dist/trace/utils.d.ts.map +0 -1
- package/dist/trace/utils.js +0 -30
- package/src/trace/processor.ts +0 -0
- package/src/trace/traces.ts +0 -86
- package/src/trace/utils.ts +0 -38
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { CompositeSubscriber } from "../subscribers/composite";
|
|
4
|
+
import { TestSubscriber } from "./helpers";
|
|
5
|
+
|
|
6
|
+
describe("CompositeSubscriber", () => {
|
|
7
|
+
let sub1: TestSubscriber;
|
|
8
|
+
let sub2: TestSubscriber;
|
|
9
|
+
let composite: CompositeSubscriber;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
sub1 = new TestSubscriber();
|
|
13
|
+
sub2 = new TestSubscriber();
|
|
14
|
+
composite = new CompositeSubscriber([sub1, sub2]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("enabled", () => {
|
|
18
|
+
it("should return true if any subscriber is enabled", () => {
|
|
19
|
+
sub1.enabledKinds = new Set(["thread"]);
|
|
20
|
+
sub2.enabledKinds = new Set(["model.call"]);
|
|
21
|
+
|
|
22
|
+
expect(composite.enabled({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" })).toBe(true);
|
|
23
|
+
expect(composite.enabled({ kind: "model.call", provider: "test", modelId: "m1" })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return false if no subscriber is enabled", () => {
|
|
27
|
+
sub1.enabledKinds = new Set(["thread"]);
|
|
28
|
+
sub2.enabledKinds = new Set(["thread"]);
|
|
29
|
+
|
|
30
|
+
expect(composite.enabled({ kind: "model.call", provider: "test", modelId: "m1" })).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return true if all subscribers are enabled (default)", () => {
|
|
34
|
+
expect(composite.enabled({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" })).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("span", () => {
|
|
39
|
+
it("should create span in all enabled subscribers", () => {
|
|
40
|
+
const data = { kind: "thread" as const, threadId: "t1", agentId: "a1", namespace: "ns" };
|
|
41
|
+
composite.span(data, null);
|
|
42
|
+
|
|
43
|
+
expect(sub1.spans.size).toBe(1);
|
|
44
|
+
expect(sub2.spans.size).toBe(1);
|
|
45
|
+
|
|
46
|
+
const [, s1] = [...sub1.spans.entries()][0];
|
|
47
|
+
const [, s2] = [...sub2.spans.entries()][0];
|
|
48
|
+
expect(s1.data).toEqual(data);
|
|
49
|
+
expect(s2.data).toEqual(data);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return composite span ID", () => {
|
|
53
|
+
const id = composite.span(
|
|
54
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
55
|
+
null,
|
|
56
|
+
);
|
|
57
|
+
expect(id).toMatch(/^composite_\d+$/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should skip disabled subscribers", () => {
|
|
61
|
+
sub1.enabledKinds = new Set(["thread"]);
|
|
62
|
+
sub2.enabledKinds = new Set(["model.call"]); // not enabled for thread
|
|
63
|
+
|
|
64
|
+
composite.span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" }, null);
|
|
65
|
+
|
|
66
|
+
expect(sub1.spans.size).toBe(1);
|
|
67
|
+
expect(sub2.spans.size).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should map parent span IDs correctly", () => {
|
|
71
|
+
// Create parent
|
|
72
|
+
const parentId = composite.span(
|
|
73
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
74
|
+
null,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Create child with parent
|
|
78
|
+
composite.span({ kind: "model.call", provider: "test", modelId: "m1" }, parentId);
|
|
79
|
+
|
|
80
|
+
// Each subscriber should have correct parent mapping
|
|
81
|
+
const threadSpan1 = sub1.spansOfKind("thread")[0];
|
|
82
|
+
const modelSpan1 = sub1.spansOfKind("model.call")[0];
|
|
83
|
+
expect(modelSpan1.parent).toBe(threadSpan1.id);
|
|
84
|
+
|
|
85
|
+
const threadSpan2 = sub2.spansOfKind("thread")[0];
|
|
86
|
+
const modelSpan2 = sub2.spansOfKind("model.call")[0];
|
|
87
|
+
expect(modelSpan2.parent).toBe(threadSpan2.id);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("enter / exit", () => {
|
|
92
|
+
it("should dispatch to all subscribers", () => {
|
|
93
|
+
const spanId = composite.span(
|
|
94
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
95
|
+
null,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
composite.enter(spanId);
|
|
99
|
+
expect(sub1.entered.size).toBe(1);
|
|
100
|
+
expect(sub2.entered.size).toBe(1);
|
|
101
|
+
|
|
102
|
+
composite.exit(spanId);
|
|
103
|
+
expect(sub1.exited.size).toBe(1);
|
|
104
|
+
expect(sub2.exited.size).toBe(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should skip disabled subscribers", () => {
|
|
108
|
+
sub2.enabledKinds = new Set(["model.call"]); // not enabled for thread
|
|
109
|
+
|
|
110
|
+
const spanId = composite.span(
|
|
111
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
112
|
+
null,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
composite.enter(spanId);
|
|
116
|
+
expect(sub1.entered.size).toBe(1);
|
|
117
|
+
expect(sub2.entered.size).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("record", () => {
|
|
122
|
+
it("should dispatch to all subscribers", () => {
|
|
123
|
+
const spanId = composite.span(
|
|
124
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
125
|
+
null,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
composite.record(spanId, { state: "running" } as any);
|
|
129
|
+
|
|
130
|
+
const sub1SpanId = [...sub1.spans.keys()][0];
|
|
131
|
+
const sub2SpanId = [...sub2.spans.keys()][0];
|
|
132
|
+
|
|
133
|
+
expect(sub1.getRecorded(sub1SpanId)).toHaveLength(1);
|
|
134
|
+
expect(sub2.getRecorded(sub2SpanId)).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("error", () => {
|
|
139
|
+
it("should dispatch to all subscribers", () => {
|
|
140
|
+
const spanId = composite.span(
|
|
141
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
142
|
+
null,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const err = new Error("test");
|
|
146
|
+
composite.error(spanId, err);
|
|
147
|
+
|
|
148
|
+
const sub1SpanId = [...sub1.spans.keys()][0];
|
|
149
|
+
const sub2SpanId = [...sub2.spans.keys()][0];
|
|
150
|
+
|
|
151
|
+
expect(sub1.errors.get(sub1SpanId)).toHaveLength(1);
|
|
152
|
+
expect(sub2.errors.get(sub2SpanId)).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("close", () => {
|
|
157
|
+
it("should dispatch to all subscribers and clean up", () => {
|
|
158
|
+
const spanId = composite.span(
|
|
159
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
160
|
+
null,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
composite.close(spanId);
|
|
164
|
+
|
|
165
|
+
expect(sub1.closed.size).toBe(1);
|
|
166
|
+
expect(sub2.closed.size).toBe(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("event", () => {
|
|
171
|
+
it("should dispatch to all subscribers", () => {
|
|
172
|
+
composite.event({ kind: "thread.error", message: "test" }, null);
|
|
173
|
+
|
|
174
|
+
expect(sub1.events).toHaveLength(1);
|
|
175
|
+
expect(sub2.events).toHaveLength(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should map parent span IDs correctly", () => {
|
|
179
|
+
const spanId = composite.span(
|
|
180
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
181
|
+
null,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
composite.event({ kind: "thread.error", message: "test" }, spanId);
|
|
185
|
+
|
|
186
|
+
const sub1SpanId = [...sub1.spans.keys()][0];
|
|
187
|
+
const sub2SpanId = [...sub2.spans.keys()][0];
|
|
188
|
+
|
|
189
|
+
expect(sub1.events[0].parent).toBe(sub1SpanId);
|
|
190
|
+
expect(sub2.events[0].parent).toBe(sub2SpanId);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should handle null parent", () => {
|
|
194
|
+
composite.event({ kind: "thread.error", message: "test" }, null);
|
|
195
|
+
|
|
196
|
+
expect(sub1.events[0].parent).toBeNull();
|
|
197
|
+
expect(sub2.events[0].parent).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("flush", () => {
|
|
202
|
+
it("should flush all subscribers", async () => {
|
|
203
|
+
await composite.flush();
|
|
204
|
+
|
|
205
|
+
expect(sub1.calls.some((c) => c.method === "flush")).toBe(true);
|
|
206
|
+
expect(sub2.calls.some((c) => c.method === "flush")).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("shutdown", () => {
|
|
211
|
+
it("should shutdown all subscribers", async () => {
|
|
212
|
+
await composite.shutdown(5000);
|
|
213
|
+
|
|
214
|
+
expect(sub1.calls.some((c) => c.method === "shutdown")).toBe(true);
|
|
215
|
+
expect(sub2.calls.some((c) => c.method === "shutdown")).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
span,
|
|
5
|
+
event,
|
|
6
|
+
run,
|
|
7
|
+
current,
|
|
8
|
+
setSubscriber,
|
|
9
|
+
clearSubscriber,
|
|
10
|
+
getSubscriber,
|
|
11
|
+
} from "../dispatch";
|
|
12
|
+
import { NoopSpan, SpanImpl } from "../span";
|
|
13
|
+
import { TestSubscriber } from "./helpers";
|
|
14
|
+
|
|
15
|
+
describe("dispatch", () => {
|
|
16
|
+
let subscriber: TestSubscriber;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
subscriber = new TestSubscriber();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
clearSubscriber();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("setSubscriber / clearSubscriber / getSubscriber", () => {
|
|
27
|
+
it("should set and get subscriber", () => {
|
|
28
|
+
expect(getSubscriber()).toBeNull();
|
|
29
|
+
setSubscriber(subscriber);
|
|
30
|
+
expect(getSubscriber()).toBe(subscriber);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should throw if subscriber already set", () => {
|
|
34
|
+
setSubscriber(subscriber);
|
|
35
|
+
expect(() => setSubscriber(new TestSubscriber())).toThrow(
|
|
36
|
+
"Global subscriber already set",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should allow re-setting after clear", () => {
|
|
41
|
+
setSubscriber(subscriber);
|
|
42
|
+
clearSubscriber();
|
|
43
|
+
expect(getSubscriber()).toBeNull();
|
|
44
|
+
|
|
45
|
+
const newSubscriber = new TestSubscriber();
|
|
46
|
+
setSubscriber(newSubscriber);
|
|
47
|
+
expect(getSubscriber()).toBe(newSubscriber);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("span", () => {
|
|
52
|
+
it("should return NoopSpan when no subscriber is set", () => {
|
|
53
|
+
const s = span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" });
|
|
54
|
+
expect(s).toBeInstanceOf(NoopSpan);
|
|
55
|
+
expect(s.noop()).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should return NoopSpan when subscriber.enabled returns false", () => {
|
|
59
|
+
subscriber.enabledKinds = new Set(["model.call"]); // only model.call enabled
|
|
60
|
+
setSubscriber(subscriber);
|
|
61
|
+
|
|
62
|
+
const s = span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" });
|
|
63
|
+
expect(s).toBeInstanceOf(NoopSpan);
|
|
64
|
+
expect(subscriber.spans.size).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should return SpanImpl when subscriber is set and enabled", () => {
|
|
68
|
+
setSubscriber(subscriber);
|
|
69
|
+
|
|
70
|
+
const s = span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" });
|
|
71
|
+
expect(s).toBeInstanceOf(SpanImpl);
|
|
72
|
+
expect(s.noop()).toBe(false);
|
|
73
|
+
expect(s.id).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should pass span data to subscriber", () => {
|
|
77
|
+
setSubscriber(subscriber);
|
|
78
|
+
|
|
79
|
+
const data = { kind: "thread" as const, threadId: "t1", agentId: "a1", namespace: "ns" };
|
|
80
|
+
span(data);
|
|
81
|
+
|
|
82
|
+
expect(subscriber.spans.size).toBe(1);
|
|
83
|
+
const [, captured] = [...subscriber.spans.entries()][0];
|
|
84
|
+
expect(captured.data).toEqual(data);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should use null parent when parent=null", () => {
|
|
88
|
+
setSubscriber(subscriber);
|
|
89
|
+
|
|
90
|
+
span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" }, null);
|
|
91
|
+
|
|
92
|
+
const [, captured] = [...subscriber.spans.entries()][0];
|
|
93
|
+
expect(captured.parent).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should use explicit parent when provided", () => {
|
|
97
|
+
setSubscriber(subscriber);
|
|
98
|
+
|
|
99
|
+
span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" }, "parent_123");
|
|
100
|
+
|
|
101
|
+
const [, captured] = [...subscriber.spans.entries()][0];
|
|
102
|
+
expect(captured.parent).toBe("parent_123");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should resolve parent from context when parent='current' (default)", () => {
|
|
106
|
+
setSubscriber(subscriber);
|
|
107
|
+
|
|
108
|
+
// Create parent span
|
|
109
|
+
const parentSpan = span(
|
|
110
|
+
{ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" },
|
|
111
|
+
null,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Create child span within run() context
|
|
115
|
+
run(parentSpan.id, () => {
|
|
116
|
+
span({ kind: "model.call", provider: "test", modelId: "m1" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const modelSpans = subscriber.spansOfKind("model.call");
|
|
120
|
+
expect(modelSpans).toHaveLength(1);
|
|
121
|
+
expect(modelSpans[0].parent).toBe(parentSpan.id);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should use null parent when parent='current' but no context", () => {
|
|
125
|
+
setSubscriber(subscriber);
|
|
126
|
+
|
|
127
|
+
// No run() context
|
|
128
|
+
span({ kind: "thread", threadId: "t1", agentId: "a1", namespace: "ns" });
|
|
129
|
+
|
|
130
|
+
const [, captured] = [...subscriber.spans.entries()][0];
|
|
131
|
+
expect(captured.parent).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("run / current", () => {
|
|
136
|
+
it("should return null when no context", () => {
|
|
137
|
+
expect(current()).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should return spanId within run context", () => {
|
|
141
|
+
const result = run("span_123", () => {
|
|
142
|
+
return current();
|
|
143
|
+
});
|
|
144
|
+
expect(result).toBe("span_123");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should return to null after run completes", () => {
|
|
148
|
+
run("span_123", () => {
|
|
149
|
+
expect(current()).toBe("span_123");
|
|
150
|
+
});
|
|
151
|
+
expect(current()).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should support nested run contexts", () => {
|
|
155
|
+
const results: (string | null)[] = [];
|
|
156
|
+
|
|
157
|
+
run("outer", () => {
|
|
158
|
+
results.push(current());
|
|
159
|
+
run("inner", () => {
|
|
160
|
+
results.push(current());
|
|
161
|
+
});
|
|
162
|
+
results.push(current());
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(results).toEqual(["outer", "inner", "outer"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should handle null spanId in run", () => {
|
|
169
|
+
run("span_123", () => {
|
|
170
|
+
run(null, () => {
|
|
171
|
+
expect(current()).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("event", () => {
|
|
178
|
+
it("should do nothing when no subscriber is set", () => {
|
|
179
|
+
// Should not throw
|
|
180
|
+
event({ kind: "thread.error", message: "test" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should emit event to subscriber", () => {
|
|
184
|
+
setSubscriber(subscriber);
|
|
185
|
+
|
|
186
|
+
event({ kind: "thread.error", message: "test error", stack: "stack" });
|
|
187
|
+
|
|
188
|
+
expect(subscriber.events).toHaveLength(1);
|
|
189
|
+
expect(subscriber.events[0].data).toEqual({
|
|
190
|
+
kind: "thread.error",
|
|
191
|
+
message: "test error",
|
|
192
|
+
stack: "stack",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should use null parent when parent=null", () => {
|
|
197
|
+
setSubscriber(subscriber);
|
|
198
|
+
|
|
199
|
+
event({ kind: "thread.error", message: "test" }, null);
|
|
200
|
+
|
|
201
|
+
expect(subscriber.events[0].parent).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should use explicit parent when provided", () => {
|
|
205
|
+
setSubscriber(subscriber);
|
|
206
|
+
|
|
207
|
+
event({ kind: "thread.error", message: "test" }, "parent_123");
|
|
208
|
+
|
|
209
|
+
expect(subscriber.events[0].parent).toBe("parent_123");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should resolve parent from context when parent='current' (default)", () => {
|
|
213
|
+
setSubscriber(subscriber);
|
|
214
|
+
|
|
215
|
+
run("span_123", () => {
|
|
216
|
+
event({ kind: "thread.error", message: "test" });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(subscriber.events[0].parent).toBe("span_123");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { SpanId, SpanData, EventData } from "../types";
|
|
2
|
+
import type { Subscriber } from "../subscriber";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A test subscriber that captures all tracing calls for assertions.
|
|
6
|
+
*/
|
|
7
|
+
export class TestSubscriber implements Subscriber {
|
|
8
|
+
private nextId = 0;
|
|
9
|
+
|
|
10
|
+
// Captured data
|
|
11
|
+
spans = new Map<SpanId, { data: SpanData; parent: SpanId | null }>();
|
|
12
|
+
recorded = new Map<SpanId, Partial<SpanData>[]>();
|
|
13
|
+
errors = new Map<SpanId, Error[]>();
|
|
14
|
+
events: { data: EventData; parent: SpanId | null }[] = [];
|
|
15
|
+
|
|
16
|
+
// Call log for verifying order
|
|
17
|
+
calls: { method: string; spanId?: SpanId; args?: unknown }[] = [];
|
|
18
|
+
|
|
19
|
+
// State tracking
|
|
20
|
+
entered = new Set<SpanId>();
|
|
21
|
+
exited = new Set<SpanId>();
|
|
22
|
+
closed = new Set<SpanId>();
|
|
23
|
+
|
|
24
|
+
// Configuration
|
|
25
|
+
enabledKinds: Set<SpanData["kind"]> | null = null; // null = all enabled
|
|
26
|
+
|
|
27
|
+
enabled(data: SpanData): boolean {
|
|
28
|
+
if (this.enabledKinds === null) return true;
|
|
29
|
+
return this.enabledKinds.has(data.kind);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
span(data: SpanData, parent: SpanId | null): SpanId {
|
|
33
|
+
const id = `test_span_${this.nextId++}`;
|
|
34
|
+
this.spans.set(id, { data, parent });
|
|
35
|
+
this.calls.push({ method: "span", spanId: id, args: { data, parent } });
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enter(spanId: SpanId): void {
|
|
40
|
+
this.entered.add(spanId);
|
|
41
|
+
this.calls.push({ method: "enter", spanId });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
exit(spanId: SpanId): void {
|
|
45
|
+
this.exited.add(spanId);
|
|
46
|
+
this.calls.push({ method: "exit", spanId });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
record(spanId: SpanId, delta: Partial<SpanData>): void {
|
|
50
|
+
const existing = this.recorded.get(spanId) ?? [];
|
|
51
|
+
existing.push(delta);
|
|
52
|
+
this.recorded.set(spanId, existing);
|
|
53
|
+
this.calls.push({ method: "record", spanId, args: delta });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
error(spanId: SpanId, err: Error): void {
|
|
57
|
+
const existing = this.errors.get(spanId) ?? [];
|
|
58
|
+
existing.push(err);
|
|
59
|
+
this.errors.set(spanId, existing);
|
|
60
|
+
this.calls.push({ method: "error", spanId, args: err });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
close(spanId: SpanId): void {
|
|
64
|
+
this.closed.add(spanId);
|
|
65
|
+
this.calls.push({ method: "close", spanId });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
event(data: EventData, parent: SpanId | null): void {
|
|
69
|
+
this.events.push({ data, parent });
|
|
70
|
+
this.calls.push({ method: "event", args: { data, parent } });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async flush(): Promise<void> {
|
|
74
|
+
this.calls.push({ method: "flush" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async shutdown(_timeout?: number): Promise<void> {
|
|
78
|
+
this.calls.push({ method: "shutdown" });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Test helpers ---
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all spans of a specific kind.
|
|
85
|
+
*/
|
|
86
|
+
spansOfKind<K extends SpanData["kind"]>(
|
|
87
|
+
kind: K,
|
|
88
|
+
): Array<{ id: SpanId; data: Extract<SpanData, { kind: K }>; parent: SpanId | null }> {
|
|
89
|
+
const result: Array<{ id: SpanId; data: Extract<SpanData, { kind: K }>; parent: SpanId | null }> = [];
|
|
90
|
+
for (const [id, { data, parent }] of this.spans) {
|
|
91
|
+
if (data.kind === kind) {
|
|
92
|
+
result.push({ id, data: data as Extract<SpanData, { kind: K }>, parent });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get events of a specific kind.
|
|
100
|
+
*/
|
|
101
|
+
eventsOfKind<K extends EventData["kind"]>(
|
|
102
|
+
kind: K,
|
|
103
|
+
): Array<{ data: Extract<EventData, { kind: K }>; parent: SpanId | null }> {
|
|
104
|
+
return this.events.filter((e) => e.data.kind === kind) as Array<{
|
|
105
|
+
data: Extract<EventData, { kind: K }>;
|
|
106
|
+
parent: SpanId | null;
|
|
107
|
+
}>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get recorded data for a span.
|
|
112
|
+
*/
|
|
113
|
+
getRecorded(spanId: SpanId): Partial<SpanData>[] {
|
|
114
|
+
return this.recorded.get(spanId) ?? [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a span was fully completed (entered, exited, closed).
|
|
119
|
+
*/
|
|
120
|
+
isComplete(spanId: SpanId): boolean {
|
|
121
|
+
return this.entered.has(spanId) && this.exited.has(spanId) && this.closed.has(spanId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reset all captured data.
|
|
126
|
+
*/
|
|
127
|
+
reset(): void {
|
|
128
|
+
this.nextId = 0;
|
|
129
|
+
this.spans.clear();
|
|
130
|
+
this.recorded.clear();
|
|
131
|
+
this.errors.clear();
|
|
132
|
+
this.events = [];
|
|
133
|
+
this.calls = [];
|
|
134
|
+
this.entered.clear();
|
|
135
|
+
this.exited.clear();
|
|
136
|
+
this.closed.clear();
|
|
137
|
+
}
|
|
138
|
+
}
|