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,693 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { AgentBase } from '../src/AgentBase.js';
|
|
3
|
+
import { SwaigFunctionResult } from '../src/SwaigFunctionResult.js';
|
|
4
|
+
import { ContextBuilder } from '../src/ContextBuilder.js';
|
|
5
|
+
import { DataMap } from '../src/DataMap.js';
|
|
6
|
+
import { SkillBase, type SkillManifest, type SkillToolDefinition } from '../src/skills/SkillBase.js';
|
|
7
|
+
|
|
8
|
+
describe('AgentBase', () => {
|
|
9
|
+
function createAgent(opts?: Partial<Parameters<typeof AgentBase.prototype.setPromptText>[0]>) {
|
|
10
|
+
return new AgentBase({ name: 'test-agent', route: '/test', ...opts } as any);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
it('renders basic SWML with prompt', () => {
|
|
14
|
+
const agent = createAgent();
|
|
15
|
+
agent.setPromptText('You are a helpful assistant');
|
|
16
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
17
|
+
expect(swml.version).toBe('1.0.0');
|
|
18
|
+
const main = swml.sections.main;
|
|
19
|
+
// Should have answer + ai verbs
|
|
20
|
+
expect(main.length).toBe(2);
|
|
21
|
+
expect(main[0]).toHaveProperty('answer');
|
|
22
|
+
expect(main[1]).toHaveProperty('ai');
|
|
23
|
+
expect(main[1].ai.prompt.text).toBe('You are a helpful assistant');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders SWML without auto-answer', () => {
|
|
27
|
+
const agent = new AgentBase({ name: 'test', route: '/test', autoAnswer: false });
|
|
28
|
+
agent.setPromptText('hello');
|
|
29
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
30
|
+
const main = swml.sections.main;
|
|
31
|
+
// Should only have AI verb, no answer
|
|
32
|
+
expect(main.length).toBe(1);
|
|
33
|
+
expect(main[0]).toHaveProperty('ai');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders SWML with recording enabled', () => {
|
|
37
|
+
const agent = new AgentBase({ name: 'test', route: '/test', recordCall: true });
|
|
38
|
+
agent.setPromptText('hello');
|
|
39
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
40
|
+
const main = swml.sections.main;
|
|
41
|
+
// answer + record_call + ai
|
|
42
|
+
expect(main.length).toBe(3);
|
|
43
|
+
expect(main[1]).toHaveProperty('record_call');
|
|
44
|
+
expect(main[1].record_call.format).toBe('mp4');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('includes tools in SWAIG', () => {
|
|
48
|
+
const agent = createAgent();
|
|
49
|
+
agent.setPromptText('hello');
|
|
50
|
+
agent.defineTool({
|
|
51
|
+
name: 'get_time',
|
|
52
|
+
description: 'Get the current time',
|
|
53
|
+
parameters: {},
|
|
54
|
+
handler: () => new SwaigFunctionResult('The time is 12:00'),
|
|
55
|
+
});
|
|
56
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
57
|
+
const ai = swml.sections.main[1].ai;
|
|
58
|
+
expect(ai.SWAIG).toBeDefined();
|
|
59
|
+
expect(ai.SWAIG.functions.length).toBe(1);
|
|
60
|
+
expect(ai.SWAIG.functions[0].function).toBe('get_time');
|
|
61
|
+
expect(ai.SWAIG.defaults).toBeDefined();
|
|
62
|
+
expect(ai.SWAIG.defaults.web_hook_url).toContain('/swaig');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('includes DataMap tools in SWAIG', () => {
|
|
66
|
+
const agent = createAgent();
|
|
67
|
+
agent.setPromptText('hello');
|
|
68
|
+
const dm = new DataMap('get_weather')
|
|
69
|
+
.purpose('Get weather')
|
|
70
|
+
.parameter('location', 'string', 'City', { required: true })
|
|
71
|
+
.webhook('GET', 'https://api.weather.com?q=${location}')
|
|
72
|
+
.output(new SwaigFunctionResult('Temp: ${response.temp}'));
|
|
73
|
+
|
|
74
|
+
agent.registerSwaigFunction(dm.toSwaigFunction());
|
|
75
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
76
|
+
const fns = swml.sections.main[1].ai.SWAIG.functions;
|
|
77
|
+
expect(fns.length).toBe(1);
|
|
78
|
+
expect(fns[0].function).toBe('get_weather');
|
|
79
|
+
expect(fns[0].data_map).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('sets post prompt', () => {
|
|
83
|
+
const agent = createAgent();
|
|
84
|
+
agent.setPromptText('hello');
|
|
85
|
+
agent.setPostPrompt('Summarize the call');
|
|
86
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
87
|
+
const ai = swml.sections.main[1].ai;
|
|
88
|
+
expect(ai.post_prompt.text).toBe('Summarize the call');
|
|
89
|
+
expect(ai.post_prompt_url).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('adds hints, languages, pronunciation', () => {
|
|
93
|
+
const agent = createAgent();
|
|
94
|
+
agent.setPromptText('hello');
|
|
95
|
+
agent.addHints(['Cabby', 'SignalWire']);
|
|
96
|
+
agent.addLanguage({ name: 'English', code: 'en-US', voice: 'rachel' });
|
|
97
|
+
agent.addPronunciation({ replace: 'SW', with: 'SignalWire' });
|
|
98
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
99
|
+
const ai = swml.sections.main[1].ai;
|
|
100
|
+
expect(ai.hints).toEqual(['Cabby', 'SignalWire']);
|
|
101
|
+
expect(ai.languages.length).toBe(1);
|
|
102
|
+
expect(ai.pronounce.length).toBe(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('sets params and global data', () => {
|
|
106
|
+
const agent = createAgent();
|
|
107
|
+
agent.setPromptText('hello');
|
|
108
|
+
agent.setParam('temperature', 0.5);
|
|
109
|
+
agent.setGlobalData({ name: 'John' });
|
|
110
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
111
|
+
const ai = swml.sections.main[1].ai;
|
|
112
|
+
expect(ai.params.temperature).toBe(0.5);
|
|
113
|
+
expect(ai.global_data.name).toBe('John');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('setPromptLlmParams merges into prompt', () => {
|
|
117
|
+
const agent = createAgent();
|
|
118
|
+
agent.setPromptText('hello');
|
|
119
|
+
agent.setPromptLlmParams({ temperature: 0.3, top_p: 0.9 });
|
|
120
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
121
|
+
const prompt = swml.sections.main[1].ai.prompt;
|
|
122
|
+
expect(prompt.temperature).toBe(0.3);
|
|
123
|
+
expect(prompt.top_p).toBe(0.9);
|
|
124
|
+
expect(prompt.text).toBe('hello');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('pre-answer, post-answer, post-ai verbs', () => {
|
|
128
|
+
const agent = createAgent();
|
|
129
|
+
agent.setPromptText('hello');
|
|
130
|
+
agent.addPreAnswerVerb('play', { urls: ['ring:us'], auto_answer: false });
|
|
131
|
+
agent.addPostAnswerVerb('play', { url: 'say:Welcome!' });
|
|
132
|
+
agent.addPostAiVerb('hangup', {});
|
|
133
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
134
|
+
const main = swml.sections.main;
|
|
135
|
+
// pre-answer play + answer + post-answer play + ai + post-ai hangup
|
|
136
|
+
expect(main.length).toBe(5);
|
|
137
|
+
expect(main[0]).toHaveProperty('play');
|
|
138
|
+
expect(main[1]).toHaveProperty('answer');
|
|
139
|
+
expect(main[2]).toHaveProperty('play');
|
|
140
|
+
expect(main[3]).toHaveProperty('ai');
|
|
141
|
+
expect(main[4]).toHaveProperty('hangup');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('native functions included in SWAIG', () => {
|
|
145
|
+
const agent = new AgentBase({ name: 'test', route: '/test', nativeFunctions: ['check_time'] });
|
|
146
|
+
agent.setPromptText('hello');
|
|
147
|
+
agent.defineTool({
|
|
148
|
+
name: 'fn',
|
|
149
|
+
description: 'd',
|
|
150
|
+
parameters: {},
|
|
151
|
+
handler: () => new SwaigFunctionResult('ok'),
|
|
152
|
+
});
|
|
153
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
154
|
+
expect(swml.sections.main[1].ai.SWAIG.native_functions).toEqual(['check_time']);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('function includes', () => {
|
|
158
|
+
const agent = createAgent();
|
|
159
|
+
agent.setPromptText('hello');
|
|
160
|
+
agent.addFunctionInclude('https://example.com/fns', ['fn1']);
|
|
161
|
+
agent.defineTool({
|
|
162
|
+
name: 'fn',
|
|
163
|
+
description: 'd',
|
|
164
|
+
parameters: {},
|
|
165
|
+
handler: () => new SwaigFunctionResult('ok'),
|
|
166
|
+
});
|
|
167
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
168
|
+
expect(swml.sections.main[1].ai.SWAIG.includes).toHaveLength(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('contexts system', () => {
|
|
172
|
+
const agent = createAgent();
|
|
173
|
+
agent.setPromptText('You are a quiz master');
|
|
174
|
+
const ctx = agent.defineContexts();
|
|
175
|
+
const def = ctx.addContext('default');
|
|
176
|
+
def.addStep('greeting', { task: 'Greet the user' })
|
|
177
|
+
.setStepCriteria('User has been greeted')
|
|
178
|
+
.setValidSteps(['quiz']);
|
|
179
|
+
def.addStep('quiz', { task: 'Ask a quiz question' });
|
|
180
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
181
|
+
const ai = swml.sections.main[1].ai;
|
|
182
|
+
expect(ai.contexts).toBeDefined();
|
|
183
|
+
expect(ai.contexts.default.steps.length).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('getFullUrl returns correct URL', () => {
|
|
187
|
+
const agent = new AgentBase({ name: 'test', route: '/myagent', port: 5000 });
|
|
188
|
+
const url = agent.getFullUrl();
|
|
189
|
+
expect(url).toContain('localhost:5000/myagent');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('getFullUrl with auth', () => {
|
|
193
|
+
const agent = new AgentBase({ name: 'test', route: '/test', basicAuth: ['user', 'pass'] });
|
|
194
|
+
const url = agent.getFullUrl(true);
|
|
195
|
+
expect(url).toContain('user:pass@');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('getBasicAuthCredentials', () => {
|
|
199
|
+
const agent = new AgentBase({ name: 'test', route: '/test', basicAuth: ['u', 'p'] });
|
|
200
|
+
expect(agent.getBasicAuthCredentials()).toEqual(['u', 'p']);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('promptAddSection builds POM prompt', () => {
|
|
204
|
+
const agent = createAgent();
|
|
205
|
+
agent.promptAddSection('Role', { body: 'You are a helper' });
|
|
206
|
+
agent.promptAddSection('Rules', { bullets: ['Be nice'] });
|
|
207
|
+
const prompt = agent.getPrompt();
|
|
208
|
+
expect(prompt).toContain('Role');
|
|
209
|
+
expect(prompt).toContain('You are a helper');
|
|
210
|
+
expect(prompt).toContain('- Be nice');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('renders multiple calls independently', () => {
|
|
214
|
+
const agent = createAgent();
|
|
215
|
+
agent.setPromptText('hello');
|
|
216
|
+
const swml1 = agent.renderSwml();
|
|
217
|
+
const swml2 = agent.renderSwml();
|
|
218
|
+
// Both should be valid JSON
|
|
219
|
+
expect(JSON.parse(swml1).version).toBe('1.0.0');
|
|
220
|
+
expect(JSON.parse(swml2).version).toBe('1.0.0');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── Auth source tracking ────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
it('getBasicAuthCredentials returns source=provided when given explicitly', () => {
|
|
226
|
+
const agent = new AgentBase({ name: 'test', route: '/test', basicAuth: ['u', 'p'] });
|
|
227
|
+
const [user, pass, source] = agent.getBasicAuthCredentials(true);
|
|
228
|
+
expect(user).toBe('u');
|
|
229
|
+
expect(pass).toBe('p');
|
|
230
|
+
expect(source).toBe('provided');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('getBasicAuthCredentials returns source=generated when auto-generated', () => {
|
|
234
|
+
const savedUser = process.env['SWML_BASIC_AUTH_USER'];
|
|
235
|
+
const savedPass = process.env['SWML_BASIC_AUTH_PASSWORD'];
|
|
236
|
+
delete process.env['SWML_BASIC_AUTH_USER'];
|
|
237
|
+
delete process.env['SWML_BASIC_AUTH_PASSWORD'];
|
|
238
|
+
try {
|
|
239
|
+
const agent = new AgentBase({ name: 'test', route: '/test' });
|
|
240
|
+
const [user, , source] = agent.getBasicAuthCredentials(true);
|
|
241
|
+
expect(user).toBe('test');
|
|
242
|
+
expect(source).toBe('generated');
|
|
243
|
+
} finally {
|
|
244
|
+
if (savedUser) process.env['SWML_BASIC_AUTH_USER'] = savedUser;
|
|
245
|
+
if (savedPass) process.env['SWML_BASIC_AUTH_PASSWORD'] = savedPass;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('getBasicAuthCredentials without source returns tuple of 2', () => {
|
|
250
|
+
const agent = new AgentBase({ name: 'test', route: '/test', basicAuth: ['u', 'p'] });
|
|
251
|
+
const result = agent.getBasicAuthCredentials();
|
|
252
|
+
expect(result).toHaveLength(2);
|
|
253
|
+
expect(result).toEqual(['u', 'p']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── Security headers ───────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
it('includes security headers in responses', async () => {
|
|
259
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
260
|
+
agent.setPromptText('hello');
|
|
261
|
+
const app = agent.getApp();
|
|
262
|
+
const res = await app.request('/health');
|
|
263
|
+
expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
|
|
264
|
+
expect(res.headers.get('X-Frame-Options')).toBe('DENY');
|
|
265
|
+
expect(res.headers.get('X-XSS-Protection')).toBe('1; mode=block');
|
|
266
|
+
expect(res.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── CORS ──────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
it('responds to CORS preflight', async () => {
|
|
272
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
273
|
+
const app = agent.getApp();
|
|
274
|
+
const res = await app.request('/health', {
|
|
275
|
+
method: 'OPTIONS',
|
|
276
|
+
headers: {
|
|
277
|
+
'Origin': 'https://example.com',
|
|
278
|
+
'Access-Control-Request-Method': 'POST',
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Debug events ──────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
it('enableDebugEvents injects debug_webhook_url in SWML', () => {
|
|
287
|
+
const agent = createAgent();
|
|
288
|
+
agent.setPromptText('hello');
|
|
289
|
+
agent.enableDebugEvents(2);
|
|
290
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
291
|
+
const ai = swml.sections.main[1].ai;
|
|
292
|
+
expect(ai.debug_webhook_url).toBeDefined();
|
|
293
|
+
expect(ai.debug_webhook_url).toContain('/debug_events');
|
|
294
|
+
expect(ai.debug_webhook_level).toBe(2);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('debug events endpoint responds to POST', async () => {
|
|
298
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
299
|
+
agent.enableDebugEvents();
|
|
300
|
+
const app = agent.getApp();
|
|
301
|
+
const res = await app.request('/debug_events', {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: {
|
|
304
|
+
'Authorization': 'Basic ' + btoa('u:p'),
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify({ event: 'test' }),
|
|
308
|
+
});
|
|
309
|
+
expect(res.status).toBe(200);
|
|
310
|
+
const body = await res.json();
|
|
311
|
+
expect(body.ok).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('no debug_webhook_url when debug events not enabled', () => {
|
|
315
|
+
const agent = createAgent();
|
|
316
|
+
agent.setPromptText('hello');
|
|
317
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
318
|
+
const ai = swml.sections.main[1].ai;
|
|
319
|
+
expect(ai.debug_webhook_url).toBeUndefined();
|
|
320
|
+
expect(ai.debug_webhook_level).toBeUndefined();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ── Tool introspection ──────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
it('getRegisteredTools lists all tools', () => {
|
|
326
|
+
const agent = createAgent();
|
|
327
|
+
agent.defineTool({
|
|
328
|
+
name: 'fn1',
|
|
329
|
+
description: 'Function 1',
|
|
330
|
+
parameters: { x: { type: 'string' } },
|
|
331
|
+
handler: () => new SwaigFunctionResult('ok'),
|
|
332
|
+
});
|
|
333
|
+
agent.defineTool({
|
|
334
|
+
name: 'fn2',
|
|
335
|
+
description: 'Function 2',
|
|
336
|
+
parameters: {},
|
|
337
|
+
handler: () => new SwaigFunctionResult('ok'),
|
|
338
|
+
});
|
|
339
|
+
const tools = agent.getRegisteredTools();
|
|
340
|
+
expect(tools).toHaveLength(2);
|
|
341
|
+
expect(tools[0].name).toBe('fn1');
|
|
342
|
+
expect(tools[1].name).toBe('fn2');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('getTool returns a SwaigFunction', () => {
|
|
346
|
+
const agent = createAgent();
|
|
347
|
+
agent.defineTool({
|
|
348
|
+
name: 'my_fn',
|
|
349
|
+
description: 'My function',
|
|
350
|
+
parameters: {},
|
|
351
|
+
handler: () => new SwaigFunctionResult('ok'),
|
|
352
|
+
});
|
|
353
|
+
const tool = agent.getTool('my_fn');
|
|
354
|
+
expect(tool).toBeDefined();
|
|
355
|
+
expect(tool!.name).toBe('my_fn');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('getTool returns undefined for missing tool', () => {
|
|
359
|
+
const agent = createAgent();
|
|
360
|
+
expect(agent.getTool('nope')).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ── Pass 1: Agent Enhancements ────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
it('getName returns the agent name', () => {
|
|
366
|
+
const agent = new AgentBase({ name: 'my-agent', route: '/test' });
|
|
367
|
+
expect(agent.getName()).toBe('my-agent');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('agentId defaults to random hex', () => {
|
|
371
|
+
const agent = createAgent();
|
|
372
|
+
expect(agent.agentId).toBeDefined();
|
|
373
|
+
expect(typeof agent.agentId).toBe('string');
|
|
374
|
+
expect(agent.agentId.length).toBe(16); // 8 bytes hex
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('agentId can be provided via options', () => {
|
|
378
|
+
const agent = new AgentBase({ name: 'test', route: '/test', agentId: 'custom-id-123' } as any);
|
|
379
|
+
expect(agent.agentId).toBe('custom-id-123');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('clearPreAnswerVerbs removes pre-answer verbs', () => {
|
|
383
|
+
const agent = createAgent();
|
|
384
|
+
agent.setPromptText('hello');
|
|
385
|
+
agent.addPreAnswerVerb('play', { url: 'ring:us' });
|
|
386
|
+
agent.addPreAnswerVerb('play', { url: 'ring:uk' });
|
|
387
|
+
agent.clearPreAnswerVerbs();
|
|
388
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
389
|
+
const main = swml.sections.main;
|
|
390
|
+
// Should only have answer + ai, no pre-answer verbs
|
|
391
|
+
expect(main.length).toBe(2);
|
|
392
|
+
expect(main[0]).toHaveProperty('answer');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('clearPostAnswerVerbs removes post-answer verbs', () => {
|
|
396
|
+
const agent = createAgent();
|
|
397
|
+
agent.setPromptText('hello');
|
|
398
|
+
agent.addPostAnswerVerb('play', { url: 'say:Welcome' });
|
|
399
|
+
agent.clearPostAnswerVerbs();
|
|
400
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
401
|
+
const main = swml.sections.main;
|
|
402
|
+
expect(main.length).toBe(2);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('clearPostAiVerbs removes post-ai verbs', () => {
|
|
406
|
+
const agent = createAgent();
|
|
407
|
+
agent.setPromptText('hello');
|
|
408
|
+
agent.addPostAiVerb('hangup', {});
|
|
409
|
+
agent.clearPostAiVerbs();
|
|
410
|
+
const swml = JSON.parse(agent.renderSwml());
|
|
411
|
+
const main = swml.sections.main;
|
|
412
|
+
expect(main.length).toBe(2);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('clear*Verbs methods return this for chaining', () => {
|
|
416
|
+
const agent = createAgent();
|
|
417
|
+
const result = agent.clearPreAnswerVerbs().clearPostAnswerVerbs().clearPostAiVerbs();
|
|
418
|
+
expect(result).toBe(agent);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('suppressLogs option suppresses log output', async () => {
|
|
422
|
+
// We test that no error is thrown; the actual suppression is tested in Logger tests
|
|
423
|
+
const { suppressAllLogs: suppress } = await import('../src/Logger.js');
|
|
424
|
+
suppress(false); // reset
|
|
425
|
+
const agent = new AgentBase({ name: 'test', route: '/test', suppressLogs: true } as any);
|
|
426
|
+
expect(agent).toBeDefined();
|
|
427
|
+
suppress(false); // cleanup
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// ── Security env vars ─────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
it('request size limit returns 413 for oversized requests', async () => {
|
|
433
|
+
const saved = process.env['SWML_MAX_REQUEST_SIZE'];
|
|
434
|
+
process.env['SWML_MAX_REQUEST_SIZE'] = '50'; // 50 bytes
|
|
435
|
+
try {
|
|
436
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
437
|
+
agent.setPromptText('hello');
|
|
438
|
+
const app = agent.getApp();
|
|
439
|
+
const res = await app.request('/', {
|
|
440
|
+
method: 'POST',
|
|
441
|
+
headers: {
|
|
442
|
+
'Authorization': 'Basic ' + btoa('u:p'),
|
|
443
|
+
'Content-Type': 'application/json',
|
|
444
|
+
'Content-Length': '100000',
|
|
445
|
+
},
|
|
446
|
+
body: JSON.stringify({ data: 'x'.repeat(100000) }),
|
|
447
|
+
});
|
|
448
|
+
expect(res.status).toBe(413);
|
|
449
|
+
} finally {
|
|
450
|
+
if (saved) process.env['SWML_MAX_REQUEST_SIZE'] = saved;
|
|
451
|
+
else delete process.env['SWML_MAX_REQUEST_SIZE'];
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('SWML_ALLOWED_HOSTS blocks disallowed hosts', async () => {
|
|
456
|
+
const saved = process.env['SWML_ALLOWED_HOSTS'];
|
|
457
|
+
process.env['SWML_ALLOWED_HOSTS'] = 'allowed.example.com, other.test';
|
|
458
|
+
try {
|
|
459
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
460
|
+
agent.setPromptText('hello');
|
|
461
|
+
const app = agent.getApp();
|
|
462
|
+
|
|
463
|
+
// Disallowed host
|
|
464
|
+
const res = await app.request('http://evil.example.com/', {
|
|
465
|
+
method: 'POST',
|
|
466
|
+
headers: {
|
|
467
|
+
'Authorization': 'Basic ' + btoa('u:p'),
|
|
468
|
+
'Content-Type': 'application/json',
|
|
469
|
+
'Host': 'evil.example.com',
|
|
470
|
+
},
|
|
471
|
+
body: '{}',
|
|
472
|
+
});
|
|
473
|
+
expect(res.status).toBe(403);
|
|
474
|
+
const body = await res.json();
|
|
475
|
+
expect(body.error).toContain('host not allowed');
|
|
476
|
+
} finally {
|
|
477
|
+
if (saved) process.env['SWML_ALLOWED_HOSTS'] = saved;
|
|
478
|
+
else delete process.env['SWML_ALLOWED_HOSTS'];
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('SWML_ALLOWED_HOSTS allows matching hosts', async () => {
|
|
483
|
+
const saved = process.env['SWML_ALLOWED_HOSTS'];
|
|
484
|
+
process.env['SWML_ALLOWED_HOSTS'] = 'allowed.example.com, other.test';
|
|
485
|
+
try {
|
|
486
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
487
|
+
agent.setPromptText('hello');
|
|
488
|
+
const app = agent.getApp();
|
|
489
|
+
|
|
490
|
+
// Allowed host
|
|
491
|
+
const res = await app.request('http://allowed.example.com/', {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: {
|
|
494
|
+
'Authorization': 'Basic ' + btoa('u:p'),
|
|
495
|
+
'Content-Type': 'application/json',
|
|
496
|
+
'Host': 'allowed.example.com',
|
|
497
|
+
},
|
|
498
|
+
body: '{}',
|
|
499
|
+
});
|
|
500
|
+
expect(res.status).toBe(200);
|
|
501
|
+
} finally {
|
|
502
|
+
if (saved) process.env['SWML_ALLOWED_HOSTS'] = saved;
|
|
503
|
+
else delete process.env['SWML_ALLOWED_HOSTS'];
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('SWML_RATE_LIMIT returns 429 when exceeded', async () => {
|
|
508
|
+
const saved = process.env['SWML_RATE_LIMIT'];
|
|
509
|
+
process.env['SWML_RATE_LIMIT'] = '2'; // 2 requests per minute
|
|
510
|
+
try {
|
|
511
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
512
|
+
agent.setPromptText('hello');
|
|
513
|
+
const app = agent.getApp();
|
|
514
|
+
|
|
515
|
+
const makeReq = () => app.request('/', {
|
|
516
|
+
method: 'POST',
|
|
517
|
+
headers: {
|
|
518
|
+
'Authorization': 'Basic ' + btoa('u:p'),
|
|
519
|
+
'Content-Type': 'application/json',
|
|
520
|
+
'X-Forwarded-For': '1.2.3.4',
|
|
521
|
+
},
|
|
522
|
+
body: '{}',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// First two should succeed
|
|
526
|
+
const r1 = await makeReq();
|
|
527
|
+
expect(r1.status).toBe(200);
|
|
528
|
+
const r2 = await makeReq();
|
|
529
|
+
expect(r2.status).toBe(200);
|
|
530
|
+
|
|
531
|
+
// Third should be rate limited
|
|
532
|
+
const r3 = await makeReq();
|
|
533
|
+
expect(r3.status).toBe(429);
|
|
534
|
+
const body = await r3.json();
|
|
535
|
+
expect(body.error).toContain('Rate limit');
|
|
536
|
+
} finally {
|
|
537
|
+
if (saved) process.env['SWML_RATE_LIMIT'] = saved;
|
|
538
|
+
else delete process.env['SWML_RATE_LIMIT'];
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// ── Pass 4: Tool lifecycle + defineTools + onFunctionCall ───────
|
|
543
|
+
|
|
544
|
+
it('defineTools is not called automatically — subclass calls it explicitly', () => {
|
|
545
|
+
let defineToolsCalled = false;
|
|
546
|
+
class MyAgent extends AgentBase {
|
|
547
|
+
constructor(opts: Parameters<typeof AgentBase.prototype.constructor>[0]) {
|
|
548
|
+
super(opts as any);
|
|
549
|
+
this.defineTools();
|
|
550
|
+
}
|
|
551
|
+
protected override defineTools(): void {
|
|
552
|
+
defineToolsCalled = true;
|
|
553
|
+
this.defineTool({
|
|
554
|
+
name: 'auto_tool',
|
|
555
|
+
description: 'Auto-registered tool',
|
|
556
|
+
parameters: {},
|
|
557
|
+
handler: () => new SwaigFunctionResult('auto'),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const agent = new MyAgent({ name: 'test', route: '/test' });
|
|
562
|
+
expect(defineToolsCalled).toBe(true);
|
|
563
|
+
expect(agent.getTool('auto_tool')).toBeDefined();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('PROMPT_SECTIONS static property applies prompt sections', () => {
|
|
567
|
+
class PromptAgent extends AgentBase {
|
|
568
|
+
static PROMPT_SECTIONS = [
|
|
569
|
+
{ title: 'Role', body: 'You are a test agent' },
|
|
570
|
+
{ title: 'Rules', bullets: ['Be helpful', 'Be concise'] },
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
const agent = new PromptAgent({ name: 'test', route: '/test' });
|
|
574
|
+
const prompt = agent.getPrompt();
|
|
575
|
+
expect(prompt).toContain('Role');
|
|
576
|
+
expect(prompt).toContain('You are a test agent');
|
|
577
|
+
expect(prompt).toContain('Be helpful');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('onFunctionCall hook is invoked when SWAIG function called', async () => {
|
|
581
|
+
const calls: string[] = [];
|
|
582
|
+
class HookAgent extends AgentBase {
|
|
583
|
+
onFunctionCall(name: string, _args: Record<string, unknown>, _rawData: Record<string, unknown>): void {
|
|
584
|
+
calls.push(name);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const agent = new HookAgent({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
588
|
+
agent.defineTool({
|
|
589
|
+
name: 'test_fn',
|
|
590
|
+
description: 'Test',
|
|
591
|
+
parameters: {},
|
|
592
|
+
handler: () => new SwaigFunctionResult('ok'),
|
|
593
|
+
});
|
|
594
|
+
const app = agent.getApp();
|
|
595
|
+
const res = await app.request('/swaig', {
|
|
596
|
+
method: 'POST',
|
|
597
|
+
headers: {
|
|
598
|
+
'Authorization': 'Basic ' + btoa('u:p'),
|
|
599
|
+
'Content-Type': 'application/json',
|
|
600
|
+
},
|
|
601
|
+
body: JSON.stringify({ function: 'test_fn', argument: {} }),
|
|
602
|
+
});
|
|
603
|
+
expect(res.status).toBe(200);
|
|
604
|
+
expect(calls).toContain('test_fn');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('validateBasicAuth hook default returns true', () => {
|
|
608
|
+
const agent = createAgent();
|
|
609
|
+
expect(agent.validateBasicAuth('any', 'pass')).toBe(true);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('CORS origin defaults to *', async () => {
|
|
613
|
+
const saved = process.env['SWML_CORS_ORIGINS'];
|
|
614
|
+
delete process.env['SWML_CORS_ORIGINS'];
|
|
615
|
+
try {
|
|
616
|
+
const agent = new AgentBase({ name: 'test', route: '/', basicAuth: ['u', 'p'] });
|
|
617
|
+
const app = agent.getApp();
|
|
618
|
+
const res = await app.request('/health', {
|
|
619
|
+
method: 'OPTIONS',
|
|
620
|
+
headers: {
|
|
621
|
+
'Origin': 'https://example.com',
|
|
622
|
+
'Access-Control-Request-Method': 'POST',
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
|
|
626
|
+
} finally {
|
|
627
|
+
if (saved) process.env['SWML_CORS_ORIGINS'] = saved;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// ── Pass 5: Skills integration ──────────────────────────────────
|
|
632
|
+
|
|
633
|
+
it('addSkill registers skill tools and prompt sections', async () => {
|
|
634
|
+
class TestSkill extends SkillBase {
|
|
635
|
+
constructor() { super('test_skill'); }
|
|
636
|
+
getManifest(): SkillManifest { return { name: 'test_skill', description: 'Test', version: '1.0.0' }; }
|
|
637
|
+
getTools(): SkillToolDefinition[] {
|
|
638
|
+
return [{
|
|
639
|
+
name: 'skill_tool',
|
|
640
|
+
description: 'Skill tool',
|
|
641
|
+
handler: () => new SwaigFunctionResult('skill result'),
|
|
642
|
+
}];
|
|
643
|
+
}
|
|
644
|
+
getPromptSections() { return [{ title: 'Skill Info', body: 'From test skill' }]; }
|
|
645
|
+
getHints() { return ['skill hint']; }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const agent = createAgent();
|
|
649
|
+
agent.setPromptText('base prompt');
|
|
650
|
+
await agent.addSkill(new TestSkill());
|
|
651
|
+
|
|
652
|
+
expect(agent.getTool('skill_tool')).toBeDefined();
|
|
653
|
+
expect(agent.hasSkill('test_skill')).toBe(true);
|
|
654
|
+
expect(agent.listSkills()).toHaveLength(1);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('removeSkill removes a skill', async () => {
|
|
658
|
+
class RemoveSkill extends SkillBase {
|
|
659
|
+
constructor() { super('removable'); }
|
|
660
|
+
getManifest(): SkillManifest { return { name: 'removable', description: 'Test', version: '1.0.0' }; }
|
|
661
|
+
getTools(): SkillToolDefinition[] { return []; }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const agent = createAgent();
|
|
665
|
+
const skill = new RemoveSkill();
|
|
666
|
+
await agent.addSkill(skill);
|
|
667
|
+
expect(agent.hasSkill('removable')).toBe(true);
|
|
668
|
+
await agent.removeSkill(skill.instanceId);
|
|
669
|
+
expect(agent.hasSkill('removable')).toBe(false);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('addSkill returns this for chaining', async () => {
|
|
673
|
+
class ChainSkill extends SkillBase {
|
|
674
|
+
constructor() { super('chain'); }
|
|
675
|
+
getManifest(): SkillManifest { return { name: 'chain', description: 'Test', version: '1.0.0' }; }
|
|
676
|
+
getTools(): SkillToolDefinition[] { return []; }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const agent = createAgent();
|
|
680
|
+
const result = await agent.addSkill(new ChainSkill());
|
|
681
|
+
expect(result).toBe(agent);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('listSkills returns empty when no skills', () => {
|
|
685
|
+
const agent = createAgent();
|
|
686
|
+
expect(agent.listSkills()).toHaveLength(0);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('hasSkill returns false when no skills', () => {
|
|
690
|
+
const agent = createAgent();
|
|
691
|
+
expect(agent.hasSkill('nope')).toBe(false);
|
|
692
|
+
});
|
|
693
|
+
});
|