signalwire-agents 0.1.0
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/CLAUDE.md +112 -0
- package/README.md +417 -0
- package/dist/AgentBase.d.ts +481 -0
- package/dist/AgentBase.js +1155 -0
- package/dist/AgentBase.js.map +1 -0
- package/dist/AgentServer.d.ts +60 -0
- package/dist/AgentServer.js +128 -0
- package/dist/AgentServer.js.map +1 -0
- package/dist/AuthHandler.d.ts +56 -0
- package/dist/AuthHandler.js +119 -0
- package/dist/AuthHandler.js.map +1 -0
- package/dist/ConfigLoader.d.ts +68 -0
- package/dist/ConfigLoader.js +150 -0
- package/dist/ConfigLoader.js.map +1 -0
- package/dist/ContextBuilder.d.ts +431 -0
- package/dist/ContextBuilder.js +751 -0
- package/dist/ContextBuilder.js.map +1 -0
- package/dist/DataMap.d.ts +179 -0
- package/dist/DataMap.js +321 -0
- package/dist/DataMap.js.map +1 -0
- package/dist/Logger.d.ts +91 -0
- package/dist/Logger.js +191 -0
- package/dist/Logger.js.map +1 -0
- package/dist/PomBuilder.d.ts +144 -0
- package/dist/PomBuilder.js +249 -0
- package/dist/PomBuilder.js.map +1 -0
- package/dist/PromptManager.d.ts +85 -0
- package/dist/PromptManager.js +108 -0
- package/dist/PromptManager.js.map +1 -0
- package/dist/SchemaUtils.d.ts +42 -0
- package/dist/SchemaUtils.js +148 -0
- package/dist/SchemaUtils.js.map +1 -0
- package/dist/ServerlessAdapter.d.ts +100 -0
- package/dist/ServerlessAdapter.js +166 -0
- package/dist/ServerlessAdapter.js.map +1 -0
- package/dist/SessionManager.d.ts +86 -0
- package/dist/SessionManager.js +155 -0
- package/dist/SessionManager.js.map +1 -0
- package/dist/SslConfig.d.ts +77 -0
- package/dist/SslConfig.js +100 -0
- package/dist/SslConfig.js.map +1 -0
- package/dist/SwaigFunction.d.ts +88 -0
- package/dist/SwaigFunction.js +125 -0
- package/dist/SwaigFunction.js.map +1 -0
- package/dist/SwaigFunctionResult.d.ts +442 -0
- package/dist/SwaigFunctionResult.js +735 -0
- package/dist/SwaigFunctionResult.js.map +1 -0
- package/dist/SwmlBuilder.d.ts +37 -0
- package/dist/SwmlBuilder.js +55 -0
- package/dist/SwmlBuilder.js.map +1 -0
- package/dist/cli/agent-loader.d.ts +22 -0
- package/dist/cli/agent-loader.js +106 -0
- package/dist/cli/agent-loader.js.map +1 -0
- package/dist/cli/mock-data.d.ts +37 -0
- package/dist/cli/mock-data.js +79 -0
- package/dist/cli/mock-data.js.map +1 -0
- package/dist/cli/swaig-test.d.ts +32 -0
- package/dist/cli/swaig-test.js +405 -0
- package/dist/cli/swaig-test.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/prefabs/ConciergeAgent.d.ts +64 -0
- package/dist/prefabs/ConciergeAgent.js +188 -0
- package/dist/prefabs/ConciergeAgent.js.map +1 -0
- package/dist/prefabs/FAQBotAgent.d.ts +68 -0
- package/dist/prefabs/FAQBotAgent.js +206 -0
- package/dist/prefabs/FAQBotAgent.js.map +1 -0
- package/dist/prefabs/InfoGathererAgent.d.ts +73 -0
- package/dist/prefabs/InfoGathererAgent.js +202 -0
- package/dist/prefabs/InfoGathererAgent.js.map +1 -0
- package/dist/prefabs/ReceptionistAgent.d.ts +65 -0
- package/dist/prefabs/ReceptionistAgent.js +205 -0
- package/dist/prefabs/ReceptionistAgent.js.map +1 -0
- package/dist/prefabs/SurveyAgent.d.ts +84 -0
- package/dist/prefabs/SurveyAgent.js +324 -0
- package/dist/prefabs/SurveyAgent.js.map +1 -0
- package/dist/prefabs/index.d.ts +13 -0
- package/dist/prefabs/index.js +14 -0
- package/dist/prefabs/index.js.map +1 -0
- package/dist/skills/SkillBase.d.ts +134 -0
- package/dist/skills/SkillBase.js +105 -0
- package/dist/skills/SkillBase.js.map +1 -0
- package/dist/skills/SkillManager.d.ts +83 -0
- package/dist/skills/SkillManager.js +159 -0
- package/dist/skills/SkillManager.js.map +1 -0
- package/dist/skills/SkillRegistry.d.ts +105 -0
- package/dist/skills/SkillRegistry.js +195 -0
- package/dist/skills/SkillRegistry.js.map +1 -0
- package/dist/skills/builtin/api_ninjas_trivia.d.ts +35 -0
- package/dist/skills/builtin/api_ninjas_trivia.js +161 -0
- package/dist/skills/builtin/api_ninjas_trivia.js.map +1 -0
- package/dist/skills/builtin/claude_skills.d.ts +34 -0
- package/dist/skills/builtin/claude_skills.js +156 -0
- package/dist/skills/builtin/claude_skills.js.map +1 -0
- package/dist/skills/builtin/custom_skills.d.ts +59 -0
- package/dist/skills/builtin/custom_skills.js +231 -0
- package/dist/skills/builtin/custom_skills.js.map +1 -0
- package/dist/skills/builtin/datasphere.d.ts +34 -0
- package/dist/skills/builtin/datasphere.js +163 -0
- package/dist/skills/builtin/datasphere.js.map +1 -0
- package/dist/skills/builtin/datasphere_serverless.d.ts +54 -0
- package/dist/skills/builtin/datasphere_serverless.js +155 -0
- package/dist/skills/builtin/datasphere_serverless.js.map +1 -0
- package/dist/skills/builtin/datetime.d.ts +31 -0
- package/dist/skills/builtin/datetime.js +95 -0
- package/dist/skills/builtin/datetime.js.map +1 -0
- package/dist/skills/builtin/google_maps.d.ts +33 -0
- package/dist/skills/builtin/google_maps.js +233 -0
- package/dist/skills/builtin/google_maps.js.map +1 -0
- package/dist/skills/builtin/index.d.ts +28 -0
- package/dist/skills/builtin/index.js +75 -0
- package/dist/skills/builtin/index.js.map +1 -0
- package/dist/skills/builtin/info_gatherer.d.ts +52 -0
- package/dist/skills/builtin/info_gatherer.js +237 -0
- package/dist/skills/builtin/info_gatherer.js.map +1 -0
- package/dist/skills/builtin/joke.d.ts +32 -0
- package/dist/skills/builtin/joke.js +142 -0
- package/dist/skills/builtin/joke.js.map +1 -0
- package/dist/skills/builtin/math.d.ts +33 -0
- package/dist/skills/builtin/math.js +120 -0
- package/dist/skills/builtin/math.js.map +1 -0
- package/dist/skills/builtin/mcp_gateway.d.ts +35 -0
- package/dist/skills/builtin/mcp_gateway.js +85 -0
- package/dist/skills/builtin/mcp_gateway.js.map +1 -0
- package/dist/skills/builtin/native_vector_search.d.ts +44 -0
- package/dist/skills/builtin/native_vector_search.js +248 -0
- package/dist/skills/builtin/native_vector_search.js.map +1 -0
- package/dist/skills/builtin/play_background_file.d.ts +34 -0
- package/dist/skills/builtin/play_background_file.js +135 -0
- package/dist/skills/builtin/play_background_file.js.map +1 -0
- package/dist/skills/builtin/spider.d.ts +34 -0
- package/dist/skills/builtin/spider.js +164 -0
- package/dist/skills/builtin/spider.js.map +1 -0
- package/dist/skills/builtin/swml_transfer.d.ts +44 -0
- package/dist/skills/builtin/swml_transfer.js +204 -0
- package/dist/skills/builtin/swml_transfer.js.map +1 -0
- package/dist/skills/builtin/weather_api.d.ts +34 -0
- package/dist/skills/builtin/weather_api.js +136 -0
- package/dist/skills/builtin/weather_api.js.map +1 -0
- package/dist/skills/builtin/web_search.d.ts +34 -0
- package/dist/skills/builtin/web_search.js +141 -0
- package/dist/skills/builtin/web_search.js.map +1 -0
- package/dist/skills/builtin/wikipedia_search.d.ts +34 -0
- package/dist/skills/builtin/wikipedia_search.js +166 -0
- package/dist/skills/builtin/wikipedia_search.js.map +1 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +10 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/agent-guide.md +869 -0
- package/docs/api-reference.md +2419 -0
- package/docs/architecture.md +843 -0
- package/docs/cli-guide.md +579 -0
- package/docs/configuration.md +422 -0
- package/docs/contexts-guide.md +787 -0
- package/docs/datamap-guide.md +1141 -0
- package/docs/prefabs-guide.md +830 -0
- package/docs/security.md +436 -0
- package/docs/serverless-guide.md +513 -0
- package/docs/skills-guide.md +845 -0
- package/docs/swaig-reference.md +1627 -0
- package/examples/call-flow.ts +60 -0
- package/examples/contexts-steps.ts +62 -0
- package/examples/datamap-tools.ts +52 -0
- package/examples/dynamic-config.ts +51 -0
- package/examples/multi-agent.ts +67 -0
- package/examples/pom-prompt.ts +68 -0
- package/examples/simple-agent.ts +33 -0
- package/package.json +35 -0
- package/src/AgentBase.ts +1279 -0
- package/src/AgentServer.ts +149 -0
- package/src/AuthHandler.ts +141 -0
- package/src/ConfigLoader.ts +165 -0
- package/src/ContextBuilder.ts +796 -0
- package/src/DataMap.ts +365 -0
- package/src/Logger.ts +197 -0
- package/src/PomBuilder.ts +298 -0
- package/src/PromptManager.ts +133 -0
- package/src/SchemaUtils.ts +170 -0
- package/src/ServerlessAdapter.ts +218 -0
- package/src/SessionManager.ts +161 -0
- package/src/SslConfig.ts +118 -0
- package/src/SwaigFunction.ts +168 -0
- package/src/SwaigFunctionResult.ts +864 -0
- package/src/SwmlBuilder.ts +62 -0
- package/src/cli/agent-loader.ts +122 -0
- package/src/cli/mock-data.ts +110 -0
- package/src/cli/swaig-test.ts +440 -0
- package/src/index.ts +99 -0
- package/src/prefabs/ConciergeAgent.ts +251 -0
- package/src/prefabs/FAQBotAgent.ts +263 -0
- package/src/prefabs/InfoGathererAgent.ts +276 -0
- package/src/prefabs/ReceptionistAgent.ts +277 -0
- package/src/prefabs/SurveyAgent.ts +413 -0
- package/src/prefabs/index.ts +23 -0
- package/src/skills/SkillBase.ts +184 -0
- package/src/skills/SkillManager.ts +177 -0
- package/src/skills/SkillRegistry.ts +231 -0
- package/src/skills/builtin/api_ninjas_trivia.ts +230 -0
- package/src/skills/builtin/claude_skills.ts +239 -0
- package/src/skills/builtin/custom_skills.ts +326 -0
- package/src/skills/builtin/datasphere.ts +225 -0
- package/src/skills/builtin/datasphere_serverless.ts +194 -0
- package/src/skills/builtin/datetime.ts +115 -0
- package/src/skills/builtin/google_maps.ts +347 -0
- package/src/skills/builtin/index.ts +79 -0
- package/src/skills/builtin/info_gatherer.ts +331 -0
- package/src/skills/builtin/joke.ts +180 -0
- package/src/skills/builtin/math.ts +150 -0
- package/src/skills/builtin/mcp_gateway.ts +99 -0
- package/src/skills/builtin/native_vector_search.ts +327 -0
- package/src/skills/builtin/play_background_file.ts +189 -0
- package/src/skills/builtin/spider.ts +232 -0
- package/src/skills/builtin/swml_transfer.ts +285 -0
- package/src/skills/builtin/weather_api.ts +193 -0
- package/src/skills/builtin/web_search.ts +212 -0
- package/src/skills/builtin/wikipedia_search.ts +248 -0
- package/src/skills/index.ts +12 -0
- package/src/types.ts +99 -0
- package/tests/AgentBase.test.ts +693 -0
- package/tests/AgentServer.test.ts +102 -0
- package/tests/AuthHandler.test.ts +121 -0
- package/tests/ConfigLoader.test.ts +121 -0
- package/tests/DataMap.test.ts +286 -0
- package/tests/Logger.test.ts +218 -0
- package/tests/PomBuilder.test.ts +164 -0
- package/tests/ProxyDetection.test.ts +185 -0
- package/tests/SchemaUtils.test.ts +106 -0
- package/tests/ServerlessAdapter.test.ts +200 -0
- package/tests/SessionManager.test.ts +139 -0
- package/tests/SslConfig.test.ts +97 -0
- package/tests/SwaigFunctionResult.test.ts +298 -0
- package/tests/cli/mock-data.test.ts +94 -0
- package/tests/cli/swaig-test.test.ts +96 -0
- package/tests/prefabs/prefabs.test.ts +854 -0
- package/tests/setup.ts +6 -0
- package/tests/skills/SkillBase.test.ts +118 -0
- package/tests/skills/SkillManager.test.ts +164 -0
- package/tests/skills/SkillRegistry.test.ts +124 -0
- package/tests/skills/builtin.test.ts +924 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Logger, getLogger, setGlobalLogLevel, suppressAllLogs, setGlobalLogFormat, setGlobalLogColor, resetLoggingConfiguration } from '../src/Logger.js';
|
|
3
|
+
|
|
4
|
+
describe('Logger', () => {
|
|
5
|
+
let spyDebug: ReturnType<typeof vi.spyOn>;
|
|
6
|
+
let spyInfo: ReturnType<typeof vi.spyOn>;
|
|
7
|
+
let spyWarn: ReturnType<typeof vi.spyOn>;
|
|
8
|
+
let spyError: ReturnType<typeof vi.spyOn>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
spyDebug = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
12
|
+
spyInfo = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
13
|
+
spyWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
14
|
+
spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
15
|
+
setGlobalLogLevel('debug');
|
|
16
|
+
setGlobalLogColor(false);
|
|
17
|
+
setGlobalLogFormat('text');
|
|
18
|
+
suppressAllLogs(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
setGlobalLogLevel('info');
|
|
24
|
+
setGlobalLogFormat('text');
|
|
25
|
+
setGlobalLogColor(false);
|
|
26
|
+
suppressAllLogs(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('getLogger returns Logger instance', () => {
|
|
30
|
+
const log = getLogger('test');
|
|
31
|
+
expect(log).toBeInstanceOf(Logger);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('logs at debug level', () => {
|
|
35
|
+
const log = getLogger('mymod');
|
|
36
|
+
log.debug('hello');
|
|
37
|
+
expect(spyDebug).toHaveBeenCalledWith('[DEBUG] [mymod] hello');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('logs at info level', () => {
|
|
41
|
+
const log = getLogger('mymod');
|
|
42
|
+
log.info('world');
|
|
43
|
+
expect(spyInfo).toHaveBeenCalledWith('[INFO] [mymod] world');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('logs at warn level', () => {
|
|
47
|
+
const log = getLogger('mymod');
|
|
48
|
+
log.warn('caution');
|
|
49
|
+
expect(spyWarn).toHaveBeenCalledWith('[WARN] [mymod] caution');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('logs at error level', () => {
|
|
53
|
+
const log = getLogger('mymod');
|
|
54
|
+
log.error('failure');
|
|
55
|
+
expect(spyError).toHaveBeenCalledWith('[ERROR] [mymod] failure');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('includes data object in output', () => {
|
|
59
|
+
const log = getLogger('test');
|
|
60
|
+
log.info('msg', { key: 'val' });
|
|
61
|
+
expect(spyInfo).toHaveBeenCalledWith('[INFO] [test] msg {"key":"val"}');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('respects log level filtering', () => {
|
|
65
|
+
setGlobalLogLevel('warn');
|
|
66
|
+
const log = getLogger('test');
|
|
67
|
+
log.debug('hidden');
|
|
68
|
+
log.info('hidden');
|
|
69
|
+
log.warn('shown');
|
|
70
|
+
log.error('shown');
|
|
71
|
+
expect(spyDebug).not.toHaveBeenCalled();
|
|
72
|
+
expect(spyInfo).not.toHaveBeenCalled();
|
|
73
|
+
expect(spyWarn).toHaveBeenCalledOnce();
|
|
74
|
+
expect(spyError).toHaveBeenCalledOnce();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('setGlobalLogLevel changes level', () => {
|
|
78
|
+
setGlobalLogLevel('error');
|
|
79
|
+
const log = getLogger('test');
|
|
80
|
+
log.warn('nope');
|
|
81
|
+
log.error('yes');
|
|
82
|
+
expect(spyWarn).not.toHaveBeenCalled();
|
|
83
|
+
expect(spyError).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('suppressAllLogs suppresses everything', () => {
|
|
87
|
+
suppressAllLogs(true);
|
|
88
|
+
const log = getLogger('test');
|
|
89
|
+
log.debug('nope');
|
|
90
|
+
log.info('nope');
|
|
91
|
+
log.warn('nope');
|
|
92
|
+
log.error('nope');
|
|
93
|
+
expect(spyDebug).not.toHaveBeenCalled();
|
|
94
|
+
expect(spyInfo).not.toHaveBeenCalled();
|
|
95
|
+
expect(spyWarn).not.toHaveBeenCalled();
|
|
96
|
+
expect(spyError).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('suppressAllLogs can be turned off', () => {
|
|
100
|
+
suppressAllLogs(true);
|
|
101
|
+
const log = getLogger('test');
|
|
102
|
+
log.info('hidden');
|
|
103
|
+
expect(spyInfo).not.toHaveBeenCalled();
|
|
104
|
+
suppressAllLogs(false);
|
|
105
|
+
log.info('shown');
|
|
106
|
+
expect(spyInfo).toHaveBeenCalledOnce();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('empty data object does not append to output', () => {
|
|
110
|
+
const log = getLogger('test');
|
|
111
|
+
log.info('clean', {});
|
|
112
|
+
expect(spyInfo).toHaveBeenCalledWith('[INFO] [test] clean');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('debug level shows all messages', () => {
|
|
116
|
+
setGlobalLogLevel('debug');
|
|
117
|
+
const log = getLogger('test');
|
|
118
|
+
log.debug('d');
|
|
119
|
+
log.info('i');
|
|
120
|
+
log.warn('w');
|
|
121
|
+
log.error('e');
|
|
122
|
+
expect(spyDebug).toHaveBeenCalledOnce();
|
|
123
|
+
expect(spyInfo).toHaveBeenCalledOnce();
|
|
124
|
+
expect(spyWarn).toHaveBeenCalledOnce();
|
|
125
|
+
expect(spyError).toHaveBeenCalledOnce();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── Pass 2: New logger features ──────────────────────────────────
|
|
129
|
+
|
|
130
|
+
it('JSON format outputs structured JSON', () => {
|
|
131
|
+
setGlobalLogFormat('json');
|
|
132
|
+
const log = getLogger('mymod');
|
|
133
|
+
log.info('hello world');
|
|
134
|
+
expect(spyInfo).toHaveBeenCalledOnce();
|
|
135
|
+
const output = spyInfo.mock.calls[0][0] as string;
|
|
136
|
+
const parsed = JSON.parse(output);
|
|
137
|
+
expect(parsed.level).toBe('info');
|
|
138
|
+
expect(parsed.logger).toBe('mymod');
|
|
139
|
+
expect(parsed.message).toBe('hello world');
|
|
140
|
+
expect(parsed.timestamp).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('JSON format includes data fields', () => {
|
|
144
|
+
setGlobalLogFormat('json');
|
|
145
|
+
const log = getLogger('test');
|
|
146
|
+
log.warn('alert', { code: 500, path: '/api' });
|
|
147
|
+
const output = spyWarn.mock.calls[0][0] as string;
|
|
148
|
+
const parsed = JSON.parse(output);
|
|
149
|
+
expect(parsed.code).toBe(500);
|
|
150
|
+
expect(parsed.path).toBe('/api');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('bind creates child logger with context', () => {
|
|
154
|
+
const log = getLogger('test');
|
|
155
|
+
const child = log.bind({ callId: 'call-42', function: 'get_time' });
|
|
156
|
+
child.info('processing');
|
|
157
|
+
const output = spyInfo.mock.calls[0][0] as string;
|
|
158
|
+
expect(output).toContain('"callId":"call-42"');
|
|
159
|
+
expect(output).toContain('"function":"get_time"');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('bind context merges with log data', () => {
|
|
163
|
+
const log = getLogger('test').bind({ callId: 'c1' });
|
|
164
|
+
log.info('msg', { extra: true });
|
|
165
|
+
const output = spyInfo.mock.calls[0][0] as string;
|
|
166
|
+
expect(output).toContain('"callId":"c1"');
|
|
167
|
+
expect(output).toContain('"extra":true');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('bind context can be chained', () => {
|
|
171
|
+
const log = getLogger('test')
|
|
172
|
+
.bind({ callId: 'c1' })
|
|
173
|
+
.bind({ step: 'auth' });
|
|
174
|
+
log.info('ok');
|
|
175
|
+
const output = spyInfo.mock.calls[0][0] as string;
|
|
176
|
+
expect(output).toContain('"callId":"c1"');
|
|
177
|
+
expect(output).toContain('"step":"auth"');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('color output includes ANSI codes', () => {
|
|
181
|
+
setGlobalLogColor(true);
|
|
182
|
+
const log = getLogger('test');
|
|
183
|
+
log.info('colorful');
|
|
184
|
+
const output = spyInfo.mock.calls[0][0] as string;
|
|
185
|
+
expect(output).toContain('\x1b[32m'); // green for info
|
|
186
|
+
expect(output).toContain('\x1b[0m'); // reset
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('no ANSI codes when color disabled', () => {
|
|
190
|
+
setGlobalLogColor(false);
|
|
191
|
+
const log = getLogger('test');
|
|
192
|
+
log.error('plain');
|
|
193
|
+
const output = spyError.mock.calls[0][0] as string;
|
|
194
|
+
expect(output).not.toContain('\x1b[');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('resetLoggingConfiguration restores defaults', () => {
|
|
198
|
+
setGlobalLogLevel('error');
|
|
199
|
+
setGlobalLogFormat('json');
|
|
200
|
+
suppressAllLogs(true);
|
|
201
|
+
resetLoggingConfiguration();
|
|
202
|
+
// After reset, should use env vars or defaults
|
|
203
|
+
const log = getLogger('test');
|
|
204
|
+
log.info('after reset');
|
|
205
|
+
// Since logs were re-enabled and level reset to 'info', this should log
|
|
206
|
+
expect(spyInfo).toHaveBeenCalledOnce();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('JSON format in bind context', () => {
|
|
210
|
+
setGlobalLogFormat('json');
|
|
211
|
+
const log = getLogger('test').bind({ reqId: 'r1' });
|
|
212
|
+
log.debug('check');
|
|
213
|
+
const output = spyDebug.mock.calls[0][0] as string;
|
|
214
|
+
const parsed = JSON.parse(output);
|
|
215
|
+
expect(parsed.reqId).toBe('r1');
|
|
216
|
+
expect(parsed.message).toBe('check');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PomBuilder, PomSection } from '../src/PomBuilder.js';
|
|
3
|
+
import { SwmlBuilder } from '../src/SwmlBuilder.js';
|
|
4
|
+
|
|
5
|
+
describe('PomSection', () => {
|
|
6
|
+
it('toDict includes only non-empty fields', () => {
|
|
7
|
+
const s = new PomSection({ title: 'Hello', body: 'World' });
|
|
8
|
+
const d = s.toDict();
|
|
9
|
+
expect(d.title).toBe('Hello');
|
|
10
|
+
expect(d.body).toBe('World');
|
|
11
|
+
expect(d.bullets).toBeUndefined();
|
|
12
|
+
expect(d.subsections).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('renderMarkdown produces heading + body', () => {
|
|
16
|
+
const s = new PomSection({ title: 'Title', body: 'Body text' });
|
|
17
|
+
const md = s.renderMarkdown();
|
|
18
|
+
expect(md).toContain('## Title');
|
|
19
|
+
expect(md).toContain('Body text');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renderMarkdown with bullets', () => {
|
|
23
|
+
const s = new PomSection({ title: 'T', bullets: ['a', 'b'] });
|
|
24
|
+
const md = s.renderMarkdown();
|
|
25
|
+
expect(md).toContain('- a');
|
|
26
|
+
expect(md).toContain('- b');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renderMarkdown with numbered bullets', () => {
|
|
30
|
+
const s = new PomSection({ title: 'T', bullets: ['a', 'b'], numberedBullets: true });
|
|
31
|
+
const md = s.renderMarkdown();
|
|
32
|
+
expect(md).toContain('1. a');
|
|
33
|
+
expect(md).toContain('2. b');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('PomBuilder', () => {
|
|
38
|
+
it('addSection and toDict', () => {
|
|
39
|
+
const b = new PomBuilder()
|
|
40
|
+
.addSection('Role', { body: 'You are a helper' })
|
|
41
|
+
.addSection('Rules', { bullets: ['Be nice', 'Be helpful'] });
|
|
42
|
+
|
|
43
|
+
const dict = b.toDict();
|
|
44
|
+
expect(dict.length).toBe(2);
|
|
45
|
+
expect(dict[0].title).toBe('Role');
|
|
46
|
+
expect(dict[0].body).toBe('You are a helper');
|
|
47
|
+
expect(dict[1].bullets).toEqual(['Be nice', 'Be helpful']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('addToSection creates section if missing', () => {
|
|
51
|
+
const b = new PomBuilder().addToSection('New', { body: 'hello' });
|
|
52
|
+
expect(b.hasSection('New')).toBe(true);
|
|
53
|
+
expect(b.toDict()[0].body).toBe('hello');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('addToSection appends body', () => {
|
|
57
|
+
const b = new PomBuilder()
|
|
58
|
+
.addSection('S', { body: 'line1' })
|
|
59
|
+
.addToSection('S', { body: 'line2' });
|
|
60
|
+
expect(b.getSection('S')!.body).toBe('line1\n\nline2');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('addToSection appends bullets', () => {
|
|
64
|
+
const b = new PomBuilder()
|
|
65
|
+
.addSection('S', { bullets: ['a'] })
|
|
66
|
+
.addToSection('S', { bullet: 'b' })
|
|
67
|
+
.addToSection('S', { bullets: ['c', 'd'] });
|
|
68
|
+
expect(b.getSection('S')!.bullets).toEqual(['a', 'b', 'c', 'd']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('addSubsection', () => {
|
|
72
|
+
const b = new PomBuilder()
|
|
73
|
+
.addSection('Parent')
|
|
74
|
+
.addSubsection('Parent', 'Child', { body: 'child body' });
|
|
75
|
+
|
|
76
|
+
const dict = b.toDict();
|
|
77
|
+
expect(dict[0].subsections!.length).toBe(1);
|
|
78
|
+
expect(dict[0].subsections![0].title).toBe('Child');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('addSubsection auto-creates parent', () => {
|
|
82
|
+
const b = new PomBuilder().addSubsection('P', 'C', { body: 'x' });
|
|
83
|
+
expect(b.hasSection('P')).toBe(true);
|
|
84
|
+
expect(b.toDict()[0].subsections![0].title).toBe('C');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('hasSection / getSection / findSection', () => {
|
|
88
|
+
const b = new PomBuilder()
|
|
89
|
+
.addSection('Top')
|
|
90
|
+
.addSubsection('Top', 'Nested', { body: 'x' });
|
|
91
|
+
|
|
92
|
+
expect(b.hasSection('Top')).toBe(true);
|
|
93
|
+
expect(b.hasSection('Missing')).toBe(false);
|
|
94
|
+
expect(b.getSection('Top')).toBeDefined();
|
|
95
|
+
expect(b.findSection('Nested')).toBeDefined();
|
|
96
|
+
expect(b.findSection('Missing')).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renderMarkdown produces valid markdown', () => {
|
|
100
|
+
const b = new PomBuilder()
|
|
101
|
+
.addSection('Persona', { body: 'You are helpful' })
|
|
102
|
+
.addSection('Rules', { bullets: ['Be nice', 'Be clear'] });
|
|
103
|
+
|
|
104
|
+
const md = b.renderMarkdown();
|
|
105
|
+
expect(md).toContain('## Persona');
|
|
106
|
+
expect(md).toContain('You are helpful');
|
|
107
|
+
expect(md).toContain('## Rules');
|
|
108
|
+
expect(md).toContain('- Be nice');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('addSection with subsections in options', () => {
|
|
112
|
+
const b = new PomBuilder().addSection('Root', {
|
|
113
|
+
body: 'root body',
|
|
114
|
+
subsections: [
|
|
115
|
+
{ title: 'Sub1', body: 'sub1 body' },
|
|
116
|
+
{ title: 'Sub2', bullets: ['x'] },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
const dict = b.toDict();
|
|
120
|
+
expect(dict[0].subsections!.length).toBe(2);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('SwmlBuilder', () => {
|
|
125
|
+
it('creates empty document', () => {
|
|
126
|
+
const b = new SwmlBuilder();
|
|
127
|
+
const doc = b.getDocument();
|
|
128
|
+
expect(doc['version']).toBe('1.0.0');
|
|
129
|
+
expect((doc['sections'] as Record<string, unknown[]>)['main']).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('addVerb', () => {
|
|
133
|
+
const b = new SwmlBuilder();
|
|
134
|
+
b.addVerb('answer', {});
|
|
135
|
+
b.addVerb('ai', { prompt: { text: 'hello' } });
|
|
136
|
+
const main = (b.getDocument()['sections'] as Record<string, unknown[]>)['main'];
|
|
137
|
+
expect(main.length).toBe(2);
|
|
138
|
+
expect(main[0]).toEqual({ answer: {} });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('addVerbToSection creates section', () => {
|
|
142
|
+
const b = new SwmlBuilder();
|
|
143
|
+
b.addVerbToSection('custom', 'play', { url: 'test.mp3' });
|
|
144
|
+
const sections = b.getDocument()['sections'] as Record<string, unknown[]>;
|
|
145
|
+
expect(sections['custom']).toBeDefined();
|
|
146
|
+
expect(sections['custom'][0]).toEqual({ play: { url: 'test.mp3' } });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('reset clears document', () => {
|
|
150
|
+
const b = new SwmlBuilder();
|
|
151
|
+
b.addVerb('answer', {});
|
|
152
|
+
b.reset();
|
|
153
|
+
const main = (b.getDocument()['sections'] as Record<string, unknown[]>)['main'];
|
|
154
|
+
expect(main).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('renderDocument returns JSON', () => {
|
|
158
|
+
const b = new SwmlBuilder();
|
|
159
|
+
b.addVerb('answer', {});
|
|
160
|
+
const json = b.renderDocument();
|
|
161
|
+
const parsed = JSON.parse(json);
|
|
162
|
+
expect(parsed.version).toBe('1.0.0');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AgentBase } from '../src/AgentBase.js';
|
|
3
|
+
|
|
4
|
+
describe('ProxyDetection', () => {
|
|
5
|
+
let agent: AgentBase;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Clear env vars that could interfere
|
|
9
|
+
delete process.env['SWML_PROXY_URL_BASE'];
|
|
10
|
+
delete process.env['SWML_PROXY_DEBUG'];
|
|
11
|
+
agent = new AgentBase({ name: 'proxy-test', route: '/', basicAuth: ['user', 'pass'] });
|
|
12
|
+
agent.setPromptText('test prompt');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('getFullUrl returns localhost by default', () => {
|
|
16
|
+
const url = agent.getFullUrl();
|
|
17
|
+
expect(url).toContain('localhost');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('manualSetProxyUrl overrides getFullUrl', () => {
|
|
21
|
+
agent.manualSetProxyUrl('https://tunnel.example.com');
|
|
22
|
+
const url = agent.getFullUrl();
|
|
23
|
+
expect(url).toBe('https://tunnel.example.com');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('manualSetProxyUrl strips trailing slashes', () => {
|
|
27
|
+
agent.manualSetProxyUrl('https://tunnel.example.com///');
|
|
28
|
+
const url = agent.getFullUrl();
|
|
29
|
+
expect(url).toBe('https://tunnel.example.com');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('manualSetProxyUrl with auth', () => {
|
|
33
|
+
agent.manualSetProxyUrl('https://tunnel.example.com');
|
|
34
|
+
const url = agent.getFullUrl(true);
|
|
35
|
+
expect(url).toBe('https://user:pass@tunnel.example.com');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('detects proxy from X-Forwarded-Host in SWML request', async () => {
|
|
39
|
+
const app = agent.getApp();
|
|
40
|
+
const res = await app.request('http://localhost/', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'X-Forwarded-Host': 'ngrok.example.com',
|
|
46
|
+
'X-Forwarded-Proto': 'https',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({}),
|
|
49
|
+
});
|
|
50
|
+
expect(res.status).toBe(200);
|
|
51
|
+
// After request, the agent should have detected the proxy
|
|
52
|
+
expect(agent.getFullUrl()).toBe('https://ngrok.example.com');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('detects proxy from X-Forwarded-Host defaults to https', async () => {
|
|
56
|
+
const app = agent.getApp();
|
|
57
|
+
await app.request('http://localhost/', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'X-Forwarded-Host': 'tunnel.io',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({}),
|
|
65
|
+
});
|
|
66
|
+
expect(agent.getFullUrl()).toBe('https://tunnel.io');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detects proxy from Forwarded header (RFC 7239)', async () => {
|
|
70
|
+
const app = agent.getApp();
|
|
71
|
+
await app.request('http://localhost/', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
'Forwarded': 'host=proxy.example.com;proto=https',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({}),
|
|
79
|
+
});
|
|
80
|
+
expect(agent.getFullUrl()).toBe('https://proxy.example.com');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('detects proxy from Forwarded header with default proto', async () => {
|
|
84
|
+
const app = agent.getApp();
|
|
85
|
+
await app.request('http://localhost/', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
'Forwarded': 'host=proxy.example.com',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({}),
|
|
93
|
+
});
|
|
94
|
+
expect(agent.getFullUrl()).toBe('https://proxy.example.com');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('detects proxy from X-Original-Host', async () => {
|
|
98
|
+
const app = agent.getApp();
|
|
99
|
+
await app.request('http://localhost/', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
'X-Original-Host': 'original.example.com',
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({}),
|
|
107
|
+
});
|
|
108
|
+
expect(agent.getFullUrl()).toBe('https://original.example.com');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('X-Forwarded-Host takes priority over Forwarded header', async () => {
|
|
112
|
+
const app = agent.getApp();
|
|
113
|
+
await app.request('http://localhost/', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
'X-Forwarded-Host': 'xfh.example.com',
|
|
119
|
+
'X-Forwarded-Proto': 'https',
|
|
120
|
+
'Forwarded': 'host=fwd.example.com;proto=https',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({}),
|
|
123
|
+
});
|
|
124
|
+
expect(agent.getFullUrl()).toBe('https://xfh.example.com');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('env var SWML_PROXY_URL_BASE takes priority and is never overridden', async () => {
|
|
128
|
+
process.env['SWML_PROXY_URL_BASE'] = 'https://env-proxy.example.com';
|
|
129
|
+
const envAgent = new AgentBase({ name: 'env-test', route: '/', basicAuth: ['user', 'pass'] });
|
|
130
|
+
envAgent.setPromptText('test');
|
|
131
|
+
const app = envAgent.getApp();
|
|
132
|
+
await app.request('http://localhost/', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'X-Forwarded-Host': 'ngrok.example.com',
|
|
138
|
+
'X-Forwarded-Proto': 'https',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({}),
|
|
141
|
+
});
|
|
142
|
+
// Should still use env var, not detected proxy
|
|
143
|
+
expect(envAgent.getFullUrl()).toBe('https://env-proxy.example.com');
|
|
144
|
+
delete process.env['SWML_PROXY_URL_BASE'];
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('proxy URL is used in rendered SWML webhook URLs', async () => {
|
|
148
|
+
agent.defineTool({
|
|
149
|
+
name: 'test_fn',
|
|
150
|
+
description: 'Test',
|
|
151
|
+
parameters: {},
|
|
152
|
+
handler: () => ({ response: 'ok' }),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const app = agent.getApp();
|
|
156
|
+
const res = await app.request('http://localhost/', {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'X-Forwarded-Host': 'webhook.example.com',
|
|
162
|
+
'X-Forwarded-Proto': 'https',
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({}),
|
|
165
|
+
});
|
|
166
|
+
const swml = await res.json();
|
|
167
|
+
const webhookUrl = swml.sections.main[1].ai.SWAIG.defaults.web_hook_url;
|
|
168
|
+
expect(webhookUrl).toContain('webhook.example.com');
|
|
169
|
+
expect(webhookUrl).toContain('https://');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('no proxy headers leaves getFullUrl unchanged', async () => {
|
|
173
|
+
const beforeUrl = agent.getFullUrl();
|
|
174
|
+
const app = agent.getApp();
|
|
175
|
+
await app.request('http://localhost/', {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: {
|
|
178
|
+
'Authorization': 'Basic ' + btoa('user:pass'),
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify({}),
|
|
182
|
+
});
|
|
183
|
+
expect(agent.getFullUrl()).toBe(beforeUrl);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SchemaUtils } from '../src/SchemaUtils.js';
|
|
3
|
+
|
|
4
|
+
describe('SchemaUtils', () => {
|
|
5
|
+
it('validates a minimal valid SWML document', () => {
|
|
6
|
+
const validator = new SchemaUtils();
|
|
7
|
+
const result = validator.validate({
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
sections: { main: [] },
|
|
10
|
+
});
|
|
11
|
+
expect(result.valid).toBe(true);
|
|
12
|
+
expect(result.errors).toHaveLength(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects missing version', () => {
|
|
16
|
+
const validator = new SchemaUtils();
|
|
17
|
+
const result = validator.validate({ sections: { main: [] } });
|
|
18
|
+
expect(result.valid).toBe(false);
|
|
19
|
+
expect(result.errors.some(e => e.includes('version'))).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('rejects missing sections', () => {
|
|
23
|
+
const validator = new SchemaUtils();
|
|
24
|
+
const result = validator.validate({ version: '1.0.0' });
|
|
25
|
+
expect(result.valid).toBe(false);
|
|
26
|
+
expect(result.errors.some(e => e.includes('sections'))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('rejects invalid version', () => {
|
|
30
|
+
const validator = new SchemaUtils();
|
|
31
|
+
const result = validator.validate({
|
|
32
|
+
version: '2.0.0',
|
|
33
|
+
sections: { main: [] },
|
|
34
|
+
});
|
|
35
|
+
expect(result.valid).toBe(false);
|
|
36
|
+
expect(result.errors.some(e => e.includes('Invalid version'))).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects missing main section', () => {
|
|
40
|
+
const validator = new SchemaUtils();
|
|
41
|
+
const result = validator.validate({
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
sections: { other: [] },
|
|
44
|
+
});
|
|
45
|
+
expect(result.valid).toBe(false);
|
|
46
|
+
expect(result.errors.some(e => e.includes('main'))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('validates SWML from JSON string', () => {
|
|
50
|
+
const validator = new SchemaUtils();
|
|
51
|
+
const result = validator.validate(JSON.stringify({
|
|
52
|
+
version: '1.0.0',
|
|
53
|
+
sections: { main: [{ answer: {} }] },
|
|
54
|
+
}));
|
|
55
|
+
expect(result.valid).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects invalid JSON string', () => {
|
|
59
|
+
const validator = new SchemaUtils();
|
|
60
|
+
const result = validator.validate('not json');
|
|
61
|
+
expect(result.valid).toBe(false);
|
|
62
|
+
expect(result.errors).toContain('Invalid JSON');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('validates AI verb with prompt', () => {
|
|
66
|
+
const validator = new SchemaUtils();
|
|
67
|
+
const result = validator.validate({
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
sections: { main: [{ ai: { prompt: { text: 'Hello' } } }] },
|
|
70
|
+
});
|
|
71
|
+
expect(result.valid).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects AI verb with empty prompt', () => {
|
|
75
|
+
const validator = new SchemaUtils();
|
|
76
|
+
const result = validator.validate({
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
sections: { main: [{ ai: { prompt: {} } }] },
|
|
79
|
+
});
|
|
80
|
+
expect(result.valid).toBe(false);
|
|
81
|
+
expect(result.errors.some(e => e.includes('prompt'))).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('caches validation results', () => {
|
|
85
|
+
const validator = new SchemaUtils();
|
|
86
|
+
const doc = JSON.stringify({ version: '1.0.0', sections: { main: [] } });
|
|
87
|
+
validator.validate(doc);
|
|
88
|
+
expect(validator.getCacheSize()).toBe(1);
|
|
89
|
+
validator.validate(doc); // should hit cache
|
|
90
|
+
expect(validator.getCacheSize()).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('clearCache empties the cache', () => {
|
|
94
|
+
const validator = new SchemaUtils();
|
|
95
|
+
validator.validate({ version: '1.0.0', sections: { main: [] } });
|
|
96
|
+
expect(validator.getCacheSize()).toBe(1);
|
|
97
|
+
validator.clearCache();
|
|
98
|
+
expect(validator.getCacheSize()).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('skipValidation always returns valid', () => {
|
|
102
|
+
const validator = new SchemaUtils({ skipValidation: true });
|
|
103
|
+
const result = validator.validate({ invalid: true } as any);
|
|
104
|
+
expect(result.valid).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|