@amodalai/amodal 0.3.26 → 0.3.28

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.
@@ -1,305 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- /**
8
- * End-to-end test: Automation lifecycle
9
- *
10
- * Tests the full automation control flow via the HTTP API:
11
- * 1. Create repo with cron + webhook automations on disk
12
- * 2. Boot `createLocalServer` (which integrates ProactiveRunner)
13
- * 3. List automations — verify both appear, cron is stopped, webhook is running
14
- * 4. Start a cron automation — verify it becomes running
15
- * 5. Stop a cron automation — verify it becomes stopped
16
- * 6. Reject starting a webhook automation (always active)
17
- * 7. Manually trigger an automation (run)
18
- * 8. Webhook endpoint accepts events
19
- */
20
-
21
- import {describe, it, expect, beforeAll, afterAll} from 'vitest';
22
- import {mkdtempSync, mkdirSync, writeFileSync, rmSync} from 'node:fs';
23
- import {join} from 'node:path';
24
- import {tmpdir} from 'node:os';
25
- import type {AddressInfo} from 'node:net';
26
-
27
- // ---------------------------------------------------------------------------
28
- // Fixture data
29
- // ---------------------------------------------------------------------------
30
-
31
- const CONFIG = {
32
- name: 'automation-test-agent',
33
- version: '1.0.0',
34
- description: 'Agent with automations for e2e testing',
35
- models: {
36
- main: {provider: 'anthropic', model: 'claude-sonnet-4-20250514'},
37
- },
38
- };
39
-
40
- const CRON_AUTOMATION = `# Automation: Daily Scan
41
-
42
- Schedule: */5 * * * *
43
-
44
- ## Check
45
- Scan all zones for anomalies and report findings.
46
-
47
- ## Output
48
- Summary of anomalies found.
49
-
50
- ## Delivery
51
- stdout
52
- `;
53
-
54
- const WEBHOOK_AUTOMATION = `# Automation: Alert Handler
55
-
56
- ## Check
57
- Run on webhook when an alert fires. Triage the alert and determine severity.
58
-
59
- ## Output
60
- Triage assessment with severity level.
61
-
62
- ## Delivery
63
- stdout
64
- `;
65
-
66
- // ---------------------------------------------------------------------------
67
- // Test suite
68
- // ---------------------------------------------------------------------------
69
-
70
- describe('E2E: Automation Lifecycle', () => {
71
- let repoDir: string;
72
- let server: {app: unknown; start: () => Promise<unknown>; stop: () => Promise<void>} | null = null;
73
- let baseUrl: string;
74
-
75
- beforeAll(async () => {
76
- // 1. Create repo directory with automations
77
- repoDir = mkdtempSync(join(tmpdir(), 'amodal-e2e-automations-'));
78
-
79
- // Config
80
- writeFileSync(join(repoDir, 'amodal.json'), JSON.stringify(CONFIG, null, 2));
81
-
82
- // Automations
83
- const autoDir = join(repoDir, 'automations');
84
- mkdirSync(autoDir, {recursive: true});
85
- writeFileSync(join(autoDir, 'daily-scan.md'), CRON_AUTOMATION);
86
- writeFileSync(join(autoDir, 'alert-handler.md'), WEBHOOK_AUTOMATION);
87
-
88
- // 2. Boot repo server
89
- const {createLocalServer} = await import('@amodalai/runtime');
90
- const srv = await createLocalServer({
91
- repoPath: repoDir,
92
- port: 0, // random port
93
- host: '127.0.0.1',
94
- hotReload: false,
95
- corsOrigin: '*',
96
- });
97
-
98
- const httpServer = await srv.start();
99
- const addr = httpServer.address() as AddressInfo;
100
- baseUrl = `http://127.0.0.1:${addr.port}`;
101
- server = srv;
102
- }, 30000);
103
-
104
- afterAll(async () => {
105
- if (server) {
106
- await server.stop();
107
- server = null;
108
- }
109
- rmSync(repoDir, {recursive: true, force: true});
110
- });
111
-
112
- // =========================================================================
113
- // Health check — server is up
114
- // =========================================================================
115
-
116
- it('should respond to health check', async () => {
117
- const resp = await fetch(`${baseUrl}/health`);
118
- expect(resp.status).toBe(200);
119
- const data = (await resp.json()) as Record<string, unknown>;
120
- expect(data['status']).toBe('ok');
121
- expect(data['mode']).toBe('repo');
122
- });
123
-
124
- // =========================================================================
125
- // List — both automations appear with correct initial state
126
- // =========================================================================
127
-
128
- it('should list automations with correct types and initial state', async () => {
129
- const resp = await fetch(`${baseUrl}/automations`);
130
- expect(resp.status).toBe(200);
131
-
132
-
133
- const data = (await resp.json()) as {
134
- automations: Array<{
135
- name: string;
136
- title: string;
137
- schedule?: string;
138
- webhookTriggered: boolean;
139
- running: boolean;
140
- }>;
141
- };
142
-
143
- expect(data.automations).toHaveLength(2);
144
-
145
- const cronAuto = data.automations.find((a) => a.name === 'daily-scan');
146
- expect(cronAuto).toBeDefined();
147
- expect(cronAuto?.title).toBe('Daily Scan');
148
- expect(cronAuto?.schedule).toBe('*/5 * * * *');
149
- expect(cronAuto?.webhookTriggered).toBe(false);
150
- expect(cronAuto?.running).toBe(false); // not started yet
151
-
152
- const webhookAuto = data.automations.find((a) => a.name === 'alert-handler');
153
- expect(webhookAuto).toBeDefined();
154
- expect(webhookAuto?.title).toBe('Alert Handler');
155
- expect(webhookAuto?.webhookTriggered).toBe(true);
156
- expect(webhookAuto?.running).toBe(true); // webhooks always active
157
- });
158
-
159
- // =========================================================================
160
- // Start — cron automation becomes running
161
- // =========================================================================
162
-
163
- it('should start a cron automation', async () => {
164
- const resp = await fetch(`${baseUrl}/automations/daily-scan/start`, {method: 'POST'});
165
- expect(resp.status).toBe(200);
166
- const data = (await resp.json()) as Record<string, unknown>;
167
- expect(data['status']).toBe('started');
168
-
169
- // Verify it's now running
170
- const listResp = await fetch(`${baseUrl}/automations`);
171
-
172
- const listData = (await listResp.json()) as {
173
- automations: Array<{name: string; running: boolean}>;
174
- };
175
- const cronAuto = listData.automations.find((a) => a.name === 'daily-scan');
176
- expect(cronAuto?.running).toBe(true);
177
- });
178
-
179
- // =========================================================================
180
- // Start again — should fail (already running)
181
- // =========================================================================
182
-
183
- it('should reject starting an already running automation', async () => {
184
- const resp = await fetch(`${baseUrl}/automations/daily-scan/start`, {method: 'POST'});
185
- expect(resp.status).toBe(400);
186
- const data = (await resp.json()) as Record<string, unknown>;
187
- expect(data['error']).toContain('already running');
188
- });
189
-
190
- // =========================================================================
191
- // Stop — cron automation becomes stopped
192
- // =========================================================================
193
-
194
- it('should stop a running cron automation', async () => {
195
- const resp = await fetch(`${baseUrl}/automations/daily-scan/stop`, {method: 'POST'});
196
- expect(resp.status).toBe(200);
197
- const data = (await resp.json()) as Record<string, unknown>;
198
- expect(data['status']).toBe('stopped');
199
-
200
- // Verify it's now stopped
201
- const listResp = await fetch(`${baseUrl}/automations`);
202
-
203
- const listData = (await listResp.json()) as {
204
- automations: Array<{name: string; running: boolean}>;
205
- };
206
- const cronAuto = listData.automations.find((a) => a.name === 'daily-scan');
207
- expect(cronAuto?.running).toBe(false);
208
- });
209
-
210
- // =========================================================================
211
- // Stop again — should fail (not running)
212
- // =========================================================================
213
-
214
- it('should reject stopping a non-running automation', async () => {
215
- const resp = await fetch(`${baseUrl}/automations/daily-scan/stop`, {method: 'POST'});
216
- expect(resp.status).toBe(400);
217
- const data = (await resp.json()) as Record<string, unknown>;
218
- expect(data['error']).toContain('not running');
219
- });
220
-
221
- // =========================================================================
222
- // Start webhook automation — should fail (always active)
223
- // =========================================================================
224
-
225
- it('should reject starting a webhook-triggered automation', async () => {
226
- const resp = await fetch(`${baseUrl}/automations/alert-handler/start`, {method: 'POST'});
227
- expect(resp.status).toBe(400);
228
- const data = (await resp.json()) as Record<string, unknown>;
229
- expect(data['error']).toContain('webhook-triggered');
230
- });
231
-
232
- // =========================================================================
233
- // Start unknown — should fail
234
- // =========================================================================
235
-
236
- it('should reject starting unknown automation', async () => {
237
- const resp = await fetch(`${baseUrl}/automations/nonexistent/start`, {method: 'POST'});
238
- expect(resp.status).toBe(400);
239
- const data = (await resp.json()) as Record<string, unknown>;
240
- expect(data['error']).toContain('not found');
241
- });
242
-
243
- // =========================================================================
244
- // Run — manually trigger an automation (fire and forget)
245
- // =========================================================================
246
-
247
- it('should reject triggering unknown automation', async () => {
248
- const resp = await fetch(`${baseUrl}/automations/nonexistent/run`, {
249
- method: 'POST',
250
- headers: {'Content-Type': 'application/json'},
251
- body: JSON.stringify({}),
252
- });
253
- expect(resp.status).toBe(404);
254
- });
255
-
256
- // =========================================================================
257
- // Webhook endpoint — accepts events
258
- // =========================================================================
259
-
260
- it('should accept webhook events for webhook-triggered automations', async () => {
261
- const resp = await fetch(`${baseUrl}/webhooks/alert-handler`, {
262
- method: 'POST',
263
- headers: {'Content-Type': 'application/json'},
264
- body: JSON.stringify({alert: 'high-cpu', host: 'web-01'}),
265
- });
266
-
267
- // May succeed or fail (depends on LLM availability), but route exists
268
- expect([200, 500]).toContain(resp.status);
269
- const data = (await resp.json()) as Record<string, unknown>;
270
- // If 200, it matched and ran
271
- if (resp.status === 200) {
272
- expect(data['status']).toBe('accepted');
273
- }
274
- // If 500, it matched but execution failed (no LLM configured) — still validates routing
275
- if (resp.status === 500) {
276
- expect(data['matched']).toBe(true);
277
- }
278
- });
279
-
280
- // =========================================================================
281
- // Webhook for non-webhook automation — should 404
282
- // =========================================================================
283
-
284
- it('should reject webhook for cron-only automation', async () => {
285
- const resp = await fetch(`${baseUrl}/webhooks/daily-scan`, {
286
- method: 'POST',
287
- headers: {'Content-Type': 'application/json'},
288
- body: JSON.stringify({}),
289
- });
290
- expect(resp.status).toBe(404);
291
- });
292
-
293
- // =========================================================================
294
- // Webhook for unknown automation — should 404
295
- // =========================================================================
296
-
297
- it('should reject webhook for unknown automation', async () => {
298
- const resp = await fetch(`${baseUrl}/webhooks/nonexistent`, {
299
- method: 'POST',
300
- headers: {'Content-Type': 'application/json'},
301
- body: JSON.stringify({}),
302
- });
303
- expect(resp.status).toBe(404);
304
- });
305
- });
@@ -1,345 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- /**
8
- * End-to-end test: Incident Response Agent
9
- *
10
- * Tests the full content pipeline with all four content types:
11
- * Connection (statuspage API) + Skill (incident triage) +
12
- * Knowledge (oncall runbook) + Automation (health check)
13
- *
14
- * Flow:
15
- * 1. Create repo with all content types written to disk
16
- * 2. Build snapshot → verify all content present
17
- * 3. Start mock StatusPage API
18
- * 4. Boot runtime from snapshot → send chat → verify agent uses
19
- * the connection, skill, and knowledge in its response
20
- */
21
-
22
- import {describe, it, expect, beforeAll, afterAll} from 'vitest';
23
- import {mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync} from 'node:fs';
24
- import {join} from 'node:path';
25
- import {tmpdir} from 'node:os';
26
- import http from 'node:http';
27
-
28
- import type {
29
- MOCK_COMPONENTS} from './fixtures/incident-response.js';
30
- import {
31
- CONFIG,
32
- STATUSPAGE_SPEC,
33
- STATUSPAGE_ACCESS,
34
- STATUSPAGE_SURFACE,
35
- TRIAGE_SKILL,
36
- ONCALL_RUNBOOK,
37
- HEALTH_CHECK_AUTOMATION,
38
- createMockStatusPageApi,
39
- } from './fixtures/incident-response.js';
40
- import {runBuild} from './commands/build.js';
41
- import type {DeploySnapshot} from '@amodalai/core';
42
- import {loadRepo, loadSnapshotFromFile, snapshotToBundle} from '@amodalai/core';
43
-
44
- // ---------------------------------------------------------------------------
45
- // Helper: send chat and parse SSE events
46
- // ---------------------------------------------------------------------------
47
-
48
- async function sendChat(
49
- port: number,
50
- message: string,
51
- appId: string,
52
- sessionId?: string,
53
- timeoutMs = 30000,
54
- ): Promise<{events: Array<Record<string, unknown>>; rawBody: string}> {
55
- return new Promise((resolve, reject) => {
56
- const payload: Record<string, string> = {message, app_id: appId};
57
- if (sessionId) payload['session_id'] = sessionId;
58
- const body = JSON.stringify(payload);
59
- const req = http.request(
60
- {
61
- hostname: '127.0.0.1',
62
- port,
63
- path: '/chat',
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- 'Content-Length': Buffer.byteLength(body),
68
- },
69
- timeout: timeoutMs,
70
- },
71
- (res) => {
72
- let rawBody = '';
73
- res.setEncoding('utf8');
74
- res.on('data', (chunk: string) => { rawBody += chunk; });
75
- res.on('end', () => {
76
- const events: Array<Record<string, unknown>> = [];
77
- for (const line of rawBody.split('\n')) {
78
- if (line.startsWith('data: ')) {
79
- try { events.push(JSON.parse(line.slice(6)) as Record<string, unknown>); } catch { /* skip */ }
80
- }
81
- }
82
- resolve({events, rawBody});
83
- });
84
- },
85
- );
86
- req.on('error', reject);
87
- req.on('timeout', () => req.destroy(new Error('timeout')));
88
- req.write(body);
89
- req.end();
90
- });
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // Test suite
95
- // ---------------------------------------------------------------------------
96
-
97
- describe('E2E: Incident Response Agent', () => {
98
- let repoDir: string;
99
- let snapshotPath: string;
100
- let builtSnapshot: DeploySnapshot;
101
- let mockApi: ReturnType<typeof createMockStatusPageApi>;
102
- let runtimeServer: {app: unknown; start: () => Promise<unknown>; stop: () => Promise<void>} | null = null;
103
- let runtimePort: number;
104
-
105
- beforeAll(async () => {
106
- // 1. Create repo directory structure with all content types
107
- repoDir = mkdtempSync(join(tmpdir(), 'amodal-e2e-incident-'));
108
-
109
- // Config
110
- writeFileSync(join(repoDir, 'amodal.json'), JSON.stringify(CONFIG, null, 2));
111
-
112
- // Connection: statuspage
113
- const connDir = join(repoDir, 'connections', 'statuspage');
114
- mkdirSync(connDir, {recursive: true});
115
- writeFileSync(join(connDir, 'spec.json'), JSON.stringify(STATUSPAGE_SPEC, null, 2));
116
- writeFileSync(join(connDir, 'access.json'), JSON.stringify(STATUSPAGE_ACCESS, null, 2));
117
- writeFileSync(join(connDir, 'surface.md'), STATUSPAGE_SURFACE);
118
-
119
- // Skill: incident-triage
120
- const skillDir = join(repoDir, 'skills', 'incident-triage');
121
- mkdirSync(skillDir, {recursive: true});
122
- writeFileSync(join(skillDir, 'SKILL.md'), TRIAGE_SKILL);
123
-
124
- // Knowledge: oncall-runbook
125
- const knowledgeDir = join(repoDir, 'knowledge');
126
- mkdirSync(knowledgeDir, {recursive: true});
127
- writeFileSync(join(knowledgeDir, 'oncall-runbook.md'), ONCALL_RUNBOOK);
128
-
129
- // Automation: health-check
130
- const autoDir = join(repoDir, 'automations');
131
- mkdirSync(autoDir, {recursive: true});
132
- writeFileSync(join(autoDir, 'health-check.md'), HEALTH_CHECK_AUTOMATION);
133
-
134
- // 2. Build snapshot
135
- snapshotPath = join(repoDir, 'resolved-config.json');
136
- const code = await runBuild({cwd: repoDir, output: snapshotPath});
137
- if (code !== 0) throw new Error('Build failed');
138
-
139
- builtSnapshot = await loadSnapshotFromFile(snapshotPath);
140
-
141
- // 3. Start mock StatusPage API
142
- mockApi = createMockStatusPageApi();
143
- await mockApi.start();
144
-
145
- // 4. Update the spec in the snapshot to point to the mock API
146
- // (In production, the base URL comes from the spec; here we override it)
147
- // We need to rebuild with the mock URL baked in
148
- const specWithMockUrl = {
149
- ...STATUSPAGE_SPEC,
150
- specUrl: `http://127.0.0.1:${mockApi.port}/openapi.json`,
151
- };
152
- writeFileSync(join(connDir, 'spec.json'), JSON.stringify(specWithMockUrl, null, 2));
153
-
154
- // Rebuild snapshot with the mock URL
155
- const code2 = await runBuild({cwd: repoDir, output: snapshotPath});
156
- if (code2 !== 0) throw new Error('Rebuild with mock URL failed');
157
- builtSnapshot = await loadSnapshotFromFile(snapshotPath);
158
-
159
- // 5. Boot runtime from snapshot
160
- const {createSnapshotServer} = await import('@amodalai/runtime');
161
- runtimeServer = await createSnapshotServer({
162
- snapshotPath,
163
- port: 0,
164
- host: '127.0.0.1',
165
- });
166
-
167
- const httpServer = await runtimeServer.start();
168
- const addr = (httpServer as http.Server).address();
169
- runtimePort = typeof addr === 'object' && addr ? addr.port : 0;
170
- });
171
-
172
- afterAll(async () => {
173
- if (runtimeServer) await runtimeServer.stop();
174
- if (mockApi) await mockApi.stop();
175
- if (repoDir && existsSync(repoDir)) rmSync(repoDir, {recursive: true, force: true});
176
- });
177
-
178
- // =========================================================================
179
- // Phase 1: Repo loading — all content types load from disk
180
- // =========================================================================
181
-
182
- describe('repo loading', () => {
183
- it('should load the connection from disk', async () => {
184
- const repo = await loadRepo({localPath: repoDir});
185
- expect(repo.connections.size).toBe(1);
186
- const conn = repo.connections.get('statuspage');
187
- expect(conn).toBeDefined();
188
- expect(conn!.spec.format).toBe('openapi');
189
- expect(conn!.surface.length).toBeGreaterThanOrEqual(3);
190
- expect(conn!.surface.find((s) => s.path === '/components')).toBeDefined();
191
- });
192
-
193
- it('should load the skill from disk', async () => {
194
- const repo = await loadRepo({localPath: repoDir});
195
- expect(repo.skills.length).toBe(1);
196
- expect(repo.skills[0].name).toBe('incident-triage');
197
- expect(repo.skills[0].body).toContain('Check component status');
198
- expect(repo.skills[0].trigger).toContain('service health');
199
- });
200
-
201
- it('should load the knowledge from disk', async () => {
202
- const repo = await loadRepo({localPath: repoDir});
203
- expect(repo.knowledge.length).toBe(1);
204
- expect(repo.knowledge[0].name).toBe('oncall-runbook');
205
- expect(repo.knowledge[0].body).toContain('Severity Matrix');
206
- expect(repo.knowledge[0].body).toContain('alice@example.com');
207
- });
208
-
209
- it('should load the automation from disk', async () => {
210
- const repo = await loadRepo({localPath: repoDir});
211
- expect(repo.automations.length).toBe(1);
212
- expect(repo.automations[0].name).toBe('health-check');
213
- expect(repo.automations[0].title).toBe('Daily Health Check');
214
- expect(repo.automations[0].schedule).toBe('0 8 * * *');
215
- });
216
- });
217
-
218
- // =========================================================================
219
- // Phase 2: Snapshot — all content types serialized
220
- // =========================================================================
221
-
222
- describe('snapshot content', () => {
223
- it('should include the connection in the snapshot', () => {
224
- expect(Object.keys(builtSnapshot.connections)).toContain('statuspage');
225
- const conn = builtSnapshot.connections['statuspage'];
226
- expect(conn.spec.format).toBe('openapi');
227
- // Surface is serialized as checkbox markdown in snapshot
228
- expect(conn.surface).toContain('/components');
229
- expect(conn.access.endpoints['GET /components']).toBeDefined();
230
- });
231
-
232
- it('should include the skill in the snapshot', () => {
233
- expect(builtSnapshot.skills.length).toBe(1);
234
- expect(builtSnapshot.skills[0].name).toBe('incident-triage');
235
- expect(builtSnapshot.skills[0].body).toContain('Check component status');
236
- });
237
-
238
- it('should include the knowledge in the snapshot', () => {
239
- expect(builtSnapshot.knowledge.length).toBe(1);
240
- expect(builtSnapshot.knowledge[0].name).toBe('oncall-runbook');
241
- expect(builtSnapshot.knowledge[0].body).toContain('SEV1');
242
- });
243
-
244
- it('should include the automation in the snapshot', () => {
245
- expect(builtSnapshot.automations.length).toBe(1);
246
- expect(builtSnapshot.automations[0].name).toBe('health-check');
247
- expect(builtSnapshot.automations[0].schedule).toBe('0 8 * * *');
248
- });
249
-
250
- it('should round-trip all content through snapshot', () => {
251
- const restored = snapshotToBundle(builtSnapshot, 'test');
252
-
253
- // Connection
254
- expect(restored.connections.size).toBe(1);
255
- const conn = restored.connections.get('statuspage');
256
- expect(conn).toBeDefined();
257
- expect(conn!.spec.format).toBe('openapi');
258
- // Surface endpoints parsed from markdown
259
- expect(conn!.surface.length).toBeGreaterThanOrEqual(3);
260
- const getComponents = conn!.surface.find((s) => s.method === 'GET' && s.path === '/components');
261
- expect(getComponents).toBeDefined();
262
- expect(getComponents!.included).toBe(true);
263
- const postIncidents = conn!.surface.find((s) => s.method === 'POST');
264
- expect(postIncidents).toBeDefined();
265
- expect(postIncidents!.included).toBe(false);
266
-
267
- // Skill
268
- expect(restored.skills[0].name).toBe('incident-triage');
269
- expect(restored.skills[0].trigger).toContain('service health');
270
-
271
- // Knowledge
272
- expect(restored.knowledge[0].body).toContain('alice@example.com');
273
-
274
- // Automation
275
- expect(restored.automations[0].schedule).toBe('0 8 * * *');
276
- });
277
- });
278
-
279
- // =========================================================================
280
- // Phase 3: Runtime — server boots from snapshot and handles chat
281
- // =========================================================================
282
-
283
- describe('runtime from snapshot', () => {
284
- it('should serve health check with full content counts', async () => {
285
- const resp = await fetch(`http://127.0.0.1:${runtimePort}/health`);
286
- const data = (await resp.json()) as Record<string, unknown>;
287
- expect(data['status']).toBe('ok');
288
- expect(data['mode']).toBe('snapshot');
289
- expect(data['agent_name']).toBe('incident-response-agent');
290
- expect(data['connections']).toBe(1);
291
- expect(data['skills']).toBe(1);
292
- });
293
-
294
- it('should handle chat and stream SSE events', async () => {
295
- const {events} = await sendChat(runtimePort, 'Is the API healthy?', 'app-incident-e2e');
296
-
297
- expect(events.length).toBeGreaterThanOrEqual(2);
298
- expect(events.find((e) => e['type'] === 'init')).toBeDefined();
299
- expect(events.find((e) => e['type'] === 'done')).toBeDefined();
300
- });
301
-
302
- it('should use the connection when the agent makes a tool call', async () => {
303
- // The agent should try to call GET /components via the request tool
304
- // when asked about API health, because the triage skill says to do that.
305
- // Whether it actually calls the mock depends on the LLM, but we can
306
- // check if tool_call_start events reference the statuspage connection.
307
- const {events} = await sendChat(
308
- runtimePort,
309
- 'Check the current status of all services using the statuspage connection.',
310
- 'app-incident-tool-e2e',
311
- );
312
-
313
- const toolCalls = events.filter((e) => e['type'] === 'tool_call_start');
314
- const textEvents = events.filter((e) => e['type'] === 'text_delta');
315
- const fullText = textEvents.map((e) => String(e['content'] ?? '')).join('');
316
-
317
- // The agent should have either made a tool call or mentioned the
318
- // components in its text response
319
- const mentionsConnection = toolCalls.some((tc) => {
320
- const params = tc['parameters'] as Record<string, unknown> | undefined;
321
- return params?.['connection'] === 'statuspage';
322
- });
323
- const mentionsComponents = fullText.toLowerCase().includes('api-gateway') ||
324
- fullText.toLowerCase().includes('component') ||
325
- fullText.toLowerCase().includes('statuspage');
326
-
327
- // At least one of these should be true
328
- expect(mentionsConnection || mentionsComponents).toBe(true);
329
- });
330
- });
331
-
332
- // =========================================================================
333
- // Phase 4: Mock API verification
334
- // =========================================================================
335
-
336
- describe('mock API interaction', () => {
337
- it('should have the mock API running', async () => {
338
- const resp = await fetch(`http://127.0.0.1:${mockApi.port}/components`);
339
- expect(resp.status).toBe(200);
340
- const data = (await resp.json()) as typeof MOCK_COMPONENTS;
341
- expect(data.length).toBe(5);
342
- expect(data.find((c) => c.name === 'database-primary')?.status).toBe('degraded_performance');
343
- });
344
- });
345
- });