ScuttleBot
Merge pull request #152 from ConflictHQ/feature/103-e2e-tests feat: E2E test suite — API tests for all v1.3.0 features
Commit
14597980f083b016b0cf40ea07d25f56c63d8e7c509541cabe05ac02707ef8cb
Parent
a2b9161ba6ac0b7…
1 file changed
+235
+235
| --- a/tests/e2e/tests/api.spec.ts | ||
| +++ b/tests/e2e/tests/api.spec.ts | ||
| @@ -0,0 +1,235 @@ | ||
| 1 | +import { test, expect } from '@playwright/test'; | |
| 2 | + | |
| 3 | +const BASE = process.env.SB_BASE_URL || 'http://localhost:8080'; | |
| 4 | +const TOKEN = process.env.SB_TOKEN || ''; | |
| 5 | + | |
| 6 | +function headers() { | |
| 7 | + return { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }; | |
| 8 | +} | |
| 9 | + | |
| 10 | +test.beforeEach(() => { | |
| 11 | + test.skip(!TOKEN, 'SB_TOKEN not set'); | |
| 12 | +}); | |
| 13 | + | |
| 14 | +// --- Status --- | |
| 15 | + | |
| 16 | +test('GET /v1/status returns ok', async ({ request }) => { | |
| 17 | + const res = await request.get(`${BASE}/v1/status`, { headers: headers() }); | |
| 18 | + expect(res.ok()).toBe(true); | |
| 19 | + const data = await res.json(); | |
| 20 | + expect(data.status).toBe('ok'); | |
| 21 | + expect(data.uptime).toBeTruthy(); | |
| 22 | +}); | |
| 23 | + | |
| 24 | +test('GET /v1/metrics returns runtime stats', async ({ request }) => { | |
| 25 | + const res = await request.get(`${BASE}/v1/metrics`, { headers: headers() }); | |
| 26 | + expect(res.ok()).toBe(true); | |
| 27 | + const data = await res.json(); | |
| 28 | + expect(data.runtime).toBeTruthy(); | |
| 29 | + expect(data.runtime.goroutines).toBeGreaterThan(0); | |
| 30 | +}); | |
| 31 | + | |
| 32 | +// --- Agents --- | |
| 33 | + | |
| 34 | +test('GET /v1/agents returns agent list', async ({ request }) => { | |
| 35 | + const res = await request.get(`${BASE}/v1/agents`, { headers: headers() }); | |
| 36 | + expect(res.ok()).toBe(true); | |
| 37 | + const data = await res.json(); | |
| 38 | + expect(Array.isArray(data.agents)).toBe(true); | |
| 39 | +}); | |
| 40 | + | |
| 41 | +test('POST /v1/agents/register creates agent and returns credentials', async ({ request }) => { | |
| 42 | + const nick = `e2e-api-${Date.now()}`; | |
| 43 | + const res = await request.post(`${BASE}/v1/agents/register`, { | |
| 44 | + headers: headers(), | |
| 45 | + data: { nick, type: 'worker', channels: ['#e2e-test'] }, | |
| 46 | + }); | |
| 47 | + expect(res.status()).toBe(201); | |
| 48 | + const data = await res.json(); | |
| 49 | + expect(data.credentials.nick).toBe(nick); | |
| 50 | + expect(data.credentials.passphrase).toBeTruthy(); | |
| 51 | + | |
| 52 | + // Clean up. | |
| 53 | + await request.delete(`${BASE}/v1/agents/${nick}`, { headers: headers() }); | |
| 54 | +}); | |
| 55 | + | |
| 56 | +test('POST /v1/agents/register with skills stores them', async ({ request }) => { | |
| 57 | + const nick = `e2e-skills-${Date.now()}`; | |
| 58 | + const res = await request.post(`${BASE}/v1/agents/register`, { | |
| 59 | + headers: headers(), | |
| 60 | + data: { nick, type: 'worker', channels: ['#e2e-test'], skills: ['go', 'python'] }, | |
| 61 | + }); | |
| 62 | + expect(res.status()).toBe(201); | |
| 63 | + | |
| 64 | + const agent = await (await request.get(`${BASE}/v1/agents/${nick}`, { headers: headers() })).json(); | |
| 65 | + expect(agent.skills).toContain('go'); | |
| 66 | + expect(agent.skills).toContain('python'); | |
| 67 | + | |
| 68 | + await request.delete(`${BASE}/v1/agents/${nick}`, { headers: headers() }); | |
| 69 | +}); | |
| 70 | + | |
| 71 | +test('GET /v1/agents?skill= filters by capability', async ({ request }) => { | |
| 72 | + const nick = `e2e-skill-${Date.now()}`; | |
| 73 | + await request.post(`${BASE}/v1/agents/register`, { | |
| 74 | + headers: headers(), | |
| 75 | + data: { nick, type: 'worker', channels: ['#e2e-test'], skills: ['rare-skill-xyz'] }, | |
| 76 | + }); | |
| 77 | + | |
| 78 | + const res = await request.get(`${BASE}/v1/agents?skill=rare-skill-xyz`, { headers: headers() }); | |
| 79 | + const data = await res.json(); | |
| 80 | + expect(data.agents.some((a: any) => a.nick === nick)).toBe(true); | |
| 81 | + | |
| 82 | + await request.delete(`${BASE}/v1/agents/${nick}`, { headers: headers() }); | |
| 83 | +}); | |
| 84 | + | |
| 85 | +test('POST /v1/agents/bulk-delete removes multiple agents', async ({ request }) => { | |
| 86 | + const nicks = [`e2e-bulk-${Date.now()}-a`, `e2e-bulk-${Date.now()}-b`]; | |
| 87 | + for (const nick of nicks) { | |
| 88 | + await request.post(`${BASE}/v1/agents/register`, { | |
| 89 | + headers: headers(), | |
| 90 | + data: { nick, type: 'worker', channels: ['#e2e-test'] }, | |
| 91 | + }); | |
| 92 | + } | |
| 93 | + | |
| 94 | + const res = await request.post(`${BASE}/v1/agents/bulk-delete`, { | |
| 95 | + headers: headers(), | |
| 96 | + data: { nicks }, | |
| 97 | + }); | |
| 98 | + expect(res.ok()).toBe(true); | |
| 99 | + const data = await res.json(); | |
| 100 | + expect(data.deleted).toBe(2); | |
| 101 | +}); | |
| 102 | + | |
| 103 | +// --- Channels --- | |
| 104 | + | |
| 105 | +test('GET /v1/channels returns channel list', async ({ request }) => { | |
| 106 | + const res = await request.get(`${BASE}/v1/channels`, { headers: headers() }); | |
| 107 | + expect(res.ok()).toBe(true); | |
| 108 | + const data = await res.json(); | |
| 109 | + expect(Array.isArray(data.channels)).toBe(true); | |
| 110 | +}); | |
| 111 | + | |
| 112 | +test('POST /v1/channels/{ch}/messages sends message', async ({ request }) => { | |
| 113 | + const res = await request.post(`${BASE}/v1/channels/general/messages`, { | |
| 114 | + headers: headers(), | |
| 115 | + data: { text: `e2e test message ${Date.now()}`, nick: 'e2e-test' }, | |
| 116 | + }); | |
| 117 | + expect(res.status()).toBeLessThan(300); | |
| 118 | +}); | |
| 119 | + | |
| 120 | +test('GET /v1/channels/{ch}/users returns user info with modes', async ({ request }) => { | |
| 121 | + const res = await request.get(`${BASE}/v1/channels/general/users`, { headers: headers() }); | |
| 122 | + expect(res.ok()).toBe(true); | |
| 123 | + const data = await res.json(); | |
| 124 | + expect(Array.isArray(data.users)).toBe(true); | |
| 125 | + expect(typeof data.channel_modes).toBe('string'); | |
| 126 | +}); | |
| 127 | + | |
| 128 | +// --- Settings --- | |
| 129 | + | |
| 130 | +test('GET /v1/settings returns policies and bot_commands', async ({ request }) => { | |
| 131 | + const res = await request.get(`${BASE}/v1/settings`, { headers: headers() }); | |
| 132 | + expect(res.ok()).toBe(true); | |
| 133 | + const data = await res.json(); | |
| 134 | + expect(data.policies).toBeTruthy(); | |
| 135 | + expect(data.bot_commands).toBeTruthy(); | |
| 136 | + expect(data.bot_commands.oracle).toBeTruthy(); | |
| 137 | + expect(data.bot_commands.shepherd).toBeTruthy(); | |
| 138 | +}); | |
| 139 | + | |
| 140 | +// --- Topology --- | |
| 141 | + | |
| 142 | +test('GET /v1/topology returns types and static channels', async ({ request }) => { | |
| 143 | + const res = await request.get(`${BASE}/v1/topology`, { headers: headers() }); | |
| 144 | + expect(res.ok()).toBe(true); | |
| 145 | + const data = await res.json(); | |
| 146 | + expect(Array.isArray(data.static_channels)).toBe(true); | |
| 147 | + expect(Array.isArray(data.types)).toBe(true); | |
| 148 | +}); | |
| 149 | + | |
| 150 | +// --- Config --- | |
| 151 | + | |
| 152 | +test('GET /v1/config returns server config', async ({ request }) => { | |
| 153 | + const res = await request.get(`${BASE}/v1/config`, { headers: headers() }); | |
| 154 | + expect(res.ok()).toBe(true); | |
| 155 | + const data = await res.json(); | |
| 156 | + expect(data.ergo).toBeTruthy(); | |
| 157 | + expect(data.bridge).toBeTruthy(); | |
| 158 | +}); | |
| 159 | + | |
| 160 | +// --- API keys --- | |
| 161 | + | |
| 162 | +test('GET /v1/api-keys returns key list', async ({ request }) => { | |
| 163 | + const res = await request.get(`${BASE}/v1/api-keys`, { headers: headers() }); | |
| 164 | + expect(res.ok()).toBe(true); | |
| 165 | + const data = await res.json(); | |
| 166 | + expect(Array.isArray(data)).toBe(true); | |
| 167 | + expect(data.length).toBeGreaterThan(0); // at least the server key | |
| 168 | +}); | |
| 169 | + | |
| 170 | +test('POST /v1/api-keys creates key and DELETE revokes it', async ({ request }) => { | |
| 171 | + const create = await request.post(`${BASE}/v1/api-keys`, { | |
| 172 | + headers: headers(), | |
| 173 | + data: { name: `e2e-key-${Date.now()}`, scopes: ['read'] }, | |
| 174 | + }); | |
| 175 | + expect(create.status()).toBe(201); | |
| 176 | + const key = await create.json(); | |
| 177 | + expect(key.token).toBeTruthy(); | |
| 178 | + expect(key.id).toBeTruthy(); | |
| 179 | + | |
| 180 | + // Revoke. | |
| 181 | + const revoke = await request.delete(`${BASE}/v1/api-keys/${key.id}`, { headers: headers() }); | |
| 182 | + expect(revoke.status()).toBe(204); | |
| 183 | +}); | |
| 184 | + | |
| 185 | +// --- Instructions --- | |
| 186 | + | |
| 187 | +test('PUT/GET/DELETE /v1/channels/{ch}/instructions round-trip', async ({ request }) => { | |
| 188 | + const ch = 'e2e-instr'; | |
| 189 | + // Set. | |
| 190 | + const put = await request.put(`${BASE}/v1/channels/${ch}/instructions`, { | |
| 191 | + headers: headers(), | |
| 192 | + data: { instructions: 'Welcome {nick} to {channel}!' }, | |
| 193 | + }); | |
| 194 | + expect(put.status()).toBe(204); | |
| 195 | + | |
| 196 | + // Get. | |
| 197 | + const get = await request.get(`${BASE}/v1/channels/${ch}/instructions`, { headers: headers() }); | |
| 198 | + const data = await get.json(); | |
| 199 | + expect(data.instructions).toContain('Welcome {nick}'); | |
| 200 | + | |
| 201 | + // Delete. | |
| 202 | + const del = await request.delete(`${BASE}/v1/channels/${ch}/instructions`, { headers: headers() }); | |
| 203 | + expect(del.status()).toBe(204); | |
| 204 | +}); | |
| 205 | + | |
| 206 | +// --- Auth scoping --- | |
| 207 | + | |
| 208 | +test('read-only key cannot create agents', async ({ request }) => { | |
| 209 | + // Create a read-only key. | |
| 210 | + const create = await request.post(`${BASE}/v1/api-keys`, { | |
| 211 | + headers: headers(), | |
| 212 | + data: { name: `e2e-readonly-${Date.now()}`, scopes: ['read'] }, | |
| 213 | + }); | |
| 214 | + const key = await create.json(); | |
| 215 | + | |
| 216 | + // Try to register an agent with it. | |
| 217 | + const res = await request.post(`${BASE}/v1/agents/register`, { | |
| 218 | + headers: { Authorization: `Bearer ${key.token}`, 'Content-Type': 'application/json' }, | |
| 219 | + data: { nick: 'should-fail', type: 'worker' }, | |
| 220 | + }); | |
| 221 | + expect(res.status()).toBe(403); | |
| 222 | + | |
| 223 | + // Clean up. | |
| 224 | + await request.delete(`${BASE}/v1/api-keys/${key.id}`, { headers: headers() }); | |
| 225 | +}); | |
| 226 | + | |
| 227 | +// --- Blocker escalation --- | |
| 228 | + | |
| 229 | +test('POST /v1/agents/{nick}/blocker accepts alert', async ({ request }) => { | |
| 230 | + const res = await request.post(`${BASE}/v1/agents/test-agent/blocker`, { | |
| 231 | + headers: headers(), | |
| 232 | + data: { message: 'stuck on database migration', channel: '#test' }, | |
| 233 | + }); | |
| 234 | + expect(res.status()).toBe(204); | |
| 235 | +}); |
| --- a/tests/e2e/tests/api.spec.ts | |
| +++ b/tests/e2e/tests/api.spec.ts | |
| @@ -0,0 +1,235 @@ | |
| --- a/tests/e2e/tests/api.spec.ts | |
| +++ b/tests/e2e/tests/api.spec.ts | |
| @@ -0,0 +1,235 @@ | |
| 1 | import { test, expect } from '@playwright/test'; |
| 2 | |
| 3 | const BASE = process.env.SB_BASE_URL || 'http://localhost:8080'; |
| 4 | const TOKEN = process.env.SB_TOKEN || ''; |
| 5 | |
| 6 | function headers() { |
| 7 | return { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }; |
| 8 | } |
| 9 | |
| 10 | test.beforeEach(() => { |
| 11 | test.skip(!TOKEN, 'SB_TOKEN not set'); |
| 12 | }); |
| 13 | |
| 14 | // --- Status --- |
| 15 | |
| 16 | test('GET /v1/status returns ok', async ({ request }) => { |
| 17 | const res = await request.get(`${BASE}/v1/status`, { headers: headers() }); |
| 18 | expect(res.ok()).toBe(true); |
| 19 | const data = await res.json(); |
| 20 | expect(data.status).toBe('ok'); |
| 21 | expect(data.uptime).toBeTruthy(); |
| 22 | }); |
| 23 | |
| 24 | test('GET /v1/metrics returns runtime stats', async ({ request }) => { |
| 25 | const res = await request.get(`${BASE}/v1/metrics`, { headers: headers() }); |
| 26 | expect(res.ok()).toBe(true); |
| 27 | const data = await res.json(); |
| 28 | expect(data.runtime).toBeTruthy(); |
| 29 | expect(data.runtime.goroutines).toBeGreaterThan(0); |
| 30 | }); |
| 31 | |
| 32 | // --- Agents --- |
| 33 | |
| 34 | test('GET /v1/agents returns agent list', async ({ request }) => { |
| 35 | const res = await request.get(`${BASE}/v1/agents`, { headers: headers() }); |
| 36 | expect(res.ok()).toBe(true); |
| 37 | const data = await res.json(); |
| 38 | expect(Array.isArray(data.agents)).toBe(true); |
| 39 | }); |
| 40 | |
| 41 | test('POST /v1/agents/register creates agent and returns credentials', async ({ request }) => { |
| 42 | const nick = `e2e-api-${Date.now()}`; |
| 43 | const res = await request.post(`${BASE}/v1/agents/register`, { |
| 44 | headers: headers(), |
| 45 | data: { nick, type: 'worker', channels: ['#e2e-test'] }, |
| 46 | }); |
| 47 | expect(res.status()).toBe(201); |
| 48 | const data = await res.json(); |
| 49 | expect(data.credentials.nick).toBe(nick); |
| 50 | expect(data.credentials.passphrase).toBeTruthy(); |
| 51 | |
| 52 | // Clean up. |
| 53 | await request.delete(`${BASE}/v1/agents/${nick}`, { headers: headers() }); |
| 54 | }); |
| 55 | |
| 56 | test('POST /v1/agents/register with skills stores them', async ({ request }) => { |
| 57 | const nick = `e2e-skills-${Date.now()}`; |
| 58 | const res = await request.post(`${BASE}/v1/agents/register`, { |
| 59 | headers: headers(), |
| 60 | data: { nick, type: 'worker', channels: ['#e2e-test'], skills: ['go', 'python'] }, |
| 61 | }); |
| 62 | expect(res.status()).toBe(201); |
| 63 | |
| 64 | const agent = await (await request.get(`${BASE}/v1/agents/${nick}`, { headers: headers() })).json(); |
| 65 | expect(agent.skills).toContain('go'); |
| 66 | expect(agent.skills).toContain('python'); |
| 67 | |
| 68 | await request.delete(`${BASE}/v1/agents/${nick}`, { headers: headers() }); |
| 69 | }); |
| 70 | |
| 71 | test('GET /v1/agents?skill= filters by capability', async ({ request }) => { |
| 72 | const nick = `e2e-skill-${Date.now()}`; |
| 73 | await request.post(`${BASE}/v1/agents/register`, { |
| 74 | headers: headers(), |
| 75 | data: { nick, type: 'worker', channels: ['#e2e-test'], skills: ['rare-skill-xyz'] }, |
| 76 | }); |
| 77 | |
| 78 | const res = await request.get(`${BASE}/v1/agents?skill=rare-skill-xyz`, { headers: headers() }); |
| 79 | const data = await res.json(); |
| 80 | expect(data.agents.some((a: any) => a.nick === nick)).toBe(true); |
| 81 | |
| 82 | await request.delete(`${BASE}/v1/agents/${nick}`, { headers: headers() }); |
| 83 | }); |
| 84 | |
| 85 | test('POST /v1/agents/bulk-delete removes multiple agents', async ({ request }) => { |
| 86 | const nicks = [`e2e-bulk-${Date.now()}-a`, `e2e-bulk-${Date.now()}-b`]; |
| 87 | for (const nick of nicks) { |
| 88 | await request.post(`${BASE}/v1/agents/register`, { |
| 89 | headers: headers(), |
| 90 | data: { nick, type: 'worker', channels: ['#e2e-test'] }, |
| 91 | }); |
| 92 | } |
| 93 | |
| 94 | const res = await request.post(`${BASE}/v1/agents/bulk-delete`, { |
| 95 | headers: headers(), |
| 96 | data: { nicks }, |
| 97 | }); |
| 98 | expect(res.ok()).toBe(true); |
| 99 | const data = await res.json(); |
| 100 | expect(data.deleted).toBe(2); |
| 101 | }); |
| 102 | |
| 103 | // --- Channels --- |
| 104 | |
| 105 | test('GET /v1/channels returns channel list', async ({ request }) => { |
| 106 | const res = await request.get(`${BASE}/v1/channels`, { headers: headers() }); |
| 107 | expect(res.ok()).toBe(true); |
| 108 | const data = await res.json(); |
| 109 | expect(Array.isArray(data.channels)).toBe(true); |
| 110 | }); |
| 111 | |
| 112 | test('POST /v1/channels/{ch}/messages sends message', async ({ request }) => { |
| 113 | const res = await request.post(`${BASE}/v1/channels/general/messages`, { |
| 114 | headers: headers(), |
| 115 | data: { text: `e2e test message ${Date.now()}`, nick: 'e2e-test' }, |
| 116 | }); |
| 117 | expect(res.status()).toBeLessThan(300); |
| 118 | }); |
| 119 | |
| 120 | test('GET /v1/channels/{ch}/users returns user info with modes', async ({ request }) => { |
| 121 | const res = await request.get(`${BASE}/v1/channels/general/users`, { headers: headers() }); |
| 122 | expect(res.ok()).toBe(true); |
| 123 | const data = await res.json(); |
| 124 | expect(Array.isArray(data.users)).toBe(true); |
| 125 | expect(typeof data.channel_modes).toBe('string'); |
| 126 | }); |
| 127 | |
| 128 | // --- Settings --- |
| 129 | |
| 130 | test('GET /v1/settings returns policies and bot_commands', async ({ request }) => { |
| 131 | const res = await request.get(`${BASE}/v1/settings`, { headers: headers() }); |
| 132 | expect(res.ok()).toBe(true); |
| 133 | const data = await res.json(); |
| 134 | expect(data.policies).toBeTruthy(); |
| 135 | expect(data.bot_commands).toBeTruthy(); |
| 136 | expect(data.bot_commands.oracle).toBeTruthy(); |
| 137 | expect(data.bot_commands.shepherd).toBeTruthy(); |
| 138 | }); |
| 139 | |
| 140 | // --- Topology --- |
| 141 | |
| 142 | test('GET /v1/topology returns types and static channels', async ({ request }) => { |
| 143 | const res = await request.get(`${BASE}/v1/topology`, { headers: headers() }); |
| 144 | expect(res.ok()).toBe(true); |
| 145 | const data = await res.json(); |
| 146 | expect(Array.isArray(data.static_channels)).toBe(true); |
| 147 | expect(Array.isArray(data.types)).toBe(true); |
| 148 | }); |
| 149 | |
| 150 | // --- Config --- |
| 151 | |
| 152 | test('GET /v1/config returns server config', async ({ request }) => { |
| 153 | const res = await request.get(`${BASE}/v1/config`, { headers: headers() }); |
| 154 | expect(res.ok()).toBe(true); |
| 155 | const data = await res.json(); |
| 156 | expect(data.ergo).toBeTruthy(); |
| 157 | expect(data.bridge).toBeTruthy(); |
| 158 | }); |
| 159 | |
| 160 | // --- API keys --- |
| 161 | |
| 162 | test('GET /v1/api-keys returns key list', async ({ request }) => { |
| 163 | const res = await request.get(`${BASE}/v1/api-keys`, { headers: headers() }); |
| 164 | expect(res.ok()).toBe(true); |
| 165 | const data = await res.json(); |
| 166 | expect(Array.isArray(data)).toBe(true); |
| 167 | expect(data.length).toBeGreaterThan(0); // at least the server key |
| 168 | }); |
| 169 | |
| 170 | test('POST /v1/api-keys creates key and DELETE revokes it', async ({ request }) => { |
| 171 | const create = await request.post(`${BASE}/v1/api-keys`, { |
| 172 | headers: headers(), |
| 173 | data: { name: `e2e-key-${Date.now()}`, scopes: ['read'] }, |
| 174 | }); |
| 175 | expect(create.status()).toBe(201); |
| 176 | const key = await create.json(); |
| 177 | expect(key.token).toBeTruthy(); |
| 178 | expect(key.id).toBeTruthy(); |
| 179 | |
| 180 | // Revoke. |
| 181 | const revoke = await request.delete(`${BASE}/v1/api-keys/${key.id}`, { headers: headers() }); |
| 182 | expect(revoke.status()).toBe(204); |
| 183 | }); |
| 184 | |
| 185 | // --- Instructions --- |
| 186 | |
| 187 | test('PUT/GET/DELETE /v1/channels/{ch}/instructions round-trip', async ({ request }) => { |
| 188 | const ch = 'e2e-instr'; |
| 189 | // Set. |
| 190 | const put = await request.put(`${BASE}/v1/channels/${ch}/instructions`, { |
| 191 | headers: headers(), |
| 192 | data: { instructions: 'Welcome {nick} to {channel}!' }, |
| 193 | }); |
| 194 | expect(put.status()).toBe(204); |
| 195 | |
| 196 | // Get. |
| 197 | const get = await request.get(`${BASE}/v1/channels/${ch}/instructions`, { headers: headers() }); |
| 198 | const data = await get.json(); |
| 199 | expect(data.instructions).toContain('Welcome {nick}'); |
| 200 | |
| 201 | // Delete. |
| 202 | const del = await request.delete(`${BASE}/v1/channels/${ch}/instructions`, { headers: headers() }); |
| 203 | expect(del.status()).toBe(204); |
| 204 | }); |
| 205 | |
| 206 | // --- Auth scoping --- |
| 207 | |
| 208 | test('read-only key cannot create agents', async ({ request }) => { |
| 209 | // Create a read-only key. |
| 210 | const create = await request.post(`${BASE}/v1/api-keys`, { |
| 211 | headers: headers(), |
| 212 | data: { name: `e2e-readonly-${Date.now()}`, scopes: ['read'] }, |
| 213 | }); |
| 214 | const key = await create.json(); |
| 215 | |
| 216 | // Try to register an agent with it. |
| 217 | const res = await request.post(`${BASE}/v1/agents/register`, { |
| 218 | headers: { Authorization: `Bearer ${key.token}`, 'Content-Type': 'application/json' }, |
| 219 | data: { nick: 'should-fail', type: 'worker' }, |
| 220 | }); |
| 221 | expect(res.status()).toBe(403); |
| 222 | |
| 223 | // Clean up. |
| 224 | await request.delete(`${BASE}/v1/api-keys/${key.id}`, { headers: headers() }); |
| 225 | }); |
| 226 | |
| 227 | // --- Blocker escalation --- |
| 228 | |
| 229 | test('POST /v1/agents/{nick}/blocker accepts alert', async ({ request }) => { |
| 230 | const res = await request.post(`${BASE}/v1/agents/test-agent/blocker`, { |
| 231 | headers: headers(), |
| 232 | data: { message: 'stuck on database migration', channel: '#test' }, |
| 233 | }); |
| 234 | expect(res.status()).toBe(204); |
| 235 | }); |