@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
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { VercelPlugin } from '../src/plugins/builtin/vercel.js';
|
|
3
|
+
import { Keychain } from '../src/security/keychain.js';
|
|
4
|
+
|
|
5
|
+
// Helper: get a tool's handler by name
|
|
6
|
+
function tool(plugin: VercelPlugin, name: string) {
|
|
7
|
+
const t = plugin.getTools().find((t) => t.name === name);
|
|
8
|
+
if (!t) throw new Error(`Tool not found: ${name}`);
|
|
9
|
+
return t.handler as (args: Record<string, unknown>) => Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Minimal conductor mock
|
|
13
|
+
function makeConductor(configDir = '/tmp/conductor-test-vercel') {
|
|
14
|
+
return {
|
|
15
|
+
getConfig: () => ({
|
|
16
|
+
getConfigDir: () => configDir,
|
|
17
|
+
}),
|
|
18
|
+
} as any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let plugin: VercelPlugin;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
plugin = new VercelPlugin();
|
|
25
|
+
await plugin.initialize(makeConductor());
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ── Structure ────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('VercelPlugin structure', () => {
|
|
36
|
+
it('has correct name and version', () => {
|
|
37
|
+
expect(plugin.name).toBe('vercel');
|
|
38
|
+
expect(plugin.version).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('registers expected tools', () => {
|
|
42
|
+
const names = plugin.getTools().map((t) => t.name);
|
|
43
|
+
expect(names).toContain('vercel_projects');
|
|
44
|
+
expect(names).toContain('vercel_project');
|
|
45
|
+
expect(names).toContain('vercel_deployments');
|
|
46
|
+
expect(names).toContain('vercel_deployment');
|
|
47
|
+
expect(names).toContain('vercel_redeploy');
|
|
48
|
+
expect(names).toContain('vercel_cancel');
|
|
49
|
+
expect(names).toContain('vercel_logs');
|
|
50
|
+
expect(names).toContain('vercel_env_list');
|
|
51
|
+
expect(names).toContain('vercel_env_add');
|
|
52
|
+
expect(names).toContain('vercel_env_delete');
|
|
53
|
+
expect(names).toContain('vercel_domains');
|
|
54
|
+
expect(names).toContain('vercel_add_domain');
|
|
55
|
+
expect(names).toContain('vercel_team_info');
|
|
56
|
+
expect(names).toContain('vercel_set_team');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('marks destructive operations as requiresApproval', () => {
|
|
60
|
+
const approvalTools = ['vercel_cancel', 'vercel_env_delete', 'vercel_add_domain'];
|
|
61
|
+
for (const name of approvalTools) {
|
|
62
|
+
const t = plugin.getTools().find((t) => t.name === name);
|
|
63
|
+
expect(t?.requiresApproval).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Note: isConfigured() returns true by design - real check happens at tool invocation
|
|
69
|
+
|
|
70
|
+
// ── Unconfigured error messages ───────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe('Vercel tools — unconfigured', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.spyOn(Keychain.prototype, 'get').mockResolvedValue(null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('vercel_projects throws with actionable message when not configured', async () => {
|
|
78
|
+
await expect(tool(plugin, 'vercel_projects')({})).rejects.toThrow(/vercel/i);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('vercel_deployments throws with actionable message when not configured', async () => {
|
|
82
|
+
await expect(tool(plugin, 'vercel_deployments')({})).rejects.toThrow(/vercel/i);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('vercel_team_info throws with actionable message when not configured', async () => {
|
|
86
|
+
await expect(tool(plugin, 'vercel_team_info')({})).rejects.toThrow(/vercel/i);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── Configured — mocked fetch calls ──────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe('Vercel tools — configured (no team)', () => {
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
// First call for 'token', second for 'team_id' (returns null = personal account)
|
|
95
|
+
vi.spyOn(Keychain.prototype, 'get')
|
|
96
|
+
.mockImplementation(async (service: string, key: string) => {
|
|
97
|
+
if (service === 'vercel' && key === 'token') return 'fake_vercel_token_xyz';
|
|
98
|
+
if (service === 'vercel' && key === 'team_id') return null;
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
plugin = new VercelPlugin();
|
|
102
|
+
await plugin.initialize(makeConductor());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('vercel_projects returns project list', async () => {
|
|
106
|
+
const mockResponse = {
|
|
107
|
+
projects: [
|
|
108
|
+
{
|
|
109
|
+
id: 'prj_abc',
|
|
110
|
+
name: 'my-app',
|
|
111
|
+
framework: 'nextjs',
|
|
112
|
+
nodeVersion: '18',
|
|
113
|
+
latestDeployments: [{ url: 'my-app.vercel.app', readyState: 'READY', target: 'production' }],
|
|
114
|
+
alias: [{ domain: 'my-app.com' }],
|
|
115
|
+
createdAt: 1700000000000,
|
|
116
|
+
updatedAt: 1720000000000,
|
|
117
|
+
link: null,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: () => Promise.resolve(mockResponse),
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const result = await tool(plugin, 'vercel_projects')({}) as any;
|
|
128
|
+
expect(result.count).toBe(1);
|
|
129
|
+
expect(result.projects[0].name).toBe('my-app');
|
|
130
|
+
expect(result.projects[0].framework).toBe('nextjs');
|
|
131
|
+
expect(result.projects[0].productionUrl).toBe('https://my-app.com');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('vercel_deployments returns deployment list', async () => {
|
|
135
|
+
const mockResponse = {
|
|
136
|
+
deployments: [
|
|
137
|
+
{
|
|
138
|
+
uid: 'dpl_abc123',
|
|
139
|
+
name: 'my-app',
|
|
140
|
+
url: 'my-app-abc.vercel.app',
|
|
141
|
+
readyState: 'READY',
|
|
142
|
+
target: 'production',
|
|
143
|
+
meta: {
|
|
144
|
+
githubCommitRef: 'main',
|
|
145
|
+
githubCommitSha: 'abc123def456',
|
|
146
|
+
githubCommitMessage: 'feat: add new feature',
|
|
147
|
+
githubCommitAuthorLogin: 'alice',
|
|
148
|
+
},
|
|
149
|
+
createdAt: 1700000000000,
|
|
150
|
+
buildingAt: 1700000060000,
|
|
151
|
+
ready: 1700000120000,
|
|
152
|
+
aliases: [],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
158
|
+
ok: true,
|
|
159
|
+
json: () => Promise.resolve(mockResponse),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const result = await tool(plugin, 'vercel_deployments')({}) as any;
|
|
163
|
+
expect(result.count).toBe(1);
|
|
164
|
+
expect(result.deployments[0].id).toBe('dpl_abc123');
|
|
165
|
+
expect(result.deployments[0].state).toBe('READY');
|
|
166
|
+
expect(result.deployments[0].url).toBe('https://my-app-abc.vercel.app');
|
|
167
|
+
expect(result.deployments[0].branch).toBe('main');
|
|
168
|
+
expect(result.deployments[0].commit.message).toBe('feat: add new feature');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('vercel_deployment returns single deployment detail', async () => {
|
|
172
|
+
const mockDeployment = {
|
|
173
|
+
uid: 'dpl_xyz789',
|
|
174
|
+
name: 'my-app',
|
|
175
|
+
url: 'my-app-xyz.vercel.app',
|
|
176
|
+
readyState: 'ERROR',
|
|
177
|
+
target: 'preview',
|
|
178
|
+
meta: {},
|
|
179
|
+
createdAt: 1700000000000,
|
|
180
|
+
buildingAt: null,
|
|
181
|
+
ready: null,
|
|
182
|
+
aliases: [],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: () => Promise.resolve(mockDeployment),
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
const result = await tool(plugin, 'vercel_deployment')({ id: 'dpl_xyz789' }) as any;
|
|
191
|
+
expect(result.id).toBe('dpl_xyz789');
|
|
192
|
+
expect(result.state).toBe('ERROR');
|
|
193
|
+
expect(result.target).toBe('preview');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('vercel_env_list returns environment variables', async () => {
|
|
197
|
+
const mockResponse = {
|
|
198
|
+
envs: [
|
|
199
|
+
{
|
|
200
|
+
id: 'env-1',
|
|
201
|
+
key: 'DATABASE_URL',
|
|
202
|
+
value: '[encrypted]',
|
|
203
|
+
type: 'encrypted',
|
|
204
|
+
target: ['production'],
|
|
205
|
+
createdAt: 1700000000000,
|
|
206
|
+
updatedAt: 1700000000000,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'env-2',
|
|
210
|
+
key: 'API_KEY',
|
|
211
|
+
value: '[encrypted]',
|
|
212
|
+
type: 'encrypted',
|
|
213
|
+
target: ['production', 'preview'],
|
|
214
|
+
createdAt: 1700000000000,
|
|
215
|
+
updatedAt: 1700000000000,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
221
|
+
ok: true,
|
|
222
|
+
json: () => Promise.resolve(mockResponse),
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
const result = await tool(plugin, 'vercel_env_list')({ projectId: 'my-app' }) as any;
|
|
226
|
+
expect(result.count).toBe(2);
|
|
227
|
+
expect(result.envs[0].key).toBe('DATABASE_URL');
|
|
228
|
+
expect(result.envs[1].key).toBe('API_KEY');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('vercel_domains returns domain list', async () => {
|
|
232
|
+
const mockResponse = {
|
|
233
|
+
domains: [
|
|
234
|
+
{
|
|
235
|
+
name: 'my-app.com',
|
|
236
|
+
apexName: 'my-app.com',
|
|
237
|
+
verified: true,
|
|
238
|
+
misconfigured: false,
|
|
239
|
+
redirect: null,
|
|
240
|
+
createdAt: 1700000000000,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
246
|
+
ok: true,
|
|
247
|
+
json: () => Promise.resolve(mockResponse),
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
const result = await tool(plugin, 'vercel_domains')({ projectId: 'my-app' }) as any;
|
|
251
|
+
expect(result.count).toBe(1);
|
|
252
|
+
expect(result.domains[0].name).toBe('my-app.com');
|
|
253
|
+
expect(result.domains[0].verified).toBe(true);
|
|
254
|
+
expect(result.domains[0].configured).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('vercel_team_info returns personal account info', async () => {
|
|
258
|
+
const mockUser = {
|
|
259
|
+
uid: 'user-123',
|
|
260
|
+
username: 'alice',
|
|
261
|
+
email: 'alice@example.com',
|
|
262
|
+
subscription: { plan: 'pro' },
|
|
263
|
+
avatar: 'https://vercel.com/api/www/avatar/abc',
|
|
264
|
+
createdAt: 1600000000000,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
268
|
+
ok: true,
|
|
269
|
+
json: () => Promise.resolve(mockUser),
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
const result = await tool(plugin, 'vercel_team_info')({}) as any;
|
|
273
|
+
expect(result.name).toBe('alice');
|
|
274
|
+
expect(result.email).toBe('alice@example.com');
|
|
275
|
+
expect(result.plan).toBe('pro');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('uses Authorization Bearer header', async () => {
|
|
279
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
280
|
+
ok: true,
|
|
281
|
+
json: () => Promise.resolve({ projects: [] }),
|
|
282
|
+
});
|
|
283
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
284
|
+
|
|
285
|
+
await tool(plugin, 'vercel_projects')({});
|
|
286
|
+
|
|
287
|
+
const callInit = fetchMock.mock.calls[0][1] as RequestInit;
|
|
288
|
+
const headers = callInit.headers as Record<string, string>;
|
|
289
|
+
expect(headers['Authorization']).toBe('Bearer fake_vercel_token_xyz');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('vercel_logs returns build log lines', async () => {
|
|
293
|
+
const mockEvents = [
|
|
294
|
+
{ type: 'stdout', payload: { text: 'Building...' }, date: 1700000001000 },
|
|
295
|
+
{ type: 'command', payload: { text: 'npm run build' }, date: 1700000002000 },
|
|
296
|
+
{ type: 'stdout', payload: { text: 'Build complete' }, date: 1700000060000 },
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
300
|
+
ok: true,
|
|
301
|
+
json: () => Promise.resolve(mockEvents),
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
const result = await tool(plugin, 'vercel_logs')({ deploymentId: 'dpl_abc' }) as any;
|
|
305
|
+
expect(result.count).toBe(3);
|
|
306
|
+
expect(result.logs[0].text).toBe('Building...');
|
|
307
|
+
expect(result.logs[1].type).toBe('command');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('vercel throws when API returns error', async () => {
|
|
311
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
312
|
+
ok: false,
|
|
313
|
+
status: 403,
|
|
314
|
+
json: () => Promise.resolve({ error: { message: 'Forbidden' } }),
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
await expect(tool(plugin, 'vercel_projects')({})).rejects.toThrow(/Forbidden/);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── Configured with team ID ───────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
describe('Vercel tools — configured (with team)', () => {
|
|
324
|
+
beforeEach(async () => {
|
|
325
|
+
vi.spyOn(Keychain.prototype, 'get')
|
|
326
|
+
.mockImplementation(async (service: string, key: string) => {
|
|
327
|
+
if (service === 'vercel' && key === 'token') return 'fake_vercel_token_xyz';
|
|
328
|
+
if (service === 'vercel' && key === 'team_id') return 'team_abc123';
|
|
329
|
+
return null;
|
|
330
|
+
});
|
|
331
|
+
plugin = new VercelPlugin();
|
|
332
|
+
await plugin.initialize(makeConductor());
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('includes teamId in request URL when team is set', async () => {
|
|
336
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
337
|
+
ok: true,
|
|
338
|
+
json: () => Promise.resolve({ projects: [] }),
|
|
339
|
+
});
|
|
340
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
341
|
+
|
|
342
|
+
await tool(plugin, 'vercel_projects')({});
|
|
343
|
+
|
|
344
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
345
|
+
expect(calledUrl).toContain('teamId=team_abc123');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('vercel_team_info fetches team info when team is set', async () => {
|
|
349
|
+
const mockTeam = {
|
|
350
|
+
id: 'team_abc123',
|
|
351
|
+
name: 'Acme Corp',
|
|
352
|
+
email: null,
|
|
353
|
+
plan: { id: 'enterprise' },
|
|
354
|
+
avatar: null,
|
|
355
|
+
createdAt: 1600000000000,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
359
|
+
ok: true,
|
|
360
|
+
json: () => Promise.resolve(mockTeam),
|
|
361
|
+
}));
|
|
362
|
+
|
|
363
|
+
const result = await tool(plugin, 'vercel_team_info')({}) as any;
|
|
364
|
+
expect(result.id).toBe('team_abc123');
|
|
365
|
+
expect(result.name).toBe('Acme Corp');
|
|
366
|
+
expect(result.plan).toBe('enterprise');
|
|
367
|
+
});
|
|
368
|
+
});
|