@turingpulse/sdk 1.0.1

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 (160) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/ci.yml +246 -0
  3. package/.github/workflows/framework-compat.yml +169 -0
  4. package/.github/workflows/security.yml +336 -0
  5. package/CHANGELOG.md +29 -0
  6. package/LICENSE +13 -0
  7. package/MIGRATION.md +30 -0
  8. package/README.md +221 -0
  9. package/dist/attachments.d.ts +28 -0
  10. package/dist/attachments.d.ts.map +1 -0
  11. package/dist/attachments.js +59 -0
  12. package/dist/attachments.js.map +1 -0
  13. package/dist/config.d.ts +72 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +78 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +126 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +163 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/decorators.d.ts +6 -0
  22. package/dist/decorators.d.ts.map +1 -0
  23. package/dist/decorators.js +52 -0
  24. package/dist/decorators.js.map +1 -0
  25. package/dist/deploy.d.ts +89 -0
  26. package/dist/deploy.d.ts.map +1 -0
  27. package/dist/deploy.js +203 -0
  28. package/dist/deploy.js.map +1 -0
  29. package/dist/errors.d.ts +18 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +34 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/eventBuilder.d.ts +21 -0
  34. package/dist/eventBuilder.d.ts.map +1 -0
  35. package/dist/eventBuilder.js +127 -0
  36. package/dist/eventBuilder.js.map +1 -0
  37. package/dist/fingerprint.d.ts +158 -0
  38. package/dist/fingerprint.d.ts.map +1 -0
  39. package/dist/fingerprint.js +339 -0
  40. package/dist/fingerprint.js.map +1 -0
  41. package/dist/governance.d.ts +47 -0
  42. package/dist/governance.d.ts.map +1 -0
  43. package/dist/governance.js +104 -0
  44. package/dist/governance.js.map +1 -0
  45. package/dist/http.d.ts +62 -0
  46. package/dist/http.d.ts.map +1 -0
  47. package/dist/http.js +181 -0
  48. package/dist/http.js.map +1 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +23 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation.d.ts +40 -0
  54. package/dist/instrumentation.d.ts.map +1 -0
  55. package/dist/instrumentation.js +31 -0
  56. package/dist/instrumentation.js.map +1 -0
  57. package/dist/integrations/mastra.d.ts +64 -0
  58. package/dist/integrations/mastra.d.ts.map +1 -0
  59. package/dist/integrations/mastra.js +256 -0
  60. package/dist/integrations/mastra.js.map +1 -0
  61. package/dist/kpi.d.ts +21 -0
  62. package/dist/kpi.d.ts.map +1 -0
  63. package/dist/kpi.js +83 -0
  64. package/dist/kpi.js.map +1 -0
  65. package/dist/llmDetector.d.ts +22 -0
  66. package/dist/llmDetector.d.ts.map +1 -0
  67. package/dist/llmDetector.js +269 -0
  68. package/dist/llmDetector.js.map +1 -0
  69. package/dist/plugin.d.ts +33 -0
  70. package/dist/plugin.d.ts.map +1 -0
  71. package/dist/plugin.js +312 -0
  72. package/dist/plugin.js.map +1 -0
  73. package/dist/registry.d.ts +13 -0
  74. package/dist/registry.d.ts.map +1 -0
  75. package/dist/registry.js +18 -0
  76. package/dist/registry.js.map +1 -0
  77. package/dist/tracing.d.ts +10 -0
  78. package/dist/tracing.d.ts.map +1 -0
  79. package/dist/tracing.js +30 -0
  80. package/dist/tracing.js.map +1 -0
  81. package/dist/triggerState.d.ts +5 -0
  82. package/dist/triggerState.d.ts.map +1 -0
  83. package/dist/triggerState.js +19 -0
  84. package/dist/triggerState.js.map +1 -0
  85. package/dist/utils.d.ts +27 -0
  86. package/dist/utils.d.ts.map +1 -0
  87. package/dist/utils.js +72 -0
  88. package/dist/utils.js.map +1 -0
  89. package/package.json +37 -0
  90. package/packages/anthropic/package.json +16 -0
  91. package/packages/anthropic/src/index.ts +5 -0
  92. package/packages/anthropic/src/wrapper.ts +102 -0
  93. package/packages/anthropic/tsconfig.build.json +20 -0
  94. package/packages/langchain/package.json +16 -0
  95. package/packages/langchain/src/index.ts +7 -0
  96. package/packages/langchain/src/wrapper.ts +51 -0
  97. package/packages/mastra/package.json +17 -0
  98. package/packages/mastra/src/index.ts +8 -0
  99. package/packages/mastra/src/wrapper.ts +301 -0
  100. package/packages/openai/package.json +16 -0
  101. package/packages/openai/src/index.ts +8 -0
  102. package/packages/openai/src/wrapper.ts +103 -0
  103. package/packages/openai/tsconfig.build.json +20 -0
  104. package/packages/openclaw/openclaw.plugin.json +100 -0
  105. package/packages/openclaw/package.json +41 -0
  106. package/packages/openclaw/src/buffer.ts +99 -0
  107. package/packages/openclaw/src/config.ts +139 -0
  108. package/packages/openclaw/src/hooks/governance.ts +267 -0
  109. package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
  110. package/packages/openclaw/src/hooks/telemetry.ts +207 -0
  111. package/packages/openclaw/src/index.ts +91 -0
  112. package/packages/openclaw/src/mapper.ts +233 -0
  113. package/packages/openclaw/src/session-tracker.ts +181 -0
  114. package/packages/openclaw/src/types.ts +220 -0
  115. package/packages/openclaw/tests/buffer.test.ts +148 -0
  116. package/packages/openclaw/tests/config.test.ts +122 -0
  117. package/packages/openclaw/tests/governance.test.ts +232 -0
  118. package/packages/openclaw/tests/mapper.test.ts +242 -0
  119. package/packages/openclaw/tests/session-tracker.test.ts +124 -0
  120. package/packages/openclaw/tsconfig.json +18 -0
  121. package/packages/openclaw/vitest.config.ts +8 -0
  122. package/packages/vercel-ai/package.json +16 -0
  123. package/packages/vercel-ai/src/index.ts +5 -0
  124. package/packages/vercel-ai/src/wrapper.ts +49 -0
  125. package/scripts/bump-version.sh +58 -0
  126. package/scripts/update-readme-compat.mjs +151 -0
  127. package/src/__tests__/fingerprint.test.ts +328 -0
  128. package/src/attachments.ts +88 -0
  129. package/src/config.ts +164 -0
  130. package/src/context.ts +258 -0
  131. package/src/decorators.ts +61 -0
  132. package/src/deploy.ts +260 -0
  133. package/src/errors.ts +44 -0
  134. package/src/eventBuilder.ts +153 -0
  135. package/src/fingerprint.ts +421 -0
  136. package/src/governance.ts +156 -0
  137. package/src/http.ts +241 -0
  138. package/src/index.ts +57 -0
  139. package/src/instrumentation.ts +68 -0
  140. package/src/integrations/mastra.ts +335 -0
  141. package/src/kpi.ts +112 -0
  142. package/src/llmDetector.ts +330 -0
  143. package/src/plugin.ts +384 -0
  144. package/src/registry.ts +27 -0
  145. package/src/tracing.ts +39 -0
  146. package/src/triggerState.ts +27 -0
  147. package/src/utils.ts +78 -0
  148. package/tests/compat/anthropic.test.ts +61 -0
  149. package/tests/compat/cohere.test.ts +57 -0
  150. package/tests/compat/google-genai.test.ts +61 -0
  151. package/tests/compat/langchain-openai.test.ts +41 -0
  152. package/tests/compat/langchain.test.ts +64 -0
  153. package/tests/compat/mistral.test.ts +58 -0
  154. package/tests/compat/openai.test.ts +71 -0
  155. package/tests/compat/vercel-ai.test.ts +56 -0
  156. package/tests/plugins/anthropic-wrapper.test.ts +120 -0
  157. package/tests/plugins/langchain-wrapper.test.ts +128 -0
  158. package/tests/plugins/openai-wrapper.test.ts +165 -0
  159. package/tsconfig.json +21 -0
  160. package/vitest.config.ts +9 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Type definitions for OpenClaw plugin API and hook contexts.
