@useconductor/conductor 1.0.0 → 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.
- package/.github/README.md +374 -7
- package/.github/workflows/ci.yml +3 -1
- package/.github/workflows/claude-code-review.yml +1 -15
- package/.github/workflows/publish.yml +43 -0
- package/README.md +290 -121
- package/dist/cli/commands/audit.d.ts +40 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +272 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/circuit.d.ts +13 -0
- package/dist/cli/commands/circuit.d.ts.map +1 -0
- package/dist/cli/commands/circuit.js +53 -0
- package/dist/cli/commands/circuit.js.map +1 -0
- package/dist/cli/commands/config.d.ts +31 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +152 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/init.d.ts +5 -8
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +86 -123
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/marketplace.js +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +33 -11
- package/dist/cli/commands/onboard.js.map +1 -1
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +1 -1
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/index.js +146 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/audit.js +5 -2
- package/dist/core/audit.js.map +1 -1
- package/dist/core/conductor.d.ts.map +1 -1
- package/dist/core/conductor.js +12 -0
- package/dist/core/conductor.js.map +1 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +46 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/database.d.ts +3 -0
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +26 -0
- package/dist/core/database.js.map +1 -1
- package/dist/core/encryption.d.ts +34 -0
- package/dist/core/encryption.d.ts.map +1 -0
- package/dist/core/encryption.js +96 -0
- package/dist/core/encryption.js.map +1 -0
- package/dist/core/zero-config.d.ts.map +1 -1
- package/dist/core/zero-config.js +1 -4
- package/dist/core/zero-config.js.map +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +112 -16
- package/dist/dashboard/server.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +30 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/plugins/builtin/aws.d.ts +31 -0
- package/dist/plugins/builtin/aws.d.ts.map +1 -0
- package/dist/plugins/builtin/aws.js +149 -0
- package/dist/plugins/builtin/aws.js.map +1 -0
- package/dist/plugins/builtin/database.d.ts +1 -0
- package/dist/plugins/builtin/database.d.ts.map +1 -1
- package/dist/plugins/builtin/database.js +26 -1
- package/dist/plugins/builtin/database.js.map +1 -1
- package/dist/plugins/builtin/docker.d.ts +4 -0
- package/dist/plugins/builtin/docker.d.ts.map +1 -1
- package/dist/plugins/builtin/docker.js +20 -1
- package/dist/plugins/builtin/docker.js.map +1 -1
- package/dist/plugins/builtin/gcp.d.ts +28 -0
- package/dist/plugins/builtin/gcp.d.ts.map +1 -0
- package/dist/plugins/builtin/gcp.js +135 -0
- package/dist/plugins/builtin/gcp.js.map +1 -0
- package/dist/plugins/builtin/index.d.ts.map +1 -1
- package/dist/plugins/builtin/index.js +4 -0
- package/dist/plugins/builtin/index.js.map +1 -1
- package/dist/plugins/builtin/jira.d.ts.map +1 -1
- package/dist/plugins/builtin/jira.js +4 -2
- package/dist/plugins/builtin/jira.js.map +1 -1
- package/dist/plugins/builtin/linear.js +1 -1
- package/dist/plugins/builtin/linear.js.map +1 -1
- package/dist/plugins/builtin/shell.js +1 -1
- package/dist/plugins/builtin/shell.js.map +1 -1
- package/dist/plugins/builtin/slack.d.ts +1 -0
- package/dist/plugins/builtin/slack.d.ts.map +1 -1
- package/dist/plugins/builtin/slack.js +9 -1
- package/dist/plugins/builtin/slack.js.map +1 -1
- package/dist/plugins/builtin/spotify.js +1 -1
- package/dist/plugins/builtin/spotify.js.map +1 -1
- package/dist/plugins/builtin/vercel.d.ts.map +1 -1
- package/dist/plugins/builtin/vercel.js +3 -1
- package/dist/plugins/builtin/vercel.js.map +1 -1
- package/dist/security/sso.d.ts +37 -0
- package/dist/security/sso.d.ts.map +1 -0
- package/dist/security/sso.js +92 -0
- package/dist/security/sso.js.map +1 -0
- package/docs/deployment.md +201 -0
- package/docs/plugin-sdk.md +212 -0
- package/package.json +11 -8
- package/src/cli/commands/audit.ts +318 -0
- package/src/cli/commands/circuit.ts +63 -0
- package/src/cli/commands/config.ts +176 -0
- package/src/cli/commands/init.ts +87 -145
- package/src/cli/commands/marketplace.ts +1 -1
- package/src/cli/commands/onboard.ts +33 -11
- package/src/cli/commands/release.ts +13 -6
- package/src/cli/index.ts +165 -11
- package/src/core/audit.ts +5 -2
- package/src/core/conductor.ts +11 -0
- package/src/core/config.ts +47 -2
- package/src/core/database.ts +32 -0
- package/src/core/encryption.ts +110 -0
- package/src/core/zero-config.ts +1 -5
- package/src/dashboard/server.ts +135 -16
- package/src/mcp/server.ts +40 -2
- package/src/plugins/builtin/aws.ts +162 -0
- package/src/plugins/builtin/database.ts +19 -1
- package/src/plugins/builtin/docker.ts +17 -1
- package/src/plugins/builtin/gcp.ts +145 -0
- package/src/plugins/builtin/index.ts +4 -0
- package/src/plugins/builtin/jira.ts +23 -19
- package/src/plugins/builtin/linear.ts +1 -1
- package/src/plugins/builtin/shell.ts +1 -1
- package/src/plugins/builtin/slack.ts +6 -1
- package/src/plugins/builtin/spotify.ts +1 -1
- package/src/plugins/builtin/vercel.ts +3 -1
- package/src/security/sso.ts +124 -0
- package/tests/audit.test.ts +185 -0
- package/tests/circuit-breaker.test.ts +125 -0
- package/tests/docker.test.ts +244 -39
- package/tests/errors.test.ts +122 -0
- package/tests/github.test.ts.skip +392 -0
- package/tests/jira.test.ts +310 -0
- package/tests/linear.test.ts +366 -0
- package/tests/mcp.test.ts.skip +243 -0
- package/tests/notion.test.ts +257 -0
- package/tests/retry.test.ts +104 -0
- package/tests/shell.test.ts +262 -30
- package/tests/slack.test.ts +250 -0
- package/tests/stripe.test.ts +272 -0
- package/tests/validation.test.ts +173 -0
- package/tests/vercel.test.ts +368 -0
- package/tests/zero-config.test.ts +566 -0
- package/C.png +0 -0
- package/tests/mcp.test.ts +0 -14
package/tests/docker.test.ts
CHANGED
|
@@ -1,42 +1,247 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { DockerPlugin } from '../src/plugins/builtin/docker.js';
|
|
3
|
+
|
|
4
|
+
let plugin: DockerPlugin;
|
|
5
|
+
|
|
6
|
+
// Helper to get a tool's handler by name
|
|
7
|
+
function tool(name: string) {
|
|
8
|
+
const t = plugin.getTools().find((t) => t.name === name);
|
|
9
|
+
if (!t) throw new Error(`Tool not found: ${name}`);
|
|
10
|
+
return t.handler as (args: Record<string, unknown>) => Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Mock the private `docker` method on the plugin instance
|
|
14
|
+
function mockDocker(stdout: string, stderr = '') {
|
|
15
|
+
vi.spyOn(plugin as any, 'docker').mockResolvedValue({ stdout, stderr });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mockDockerError(message: string) {
|
|
19
|
+
vi.spyOn(plugin as any, 'docker').mockRejectedValue(new Error(`Docker command failed: ${message}`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
plugin = new DockerPlugin();
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── Structure ────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe('DockerPlugin structure', () => {
|
|
30
|
+
it('has correct name', () => {
|
|
31
|
+
expect(plugin.name).toBe('docker');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('isConfigured() returns a boolean (true when Docker socket present)', () => {
|
|
35
|
+
expect(typeof plugin.isConfigured()).toBe('boolean');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('registers 16 tools', () => {
|
|
39
|
+
expect(plugin.getTools()).toHaveLength(16);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('marks docker_run as requiresApproval', () => {
|
|
43
|
+
const t = plugin.getTools().find((t) => t.name === 'docker_run');
|
|
44
|
+
expect(t?.requiresApproval).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('all tools have a description and inputSchema', () => {
|
|
48
|
+
for (const t of plugin.getTools()) {
|
|
49
|
+
expect(t.description.length).toBeGreaterThan(0);
|
|
50
|
+
expect(t.inputSchema).toHaveProperty('type');
|
|
51
|
+
expect(t.inputSchema).toHaveProperty('properties');
|
|
40
52
|
}
|
|
41
53
|
});
|
|
54
|
+
|
|
55
|
+
it('has tool names for all expected capabilities', () => {
|
|
56
|
+
const names = plugin.getTools().map((t) => t.name);
|
|
57
|
+
expect(names).toContain('docker_containers');
|
|
58
|
+
expect(names).toContain('docker_container_logs');
|
|
59
|
+
expect(names).toContain('docker_container_action');
|
|
60
|
+
expect(names).toContain('docker_images');
|
|
61
|
+
expect(names).toContain('docker_pull');
|
|
62
|
+
expect(names).toContain('docker_run');
|
|
63
|
+
expect(names).toContain('docker_volumes');
|
|
64
|
+
expect(names).toContain('docker_networks');
|
|
65
|
+
expect(names).toContain('docker_stats');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── docker_containers ────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
describe('docker_containers', () => {
|
|
72
|
+
it('returns empty array when no containers running', async () => {
|
|
73
|
+
mockDocker('');
|
|
74
|
+
const result = await tool('docker_containers')({}) as Record<string, unknown>;
|
|
75
|
+
expect(result.containers).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('parses JSON lines from docker ps --format', async () => {
|
|
79
|
+
const c1 = { ID: 'abc123', Names: 'my-app', Status: 'Up 2 hours', Image: 'nginx:latest' };
|
|
80
|
+
const c2 = { ID: 'def456', Names: 'db', Status: 'Up 1 day', Image: 'postgres:15' };
|
|
81
|
+
mockDocker(`${JSON.stringify(c1)}\n${JSON.stringify(c2)}`);
|
|
82
|
+
|
|
83
|
+
const result = await tool('docker_containers')({}) as Record<string, unknown>;
|
|
84
|
+
const containers = result.containers as any[];
|
|
85
|
+
expect(containers).toHaveLength(2);
|
|
86
|
+
expect(containers[0].ID).toBe('abc123');
|
|
87
|
+
expect(containers[1].Names).toBe('db');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('passes -a in docker args when all=true', async () => {
|
|
91
|
+
const spy = vi.spyOn(plugin as any, 'docker').mockResolvedValue({ stdout: '', stderr: '' });
|
|
92
|
+
await tool('docker_containers')({ all: true });
|
|
93
|
+
expect(spy.mock.calls[0][0]).toContain('-a');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('passes --filter in docker args when filters provided', async () => {
|
|
97
|
+
const spy = vi.spyOn(plugin as any, 'docker').mockResolvedValue({ stdout: '', stderr: '' });
|
|
98
|
+
await tool('docker_containers')({ filters: 'status=exited' });
|
|
99
|
+
const args = spy.mock.calls[0][0] as string[];
|
|
100
|
+
expect(args).toContain('--filter');
|
|
101
|
+
expect(args).toContain('status=exited');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('skips malformed JSON lines gracefully', async () => {
|
|
105
|
+
const good = JSON.stringify({ ID: 'abc', Names: 'app' });
|
|
106
|
+
mockDocker(`${good}\nnot-json\n${good}`);
|
|
107
|
+
const result = await tool('docker_containers')({}) as Record<string, unknown>;
|
|
108
|
+
expect((result.containers as any[]).length).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('throws when docker command fails', async () => {
|
|
112
|
+
mockDockerError('Cannot connect to Docker daemon');
|
|
113
|
+
await expect(tool('docker_containers')({})).rejects.toThrow('Docker command failed');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── docker_images ────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
describe('docker_images', () => {
|
|
120
|
+
it('parses image list JSON lines', async () => {
|
|
121
|
+
const img = { Repository: 'nginx', Tag: 'latest', ID: 'sha256:abc', Size: '142MB' };
|
|
122
|
+
mockDocker(JSON.stringify(img));
|
|
123
|
+
const result = await tool('docker_images')({}) as Record<string, unknown>;
|
|
124
|
+
expect((result.images as any[])[0].Repository).toBe('nginx');
|
|
125
|
+
expect((result.images as any[])[0].Tag).toBe('latest');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns empty array when no images', async () => {
|
|
129
|
+
mockDocker('');
|
|
130
|
+
const result = await tool('docker_images')({}) as Record<string, unknown>;
|
|
131
|
+
expect(result.images).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('skips malformed image JSON lines', async () => {
|
|
135
|
+
mockDocker(`bad-line\n${JSON.stringify({ Repository: 'alpine', Tag: '3' })}`);
|
|
136
|
+
const result = await tool('docker_images')({}) as Record<string, unknown>;
|
|
137
|
+
expect((result.images as any[]).length).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── docker_container_logs ────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
describe('docker_container_logs', () => {
|
|
144
|
+
it('returns stdout logs', async () => {
|
|
145
|
+
mockDocker('Line 1\nLine 2\nLine 3');
|
|
146
|
+
const result = await tool('docker_container_logs')({ container: 'my-app' }) as Record<string, unknown>;
|
|
147
|
+
expect(result.logs).toContain('Line 1');
|
|
148
|
+
expect(result.container).toBe('my-app');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('falls back to stderr when stdout is empty', async () => {
|
|
152
|
+
mockDocker('', 'stderr log output');
|
|
153
|
+
const result = await tool('docker_container_logs')({ container: 'my-app' }) as Record<string, unknown>;
|
|
154
|
+
expect(result.logs).toBe('stderr log output');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('uses default tail 100 when not specified', async () => {
|
|
158
|
+
const spy = vi.spyOn(plugin as any, 'docker').mockResolvedValue({ stdout: '', stderr: '' });
|
|
159
|
+
await tool('docker_container_logs')({ container: 'app' });
|
|
160
|
+
expect((spy.mock.calls[0][0] as string[]).includes('100')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('passes custom tail value', async () => {
|
|
164
|
+
const spy = vi.spyOn(plugin as any, 'docker').mockResolvedValue({ stdout: '', stderr: '' });
|
|
165
|
+
await tool('docker_container_logs')({ container: 'app', tail: 50 });
|
|
166
|
+
expect((spy.mock.calls[0][0] as string[]).includes('50')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── docker_container_action ──────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe('docker_container_action', () => {
|
|
173
|
+
it('stops a container and returns success', async () => {
|
|
174
|
+
mockDocker('my-app');
|
|
175
|
+
const result = await tool('docker_container_action')({
|
|
176
|
+
container: 'my-app', action: 'stop',
|
|
177
|
+
}) as Record<string, unknown>;
|
|
178
|
+
expect(result.status).toBe('success');
|
|
179
|
+
expect(result.action).toBe('stop');
|
|
180
|
+
expect(result.container).toBe('my-app');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('passes -f flag for remove action', async () => {
|
|
184
|
+
const spy = vi.spyOn(plugin as any, 'docker').mockResolvedValue({ stdout: '', stderr: '' });
|
|
185
|
+
await tool('docker_container_action')({ container: 'dead', action: 'remove' });
|
|
186
|
+
const args = spy.mock.calls[0][0] as string[];
|
|
187
|
+
expect(args[0]).toBe('remove');
|
|
188
|
+
expect(args).toContain('-f');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('throws when container not found', async () => {
|
|
192
|
+
mockDockerError('No such container: ghost');
|
|
193
|
+
await expect(tool('docker_container_action')({ container: 'ghost', action: 'start' }))
|
|
194
|
+
.rejects.toThrow('Docker command failed');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── docker_volumes ───────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe('docker_volumes', () => {
|
|
201
|
+
it('parses volume list', async () => {
|
|
202
|
+
const vol = { Name: 'my-vol', Driver: 'local', Mountpoint: '/var/lib/docker/volumes/my-vol/_data' };
|
|
203
|
+
mockDocker(JSON.stringify(vol));
|
|
204
|
+
const result = await tool('docker_volumes')({}) as Record<string, unknown>;
|
|
205
|
+
expect((result.volumes as any[])[0].Name).toBe('my-vol');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('returns empty when no volumes', async () => {
|
|
209
|
+
mockDocker('');
|
|
210
|
+
const result = await tool('docker_volumes')({}) as Record<string, unknown>;
|
|
211
|
+
expect(result.volumes).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── docker_networks ──────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe('docker_networks', () => {
|
|
218
|
+
it('parses network list', async () => {
|
|
219
|
+
const net = { ID: 'n1', Name: 'bridge', Driver: 'bridge', Scope: 'local' };
|
|
220
|
+
mockDocker(JSON.stringify(net));
|
|
221
|
+
const result = await tool('docker_networks')({}) as Record<string, unknown>;
|
|
222
|
+
expect((result.networks as any[])[0].Name).toBe('bridge');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('returns empty when no networks', async () => {
|
|
226
|
+
mockDocker('');
|
|
227
|
+
const result = await tool('docker_networks')({}) as Record<string, unknown>;
|
|
228
|
+
expect(result.networks).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── docker_stats ─────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
describe('docker_stats', () => {
|
|
235
|
+
it('returns parsed stats', async () => {
|
|
236
|
+
const s = { Name: 'my-app', CPUPerc: '2.5%', MemUsage: '50MiB / 2GiB' };
|
|
237
|
+
mockDocker(JSON.stringify(s));
|
|
238
|
+
const result = await tool('docker_stats')({}) as Record<string, unknown>;
|
|
239
|
+
expect((result.stats as any[])[0].Name).toBe('my-app');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns empty when no running containers', async () => {
|
|
243
|
+
mockDocker('');
|
|
244
|
+
const result = await tool('docker_stats')({}) as Record<string, unknown>;
|
|
245
|
+
expect(result.stats).toEqual([]);
|
|
246
|
+
});
|
|
42
247
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ConductorError, ERRORS, createError } from '../src/core/errors.js';
|
|
3
|
+
|
|
4
|
+
describe('ConductorError', () => {
|
|
5
|
+
it('extends Error', () => {
|
|
6
|
+
const e = new ConductorError({ code: 'TEST-001', message: 'test' });
|
|
7
|
+
expect(e).toBeInstanceOf(Error);
|
|
8
|
+
expect(e).toBeInstanceOf(ConductorError);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('sets name to ConductorError', () => {
|
|
12
|
+
const e = new ConductorError({ code: 'X', message: 'y' });
|
|
13
|
+
expect(e.name).toBe('ConductorError');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('stores code and message', () => {
|
|
17
|
+
const e = new ConductorError({ code: 'COND-AUTH-001', message: 'Auth failed' });
|
|
18
|
+
expect(e.code).toBe('COND-AUTH-001');
|
|
19
|
+
expect(e.message).toBe('Auth failed');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('stores optional fix and details', () => {
|
|
23
|
+
const e = new ConductorError({
|
|
24
|
+
code: 'X',
|
|
25
|
+
message: 'msg',
|
|
26
|
+
fix: 'do this',
|
|
27
|
+
details: { extra: 'info' },
|
|
28
|
+
});
|
|
29
|
+
expect(e.fix).toBe('do this');
|
|
30
|
+
expect(e.details).toEqual({ extra: 'info' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('toJSON returns all fields', () => {
|
|
34
|
+
const e = new ConductorError({ code: 'C', message: 'M', fix: 'F', details: { d: 1 } });
|
|
35
|
+
const json = e.toJSON();
|
|
36
|
+
expect(json).toEqual({ code: 'C', message: 'M', fix: 'F', details: { d: 1 } });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('toJSON omits undefined fix/details', () => {
|
|
40
|
+
const e = new ConductorError({ code: 'C', message: 'M' });
|
|
41
|
+
const json = e.toJSON();
|
|
42
|
+
expect(json.fix).toBeUndefined();
|
|
43
|
+
expect(json.details).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('ERRORS constants', () => {
|
|
48
|
+
it('has all expected categories', () => {
|
|
49
|
+
const codes = Object.values(ERRORS).map((e) => e.code);
|
|
50
|
+
expect(codes.some((c) => c.startsWith('COND-AUTH'))).toBe(true);
|
|
51
|
+
expect(codes.some((c) => c.startsWith('COND-NET'))).toBe(true);
|
|
52
|
+
expect(codes.some((c) => c.startsWith('COND-SEC'))).toBe(true);
|
|
53
|
+
expect(codes.some((c) => c.startsWith('COND-CFG'))).toBe(true);
|
|
54
|
+
expect(codes.some((c) => c.startsWith('COND-MCP'))).toBe(true);
|
|
55
|
+
expect(codes.some((c) => c.startsWith('COND-PLG'))).toBe(true);
|
|
56
|
+
expect(codes.some((c) => c.startsWith('COND-DB'))).toBe(true);
|
|
57
|
+
expect(codes.some((c) => c.startsWith('COND-SYS'))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('AUTH_TOKEN_MISSING has correct code', () => {
|
|
61
|
+
expect(ERRORS.AUTH_TOKEN_MISSING.code).toBe('COND-AUTH-001');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('all errors have code and message', () => {
|
|
65
|
+
for (const [key, err] of Object.entries(ERRORS)) {
|
|
66
|
+
expect(err.code, `${key} missing code`).toBeTruthy();
|
|
67
|
+
expect(err.message, `${key} missing message`).toBeTruthy();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('all codes are unique', () => {
|
|
72
|
+
const codes = Object.values(ERRORS).map((e) => e.code);
|
|
73
|
+
const unique = new Set(codes);
|
|
74
|
+
expect(unique.size).toBe(codes.length);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('createError', () => {
|
|
79
|
+
it('creates a ConductorError from error definition', () => {
|
|
80
|
+
const err = createError(ERRORS.AUTH_TOKEN_MISSING as unknown as ConductorError);
|
|
81
|
+
expect(err).toBeInstanceOf(ConductorError);
|
|
82
|
+
expect(err.code).toBe('COND-AUTH-001');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('interpolates variables in message', () => {
|
|
86
|
+
const err = createError(ERRORS.NET_TIMEOUT as unknown as ConductorError, { timeout: '5000' });
|
|
87
|
+
expect(err.message).toContain('5000');
|
|
88
|
+
expect(err.message).not.toContain('{timeout}');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('interpolates variables in fix', () => {
|
|
92
|
+
const err = createError(ERRORS.NET_RATE_LIMITED as unknown as ConductorError, {
|
|
93
|
+
service: 'GitHub',
|
|
94
|
+
retryAfter: '30',
|
|
95
|
+
});
|
|
96
|
+
expect(err.fix).toContain('30');
|
|
97
|
+
expect(err.fix).not.toContain('{retryAfter}');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handles multiple variable substitutions', () => {
|
|
101
|
+
const err = createError(ERRORS.SEC_COMMAND_BLOCKED as unknown as ConductorError, { command: 'rm -rf /' });
|
|
102
|
+
expect(err.message).toContain('rm -rf /');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('works without variables', () => {
|
|
106
|
+
const err = createError(ERRORS.AUTH_TOKEN_MISSING as unknown as ConductorError);
|
|
107
|
+
expect(err.message).toBe(ERRORS.AUTH_TOKEN_MISSING.message);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('handles numeric variables', () => {
|
|
111
|
+
const err = createError(ERRORS.NET_TIMEOUT as unknown as ConductorError, { timeout: 3000 });
|
|
112
|
+
expect(err.message).toContain('3000');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('preserves error code from definition', () => {
|
|
116
|
+
const err = createError(ERRORS.MCP_CIRCUIT_OPEN as unknown as ConductorError, {
|
|
117
|
+
tool: 'shell.exec',
|
|
118
|
+
retryAfter: '60',
|
|
119
|
+
});
|
|
120
|
+
expect(err.code).toBe('COND-MCP-002');
|
|
121
|
+
});
|
|
122
|
+
});
|