3
+ *
4
+ * These mirror the OpenClaw plugin SDK interfaces. Since OpenClaw
5
+ * may not publish stable type packages yet, we define the subset
6
+ * we depend on here.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Hook context types — what OpenClaw passes to lifecycle hooks
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface ToolPreContext {
14
+ /** Name of the tool being invoked (e.g., "exec", "browser", "file_write"). */
15
+ toolName: string;
16
+ /** Parameters passed to the tool. */
17
+ toolParams: Record<string, unknown>;
18
+ /** OpenClaw session key (unique per sender+agent+workspace). */
19
+ sessionKey?: string;
20
+ /** Numeric or string session identifier. */
21
+ sessionId?: string;
22
+ /** Channel the message originated from (e.g., "whatsapp", "telegram"). */
23
+ channelId?: string;
24
+ /** Agent identifier within the gateway. */
25
+ agentId?: string;
26
+ }
27
+
28
+ export interface ToolPostContext {
29
+ toolName: string;
30
+ toolParams: Record<string, unknown>;
31
+ /** Result returned by the tool. */
32
+ result: unknown;
33
+ /** Error thrown during tool execution, if any. */
34
+ error?: Error;
35
+ /** Duration of the tool execution in milliseconds. */
36
+ durationMs?: number;
37
+ sessionKey?: string;
38
+ sessionId?: string;
39
+ channelId?: string;
40
+ agentId?: string;
41
+ }
42
+
43
+ export interface MessageReceivedContext {
44
+ type: 'message';
45
+ action: 'received';
46
+ sessionKey: string;
47
+ timestamp: Date;
48
+ messages: string[];
49
+ context: {
50
+ from: string;
51
+ content: string;
52
+ timestamp?: number;
53
+ channelId: string;
54
+ accountId?: string;
55
+ conversationId?: string;
56
+ messageId?: string;
57
+ metadata?: {
58
+ to?: string;
59
+ provider?: string;
60
+ surface?: string;
61
+ threadId?: string;
62
+ senderId?: string;
63
+ senderName?: string;
64
+ senderUsername?: string;
65
+ senderE164?: string;
66
+ };
67
+ };
68
+ }
69
+
70
+ export interface MessageSendingContext {
71
+ type: 'message';
72
+ action: 'sending';
73
+ sessionKey?: string;
74
+ sessionId?: string;
75
+ timestamp: Date;
76
+ messages: string[];
77
+ context: {
78
+ to: string;
79
+ content: string;
80
+ channelId: string;
81
+ accountId?: string;
82
+ conversationId?: string;
83
+ };
84
+ }
85
+
86
+ export interface MessageSentContext {
87
+ type: 'message';
88
+ action: 'sent';
89
+ sessionKey?: string;
90
+ sessionId?: string;
91
+ timestamp: Date;
92
+ messages: string[];
93
+ context: {
94
+ to: string;
95
+ content: string;
96
+ success: boolean;
97
+ error?: string;
98
+ channelId: string;
99
+ accountId?: string;
100
+ conversationId?: string;
101
+ messageId?: string;
102
+ };
103
+ }
104
+
105
+ export interface AgentPreContext {
106
+ sessionKey?: string;
107
+ sessionId?: string;
108
+ channelId?: string;
109
+ agentId?: string;
110
+ /** Model being used for this agent turn. */
111
+ model?: string;
112
+ }
113
+
114
+ export interface AgentPostContext {
115
+ sessionKey?: string;
116
+ sessionId?: string;
117
+ channelId?: string;
118
+ agentId?: string;
119
+ model?: string;
120
+ /** Token usage reported by the agent runtime, if available. */
121
+ usage?: {
122
+ promptTokens?: number;
123
+ completionTokens?: number;
124
+ };
125
+ /** Duration of the agent turn in milliseconds. */
126
+ durationMs?: number;
127
+ error?: Error;
128
+ }
129
+
130
+ export interface GatewayStartContext {
131
+ /** Gateway configuration object. */
132
+ config?: Record<string, unknown>;
133
+ }
134
+
135
+ export interface GatewayStopContext {
136
+ /** Reason for shutdown (e.g., "SIGTERM", "manual"). */
137
+ reason?: string;
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Hook return types — what the plugin can return to modify behavior
142
+ // ---------------------------------------------------------------------------
143
+
144
+ export interface ToolPreResult {
145
+ /** If true, the tool call is cancelled and not executed. */
146
+ cancel?: boolean;
147
+ /** Optionally replace tool parameters. */
148
+ params?: Record<string, unknown>;
149
+ }
150
+
151
+ export interface MessageSendingResult {
152
+ /** If true, the outgoing message is cancelled. */
153
+ cancel?: boolean;
154
+ /** Optionally rewrite the message content. */
155
+ content?: string;
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Hook registration options
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export interface HookOptions {
163
+ /** Execution priority (higher = earlier). */
164
+ priority?: number;
165
+ /** Hook timeout in milliseconds. */
166
+ timeoutMs?: number;
167
+ /** Behavior on hook error. */
168
+ mode?: 'fail-open' | 'fail-closed';
169
+ /** Behavior on hook timeout. */
170
+ onTimeout?: 'fail-open' | 'fail-closed';
171
+ /** Retry configuration. */
172
+ retry?: {
173
+ count: number;
174
+ backoffMs?: number;
175
+ };
176
+ /** Filter scope. */
177
+ scope?: {
178
+ channels?: string[];
179
+ agentIds?: string[];
180
+ toolNames?: string[];
181
+ };
182
+ /** Custom filter predicate. */
183
+ condition?: (context: unknown) => boolean;
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Plugin API — the `api` object OpenClaw passes to register()
188
+ // ---------------------------------------------------------------------------
189
+
190
+ export interface OpenClawLifecycleAPI {
191
+ on(
192
+ phase: string,
193
+ handler: (context: unknown) => Promise<unknown> | unknown,
194
+ options?: HookOptions,
195
+ ): void;
196
+ }
197
+
198
+ export interface OpenClawCommandRegistration {
199
+ name: string;
200
+ description: string;
201
+ handler: (ctx: { channel?: string; sessionKey?: string }) => { text: string };
202
+ }
203
+
204
+ export interface OpenClawPluginAPI {
205
+ /** Plugin configuration from openclaw.json entries. */
206
+ config: Record<string, unknown>;
207
+
208
+ /** Lifecycle hook registration. */
209
+ lifecycle: OpenClawLifecycleAPI;
210
+
211
+ /** Basic hook registration for command events. */
212
+ registerHook(
213
+ event: string,
214
+ handler: (event: unknown) => Promise<void> | void,
215
+ meta?: { name?: string; description?: string },
216
+ ): void;
217
+
218
+ /** Register a slash command. */
219
+ registerCommand(registration: OpenClawCommandRegistration): void;
220
+ }
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { EventBuffer } from '../src/buffer.js';
3
+ import type { AgentEvent } from '@turingpulse/interfaces';
4
+
5
+ function makeEvent(id: string): AgentEvent {
6
+ return {
7
+ runId: `run-${id}`,
8
+ agentId: `agent-${id}`,
9
+ timestamp: new Date().toISOString(),
10
+ type: 'span',
11
+ payload: { name: `test-${id}` },
12
+ };
13
+ }
14
+
15
+ function makeMockClient() {
16
+ return {
17
+ emitEvents: vi.fn().mockResolvedValue(undefined),
18
+ maxSerializedFieldLength: 50_000,
19
+ };
20
+ }
21
+
22
+ describe('EventBuffer', () => {
23
+ beforeEach(() => {
24
+ vi.useFakeTimers();
25
+ });
26
+
27
+ afterEach(() => {
28
+ vi.useRealTimers();
29
+ });
30
+
31
+ it('buffers events and flushes when batch size reached', async () => {
32
+ const client = makeMockClient();
33
+ const buffer = new EventBuffer(client as never, 3, 60_000);
34
+
35
+ buffer.push(makeEvent('1'));
36
+ buffer.push(makeEvent('2'));
37
+ expect(client.emitEvents).not.toHaveBeenCalled();
38
+
39
+ buffer.push(makeEvent('3'));
40
+
41
+ await vi.waitFor(() => {
42
+ expect(client.emitEvents).toHaveBeenCalledTimes(1);
43
+ });
44
+
45
+ const sentEvents = client.emitEvents.mock.calls[0][0];
46
+ expect(sentEvents).toHaveLength(3);
47
+ });
48
+
49
+ it('flushes on explicit flush call', async () => {
50
+ const client = makeMockClient();
51
+ const buffer = new EventBuffer(client as never, 100, 60_000);
52
+
53
+ buffer.push(makeEvent('1'));
54
+ buffer.push(makeEvent('2'));
55
+
56
+ await buffer.flush();
57
+
58
+ expect(client.emitEvents).toHaveBeenCalledTimes(1);
59
+ expect(client.emitEvents.mock.calls[0][0]).toHaveLength(2);
60
+ });
61
+
62
+ it('does nothing when flushing empty buffer', async () => {
63
+ const client = makeMockClient();
64
+ const buffer = new EventBuffer(client as never, 10, 60_000);
65
+
66
+ await buffer.flush();
67
+ expect(client.emitEvents).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it('flushes on interval timer', async () => {
71
+ const client = makeMockClient();
72
+ const buffer = new EventBuffer(client as never, 100, 1000);
73
+
74
+ buffer.start();
75
+ buffer.push(makeEvent('1'));
76
+
77
+ vi.advanceTimersByTime(1100);
78
+
79
+ await vi.waitFor(() => {
80
+ expect(client.emitEvents).toHaveBeenCalledTimes(1);
81
+ });
82
+
83
+ await buffer.stop();
84
+ });
85
+
86
+ it('reports pending count', () => {
87
+ const client = makeMockClient();
88
+ const buffer = new EventBuffer(client as never, 100, 60_000);
89
+
90
+ expect(buffer.pending).toBe(0);
91
+
92
+ buffer.push(makeEvent('1'));
93
+ buffer.push(makeEvent('2'));
94
+ expect(buffer.pending).toBe(2);
95
+ });
96
+
97
+ it('pushMany adds multiple events', async () => {
98
+ const client = makeMockClient();
99
+ const buffer = new EventBuffer(client as never, 100, 60_000);
100
+
101
+ buffer.pushMany([makeEvent('1'), makeEvent('2'), makeEvent('3')]);
102
+ expect(buffer.pending).toBe(3);
103
+
104
+ await buffer.flush();
105
+ expect(client.emitEvents.mock.calls[0][0]).toHaveLength(3);
106
+ });
107
+
108
+ it('silently handles emit errors without crashing', async () => {
109
+ const client = makeMockClient();
110
+ client.emitEvents.mockRejectedValueOnce(new Error('network failure'));
111
+
112
+ const buffer = new EventBuffer(client as never, 2, 60_000);
113
+ buffer.push(makeEvent('1'));
114
+ buffer.push(makeEvent('2'));
115
+
116
+ await vi.waitFor(() => {
117
+ expect(client.emitEvents).toHaveBeenCalled();
118
+ });
119
+
120
+ expect(buffer.pending).toBe(0);
121
+ });
122
+
123
+ it('stop drains remaining events', async () => {
124
+ const client = makeMockClient();
125
+ const buffer = new EventBuffer(client as never, 100, 1000);
126
+
127
+ buffer.start();
128
+ buffer.push(makeEvent('1'));
129
+ buffer.push(makeEvent('2'));
130
+
131
+ await buffer.stop();
132
+
133
+ expect(client.emitEvents).toHaveBeenCalledTimes(1);
134
+ expect(client.emitEvents.mock.calls[0][0]).toHaveLength(2);
135
+ expect(buffer.pending).toBe(0);
136
+ });
137
+
138
+ it('caps buffer at MAX_BUFFER_SIZE by dropping oldest events', () => {
139
+ const client = makeMockClient();
140
+ const buffer = new EventBuffer(client as never, 1000, 60_000);
141
+
142
+ for (let i = 0; i < 510; i++) {
143
+ buffer.push(makeEvent(String(i)));
144
+ }
145
+
146
+ expect(buffer.pending).toBe(500);
147
+ });
148
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveConfig, compileRedactPatterns } from '../src/config.js';
3
+
4
+ describe('resolveConfig', () => {
5
+ it('resolves a minimal valid config with defaults', () => {
6
+ const config = resolveConfig({ apiKey: 'tp-abc123' });
7
+
8
+ expect(config.apiKey).toBe('tp-abc123');
9
+ expect(config.endpoint).toBe('https://api.turingpulse.ai');
10
+ expect(config.governance.enabled).toBe(true);
11
+ expect(config.governance.failMode).toBe('open');
12
+ expect(config.governance.timeoutMs).toBe(3000);
13
+ expect(config.governance.excludeTools).toEqual([]);
14
+ expect(config.governance.scanOutboundMessages).toBe(false);
15
+ expect(config.telemetry.enabled).toBe(true);
16
+ expect(config.telemetry.batchSize).toBe(20);
17
+ expect(config.telemetry.flushIntervalMs).toBe(5000);
18
+ expect(config.telemetry.captureMessageContent).toBe(false);
19
+ expect(config.telemetry.captureToolParams).toBe(true);
20
+ expect(config.telemetry.redactPatterns).toEqual([]);
21
+ expect(config.metadata).toEqual({});
22
+ });
23
+
24
+ it('throws when apiKey is missing', () => {
25
+ expect(() => resolveConfig({})).toThrow('apiKey');
26
+ });
27
+
28
+ it('throws when apiKey is empty string', () => {
29
+ expect(() => resolveConfig({ apiKey: ' ' })).toThrow('apiKey');
30
+ });
31
+
32
+ it('trims trailing slashes from endpoint', () => {
33
+ const config = resolveConfig({
34
+ apiKey: 'tp-key',
35
+ endpoint: 'https://custom.host.com///',
36
+ });
37
+ expect(config.endpoint).toBe('https://custom.host.com');
38
+ });
39
+
40
+ it('resolves governance overrides', () => {
41
+ const config = resolveConfig({
42
+ apiKey: 'tp-key',
43
+ governance: {
44
+ enabled: false,
45
+ failMode: 'closed',
46
+ timeoutMs: 5000,
47
+ excludeTools: ['jq', 'cat'],
48
+ scanOutboundMessages: true,
49
+ },
50
+ });
51
+
52
+ expect(config.governance.enabled).toBe(false);
53
+ expect(config.governance.failMode).toBe('closed');
54
+ expect(config.governance.timeoutMs).toBe(5000);
55
+ expect(config.governance.excludeTools).toEqual(['jq', 'cat']);
56
+ expect(config.governance.scanOutboundMessages).toBe(true);
57
+ });
58
+
59
+ it('resolves telemetry overrides', () => {
60
+ const config = resolveConfig({
61
+ apiKey: 'tp-key',
62
+ telemetry: {
63
+ enabled: false,
64
+ batchSize: 50,
65
+ flushIntervalMs: 10000,
66
+ captureMessageContent: true,
67
+ captureToolParams: false,
68
+ redactPatterns: ['\\bsecret\\b'],
69
+ },
70
+ });
71
+
72
+ expect(config.telemetry.enabled).toBe(false);
73
+ expect(config.telemetry.batchSize).toBe(50);
74
+ expect(config.telemetry.flushIntervalMs).toBe(10000);
75
+ expect(config.telemetry.captureMessageContent).toBe(true);
76
+ expect(config.telemetry.captureToolParams).toBe(false);
77
+ expect(config.telemetry.redactPatterns).toEqual(['\\bsecret\\b']);
78
+ });
79
+
80
+ it('filters non-string metadata values', () => {
81
+ const config = resolveConfig({
82
+ apiKey: 'tp-key',
83
+ metadata: { team: 'engineering', count: 42, valid: 'yes' },
84
+ });
85
+
86
+ expect(config.metadata).toEqual({ team: 'engineering', valid: 'yes' });
87
+ });
88
+
89
+ it('ignores invalid governance values and uses defaults', () => {
90
+ const config = resolveConfig({
91
+ apiKey: 'tp-key',
92
+ governance: {
93
+ timeoutMs: -100,
94
+ failMode: 'invalid-value',
95
+ excludeTools: 'not-an-array',
96
+ },
97
+ });
98
+
99
+ expect(config.governance.timeoutMs).toBe(3000);
100
+ expect(config.governance.failMode).toBe('open');
101
+ expect(config.governance.excludeTools).toEqual([]);
102
+ });
103
+ });
104
+
105
+ describe('compileRedactPatterns', () => {
106
+ it('returns null for empty patterns', () => {
107
+ expect(compileRedactPatterns([])).toBeNull();
108
+ });
109
+
110
+ it('compiles custom patterns with default PII patterns', () => {
111
+ const regex = compileRedactPatterns(['\\bpassword\\b']);
112
+ expect(regex).toBeInstanceOf(RegExp);
113
+
114
+ const text = 'my password is secret, ssn 123-45-6789, email user@test.com';
115
+ const redacted = text.replace(regex!, '[REDACTED]');
116
+
117
+ expect(redacted).toContain('[REDACTED]');
118
+ expect(redacted).not.toContain('123-45-6789');
119
+ expect(redacted).not.toContain('user@test.com');
120
+ expect(redacted).not.toContain('password');
121
+ });
122
+ });