@studio-foundation/api 0.3.0-beta.1 → 0.3.0-beta.6

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.
Files changed (62) hide show
  1. package/package.json +7 -4
  2. package/ARCHITECTURE.md +0 -52
  3. package/src/.gitkeep +0 -0
  4. package/src/api.ts +0 -8
  5. package/src/bootstrap.ts +0 -259
  6. package/src/event-bus.ts +0 -64
  7. package/src/index.ts +0 -58
  8. package/src/integration-runtime.ts +0 -180
  9. package/src/integration-store.ts +0 -125
  10. package/src/integrations/linear/failure-handler.ts +0 -93
  11. package/src/integrations/linear/webhook-handler.ts +0 -156
  12. package/src/integrations/registry.ts +0 -12
  13. package/src/integrations/types.ts +0 -37
  14. package/src/launcher.ts +0 -214
  15. package/src/logger.ts +0 -50
  16. package/src/plans.ts +0 -43
  17. package/src/routes/agents.ts +0 -131
  18. package/src/routes/config.ts +0 -154
  19. package/src/routes/contracts.ts +0 -205
  20. package/src/routes/pipelines.ts +0 -146
  21. package/src/routes/projects.ts +0 -237
  22. package/src/routes/runs.ts +0 -468
  23. package/src/routes/skills.ts +0 -136
  24. package/src/routes/tools.ts +0 -222
  25. package/src/routes/users.ts +0 -169
  26. package/src/routes/validate.ts +0 -196
  27. package/src/routes/webhooks.ts +0 -120
  28. package/src/server.ts +0 -155
  29. package/src/spawners/http-api-spawner.ts +0 -96
  30. package/src/user-store-pg.ts +0 -138
  31. package/src/user-store.ts +0 -125
  32. package/src/utils/repo-resolver.ts +0 -3
  33. package/src/webhook-dispatcher.ts +0 -142
  34. package/src/webhook-store.ts +0 -141
  35. package/tests/agents.test.ts +0 -164
  36. package/tests/cancel.test.ts +0 -120
  37. package/tests/config.test.ts +0 -196
  38. package/tests/contracts.test.ts +0 -302
  39. package/tests/event-bus.test.ts +0 -53
  40. package/tests/http-api-spawner.test.ts +0 -158
  41. package/tests/integration-runtime.test.ts +0 -199
  42. package/tests/integration-store.test.ts +0 -66
  43. package/tests/integrations/linear/failure-handler.test.ts +0 -113
  44. package/tests/integrations/linear/webhook-handler.test.ts +0 -191
  45. package/tests/launcher.test.ts +0 -380
  46. package/tests/linear-webhook.test.ts +0 -390
  47. package/tests/pipelines.test.ts +0 -166
  48. package/tests/projects.test.ts +0 -283
  49. package/tests/runs.test.ts +0 -379
  50. package/tests/server.test.ts +0 -208
  51. package/tests/skills.test.ts +0 -149
  52. package/tests/sse.test.ts +0 -129
  53. package/tests/tools.test.ts +0 -233
  54. package/tests/user-store.test.ts +0 -93
  55. package/tests/users.test.ts +0 -189
  56. package/tests/utils/repo-resolver.test.ts +0 -105
  57. package/tests/validate.test.ts +0 -233
  58. package/tests/webhook-dispatcher.test.ts +0 -214
  59. package/tests/webhook-store.test.ts +0 -98
  60. package/tests/webhooks.test.ts +0 -176
  61. package/tsconfig.json +0 -24
  62. package/vitest.config.ts +0 -7
@@ -1,164 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { buildServer } from '../src/server.js';
5
- import { InMemoryRunStore } from '@studio-foundation/engine';
6
- import type { IntegrationRuntime } from '../src/integration-runtime.js';
7
- import type { IntegrationStore } from '../src/integration-store.js';
8
-
9
- const TMP = resolve('/tmp', `.studio-agents-test-${Date.now()}`);
10
- const AGENTS_DIR = resolve(TMP, 'agents');
11
-
12
- const nullIntegrationRuntime = { registerRoutes: () => {} } as unknown as IntegrationRuntime;
13
- const nullIntegrationStore = {} as unknown as IntegrationStore;
14
-
15
- function makeServer() {
16
- return buildServer({
17
- store: new InMemoryRunStore(),
18
- launcher: { launch: async () => ({ run_id: 'x' }), cancel: async () => {} },
19
- configsDir: TMP,
20
- projectName: 'test-project',
21
- apiConfig: {},
22
- studioVersion: '0.0.0-test',
23
- maskedConfig: { providers: [] },
24
- integrationRuntime: nullIntegrationRuntime,
25
- integrationStore: nullIntegrationStore,
26
- });
27
- }
28
-
29
- beforeAll(() => {
30
- mkdirSync(AGENTS_DIR, { recursive: true });
31
- writeFileSync(
32
- resolve(AGENTS_DIR, 'analyst.agent.yaml'),
33
- 'name: analyst\nprovider: anthropic\nmodel: claude-sonnet-4-20250514\n'
34
- );
35
- writeFileSync(
36
- resolve(AGENTS_DIR, 'coder.agent.yaml'),
37
- 'name: coder\nprovider: anthropic\nmodel: claude-sonnet-4-20250514\ntemperature: 0.2\n'
38
- );
39
- writeFileSync(resolve(AGENTS_DIR, 'ignored.yaml'), ''); // must be ignored
40
- });
41
-
42
- afterAll(() => {
43
- rmSync(TMP, { recursive: true, force: true });
44
- });
45
-
46
- describe('GET /api/agents', () => {
47
- it('returns only *.agent.yaml files as agent names', async () => {
48
- const server = makeServer();
49
- const res = await server.inject({ method: 'GET', url: '/api/agents' });
50
- expect(res.statusCode).toBe(200);
51
- const { agents } = res.json() as { agents: string[] };
52
- expect(agents).toContain('analyst');
53
- expect(agents).toContain('coder');
54
- expect(agents).not.toContain('ignored');
55
- expect(agents).not.toContain('analyst.agent.yaml');
56
- });
57
-
58
- it('returns empty array when agents dir is missing', async () => {
59
- const server = buildServer({
60
- store: new InMemoryRunStore(),
61
- launcher: { launch: async () => ({ run_id: 'x' }), cancel: async () => {} },
62
- configsDir: resolve('/tmp', `.studio-no-agents-${Date.now()}`),
63
- projectName: 'test-project',
64
- apiConfig: {},
65
- studioVersion: '0.0.0-test',
66
- maskedConfig: { providers: [] },
67
- integrationRuntime: nullIntegrationRuntime,
68
- integrationStore: nullIntegrationStore,
69
- });
70
- const res = await server.inject({ method: 'GET', url: '/api/agents' });
71
- expect(res.statusCode).toBe(200);
72
- expect((res.json() as { agents: string[] }).agents).toEqual([]);
73
- });
74
- });
75
-
76
- describe('GET /api/agents/:name', () => {
77
- it('returns parsed agent content as JSON', async () => {
78
- const server = makeServer();
79
- const res = await server.inject({ method: 'GET', url: '/api/agents/analyst' });
80
- expect(res.statusCode).toBe(200);
81
- const body = res.json() as { name: string; provider: string; model: string };
82
- expect(body.name).toBe('analyst');
83
- expect(body.provider).toBe('anthropic');
84
- expect(body.model).toBe('claude-sonnet-4-20250514');
85
- });
86
-
87
- it('returns nested fields from YAML', async () => {
88
- const server = makeServer();
89
- const res = await server.inject({ method: 'GET', url: '/api/agents/coder' });
90
- expect(res.statusCode).toBe(200);
91
- const body = res.json() as { temperature: number };
92
- expect(body.temperature).toBe(0.2);
93
- });
94
-
95
- it('returns 404 for unknown agent', async () => {
96
- const server = makeServer();
97
- const res = await server.inject({ method: 'GET', url: '/api/agents/nonexistent' });
98
- expect(res.statusCode).toBe(404);
99
- expect((res.json() as { error: string }).error).toBe('Agent not found');
100
- });
101
- });
102
-
103
- describe('PUT /api/agents/:name', () => {
104
- it('creates a new agent file', async () => {
105
- const server = makeServer();
106
- const res = await server.inject({
107
- method: 'PUT',
108
- url: '/api/agents/new-agent',
109
- payload: { name: 'new-agent', provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
110
- });
111
- expect(res.statusCode).toBe(200);
112
- // Verify it can be read back
113
- const getRes = await server.inject({ method: 'GET', url: '/api/agents/new-agent' });
114
- expect(getRes.statusCode).toBe(200);
115
- expect((getRes.json() as { model: string }).model).toBe('claude-haiku-4-5-20251001');
116
- });
117
-
118
- it('updates an existing agent', async () => {
119
- const server = makeServer();
120
- const res = await server.inject({
121
- method: 'PUT',
122
- url: '/api/agents/analyst',
123
- payload: { name: 'analyst', provider: 'anthropic', model: 'claude-opus-4-6' },
124
- });
125
- expect(res.statusCode).toBe(200);
126
- // Verify model updated
127
- const getRes = await server.inject({ method: 'GET', url: '/api/agents/analyst' });
128
- expect((getRes.json() as { model: string }).model).toBe('claude-opus-4-6');
129
- });
130
-
131
- it('returns 400 when name field is missing', async () => {
132
- const server = makeServer();
133
- const res = await server.inject({
134
- method: 'PUT',
135
- url: '/api/agents/foo',
136
- payload: { provider: 'anthropic' },
137
- });
138
- expect(res.statusCode).toBe(400);
139
- });
140
- });
141
-
142
- describe('DELETE /api/agents/:name', () => {
143
- it('deletes an agent and returns 204', async () => {
144
- const server = makeServer();
145
- // Create it first via PUT
146
- await server.inject({
147
- method: 'PUT',
148
- url: '/api/agents/to-delete',
149
- payload: { name: 'to-delete', provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
150
- });
151
- const res = await server.inject({ method: 'DELETE', url: '/api/agents/to-delete' });
152
- expect(res.statusCode).toBe(204);
153
- // Verify it's gone
154
- const getRes = await server.inject({ method: 'GET', url: '/api/agents/to-delete' });
155
- expect(getRes.statusCode).toBe(404);
156
- });
157
-
158
- it('returns 404 for nonexistent agent', async () => {
159
- const server = makeServer();
160
- const res = await server.inject({ method: 'DELETE', url: '/api/agents/nonexistent' });
161
- expect(res.statusCode).toBe(404);
162
- expect((res.json() as { error: string }).error).toBe('Agent not found');
163
- });
164
- });
@@ -1,120 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { buildServer } from '../src/server.js';
3
- import { InMemoryRunStore } from '@studio-foundation/engine';
4
- import type { RunLauncher } from '../src/launcher.js';
5
- import type { PipelineRun } from '@studio-foundation/contracts';
6
- import type { IntegrationRuntime } from '../src/integration-runtime.js';
7
- import type { IntegrationStore } from '../src/integration-store.js';
8
-
9
- function makeRun(overrides: Partial<PipelineRun> = {}): PipelineRun {
10
- return {
11
- id: 'run-abc',
12
- pipeline_name: 'test-pipeline',
13
- status: 'running',
14
- started_at: '2026-01-01T10:00:00Z',
15
- stages: [],
16
- ...overrides,
17
- } as PipelineRun;
18
- }
19
-
20
- const nullIntegrationRuntime = { registerRoutes: () => {} } as unknown as IntegrationRuntime;
21
- const nullIntegrationStore = {} as unknown as IntegrationStore;
22
-
23
- function makeServer(store = new InMemoryRunStore(), launcher?: Partial<RunLauncher>) {
24
- return buildServer({
25
- store,
26
- launcher: {
27
- launch: vi.fn().mockResolvedValue({ run_id: 'new-run' }),
28
- cancel: vi.fn().mockResolvedValue(undefined),
29
- subscribe: vi.fn().mockReturnValue(() => {}),
30
- ...launcher,
31
- } as RunLauncher,
32
- configsDir: '/tmp/.studio',
33
- projectName: 'test',
34
- apiConfig: {},
35
- integrationRuntime: nullIntegrationRuntime,
36
- integrationStore: nullIntegrationStore,
37
- } as any);
38
- }
39
-
40
- describe('POST /api/runs/:id/cancel', () => {
41
- it('returns 200 with run_id when run is running', async () => {
42
- const store = new InMemoryRunStore();
43
- const cancelFn = vi.fn().mockResolvedValue(undefined);
44
- store.savePipelineRun(makeRun({ id: 'run-1', status: 'running' }));
45
- const server = makeServer(store, { cancel: cancelFn });
46
-
47
- const res = await server.inject({ method: 'POST', url: '/api/runs/run-1/cancel' });
48
-
49
- expect(res.statusCode).toBe(200);
50
- expect(res.json()).toEqual({ run_id: 'run-1' });
51
- expect(cancelFn).toHaveBeenCalledWith('run-1');
52
- });
53
-
54
- it('returns 404 when run does not exist', async () => {
55
- const server = makeServer();
56
-
57
- const res = await server.inject({ method: 'POST', url: '/api/runs/nonexistent/cancel' });
58
-
59
- expect(res.statusCode).toBe(404);
60
- expect(res.json().error).toMatch(/not found/i);
61
- });
62
-
63
- it('returns 409 when run is already success', async () => {
64
- const store = new InMemoryRunStore();
65
- store.savePipelineRun(makeRun({ id: 'run-done', status: 'success' }));
66
- const server = makeServer(store);
67
-
68
- const res = await server.inject({ method: 'POST', url: '/api/runs/run-done/cancel' });
69
-
70
- expect(res.statusCode).toBe(409);
71
- expect(res.json().error).toMatch(/not cancellable/i);
72
- });
73
-
74
- it('returns 409 for failed and cancelled statuses', async () => {
75
- for (const status of ['failed', 'cancelled'] as const) {
76
- const store = new InMemoryRunStore();
77
- store.savePipelineRun(makeRun({ id: 'run-x', status }));
78
- const server = makeServer(store);
79
-
80
- const res = await server.inject({ method: 'POST', url: '/api/runs/run-x/cancel' });
81
-
82
- expect(res.statusCode).toBe(409);
83
- }
84
- });
85
- });
86
-
87
- describe('DELETE /api/runs/:id', () => {
88
- it('returns 200 with run_id when run is running', async () => {
89
- const store = new InMemoryRunStore();
90
- const cancelFn = vi.fn().mockResolvedValue(undefined);
91
- store.savePipelineRun(makeRun({ id: 'run-del-1', status: 'running' }));
92
- const server = makeServer(store, { cancel: cancelFn });
93
-
94
- const res = await server.inject({ method: 'DELETE', url: '/api/runs/run-del-1' });
95
-
96
- expect(res.statusCode).toBe(200);
97
- expect(res.json()).toEqual({ run_id: 'run-del-1' });
98
- expect(cancelFn).toHaveBeenCalledWith('run-del-1');
99
- });
100
-
101
- it('returns 404 when run does not exist', async () => {
102
- const server = makeServer();
103
-
104
- const res = await server.inject({ method: 'DELETE', url: '/api/runs/nonexistent' });
105
-
106
- expect(res.statusCode).toBe(404);
107
- expect(res.json().error).toMatch(/not found/i);
108
- });
109
-
110
- it('returns 409 when run is already terminal', async () => {
111
- const store = new InMemoryRunStore();
112
- store.savePipelineRun(makeRun({ id: 'run-done', status: 'success' }));
113
- const server = makeServer(store);
114
-
115
- const res = await server.inject({ method: 'DELETE', url: '/api/runs/run-done' });
116
-
117
- expect(res.statusCode).toBe(409);
118
- expect(res.json().error).toMatch(/not cancellable/i);
119
- });
120
- });
@@ -1,196 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterAll } from 'vitest';
2
- import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { buildServer } from '../src/server.js';
5
- import { InMemoryRunStore } from '@studio-foundation/engine';
6
- import type { IntegrationRuntime } from '../src/integration-runtime.js';
7
- import type { IntegrationStore } from '../src/integration-store.js';
8
-
9
- const BASE_CONFIG_YAML = `providers:
10
- anthropic:
11
- apiKey: \${ANTHROPIC_API_KEY}
12
- openai:
13
- apiKey: \${OPENAI_API_KEY}
14
- defaults:
15
- provider: anthropic
16
- model: claude-sonnet-4-20250514
17
- `;
18
-
19
- // Each describe block uses its own dir to avoid cross-test mutation
20
- const GET_TMP = resolve('/tmp', `.studio-config-get-test-${Date.now()}`);
21
- const PATCH_TMP = resolve('/tmp', `.studio-config-patch-test-${Date.now()}`);
22
- const POST_TMP = resolve('/tmp', `.studio-config-post-test-${Date.now()}`);
23
-
24
- const nullIntegrationRuntime = { registerRoutes: () => {} } as unknown as IntegrationRuntime;
25
- const nullIntegrationStore = {} as unknown as IntegrationStore;
26
-
27
- function makeServer(configsDir: string) {
28
- return buildServer({
29
- store: new InMemoryRunStore(),
30
- launcher: { launch: async () => ({ run_id: 'x' }), cancel: async () => {} },
31
- configsDir,
32
- projectName: 'test-project',
33
- apiConfig: {},
34
- studioVersion: '0.0.0-test',
35
- maskedConfig: { providers: [] },
36
- integrationRuntime: nullIntegrationRuntime,
37
- integrationStore: nullIntegrationStore,
38
- });
39
- }
40
-
41
- function writeConfig(dir: string, content = BASE_CONFIG_YAML) {
42
- mkdirSync(dir, { recursive: true });
43
- writeFileSync(resolve(dir, 'config.yaml'), content);
44
- }
45
-
46
- afterAll(() => {
47
- for (const dir of [GET_TMP, PATCH_TMP, POST_TMP]) {
48
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
49
- }
50
- });
51
-
52
- describe('GET /api/config', () => {
53
- beforeEach(() => writeConfig(GET_TMP));
54
-
55
- it('returns providers with API keys masked as ***', async () => {
56
- const server = makeServer(GET_TMP);
57
- const res = await server.inject({ method: 'GET', url: '/api/config' });
58
- expect(res.statusCode).toBe(200);
59
- const body = res.json() as { providers: Record<string, { apiKey: string }> };
60
- expect(body.providers['anthropic'].apiKey).toBe('***');
61
- expect(body.providers['openai'].apiKey).toBe('***');
62
- });
63
-
64
- it('returns defaults from config', async () => {
65
- const server = makeServer(GET_TMP);
66
- const res = await server.inject({ method: 'GET', url: '/api/config' });
67
- const body = res.json() as { defaults: { provider: string; model: string } };
68
- expect(body.defaults.provider).toBe('anthropic');
69
- expect(body.defaults.model).toBe('claude-sonnet-4-20250514');
70
- });
71
-
72
- it('never exposes raw API key values in the response', async () => {
73
- const server = makeServer(GET_TMP);
74
- const res = await server.inject({ method: 'GET', url: '/api/config' });
75
- // The env var reference itself must not appear — only *** is returned
76
- expect(res.payload).not.toContain('ANTHROPIC_API_KEY');
77
- expect(res.payload).not.toContain('OPENAI_API_KEY');
78
- });
79
-
80
- it('returns empty providers when config.yaml does not exist', async () => {
81
- const emptyDir = resolve('/tmp', `.studio-no-config-${Date.now()}`);
82
- const server = makeServer(emptyDir);
83
- const res = await server.inject({ method: 'GET', url: '/api/config' });
84
- expect(res.statusCode).toBe(200);
85
- const body = res.json() as { providers: Record<string, unknown> };
86
- expect(body.providers).toEqual({});
87
- });
88
- });
89
-
90
- describe('PATCH /api/config', () => {
91
- beforeEach(() => writeConfig(PATCH_TMP));
92
-
93
- it('updates defaults and returns masked config', async () => {
94
- const server = makeServer(PATCH_TMP);
95
- const res = await server.inject({
96
- method: 'PATCH',
97
- url: '/api/config',
98
- payload: { defaults: { provider: 'openai', model: 'gpt-4o' } },
99
- });
100
- expect(res.statusCode).toBe(200);
101
- const body = res.json() as { defaults: { provider: string; model: string } };
102
- expect(body.defaults.provider).toBe('openai');
103
- expect(body.defaults.model).toBe('gpt-4o');
104
- });
105
-
106
- it('preserves providers when patching only defaults', async () => {
107
- const server = makeServer(PATCH_TMP);
108
- const res = await server.inject({
109
- method: 'PATCH',
110
- url: '/api/config',
111
- payload: { defaults: { provider: 'openai', model: 'gpt-4o' } },
112
- });
113
- const body = res.json() as { providers: Record<string, { apiKey: string }> };
114
- expect(body.providers['anthropic'].apiKey).toBe('***');
115
- expect(body.providers['openai'].apiKey).toBe('***');
116
- });
117
-
118
- it('preserves unpatched fields within defaults', async () => {
119
- const server = makeServer(PATCH_TMP);
120
- const res = await server.inject({
121
- method: 'PATCH',
122
- url: '/api/config',
123
- payload: { defaults: { model: 'claude-opus-4-6' } },
124
- });
125
- expect(res.statusCode).toBe(200);
126
- const body = res.json() as { defaults: { provider: string; model: string } };
127
- // model updated
128
- expect(body.defaults.model).toBe('claude-opus-4-6');
129
- // provider preserved
130
- expect(body.defaults.provider).toBe('anthropic');
131
- });
132
-
133
- it('persists changes so a subsequent GET reflects them', async () => {
134
- const server = makeServer(PATCH_TMP);
135
- await server.inject({
136
- method: 'PATCH',
137
- url: '/api/config',
138
- payload: { defaults: { provider: 'openai', model: 'gpt-4o' } },
139
- });
140
- const getRes = await server.inject({ method: 'GET', url: '/api/config' });
141
- const body = getRes.json() as { defaults: { provider: string } };
142
- expect(body.defaults.provider).toBe('openai');
143
- });
144
- });
145
-
146
- describe('POST /api/config/providers', () => {
147
- beforeEach(() => writeConfig(POST_TMP));
148
-
149
- it('adds a provider with apiKey stored as env var reference', async () => {
150
- const server = makeServer(POST_TMP);
151
- const res = await server.inject({
152
- method: 'POST',
153
- url: '/api/config/providers',
154
- payload: { provider: 'anthropic', apiKeyEnvVar: 'MY_ANTHROPIC_KEY' },
155
- });
156
- expect(res.statusCode).toBe(200);
157
- const body = res.json() as { providers: Record<string, { apiKey: string }> };
158
- expect(body.providers['anthropic'].apiKey).toBe('***');
159
- });
160
-
161
- it('never stores the env var name in plain text in the response', async () => {
162
- const server = makeServer(POST_TMP);
163
- const res = await server.inject({
164
- method: 'POST',
165
- url: '/api/config/providers',
166
- payload: { provider: 'anthropic', apiKeyEnvVar: 'MY_ANTHROPIC_KEY' },
167
- });
168
- expect(res.payload).not.toContain('MY_ANTHROPIC_KEY');
169
- });
170
-
171
- it('updates an existing provider', async () => {
172
- const server = makeServer(POST_TMP);
173
- const res = await server.inject({
174
- method: 'POST',
175
- url: '/api/config/providers',
176
- payload: { provider: 'openai', apiKeyEnvVar: 'OPENAI_NEW_KEY' },
177
- });
178
- expect(res.statusCode).toBe(200);
179
- // GET should show openai still masked
180
- const getRes = await server.inject({ method: 'GET', url: '/api/config' });
181
- const body = getRes.json() as { providers: Record<string, { apiKey: string }> };
182
- expect(body.providers['openai'].apiKey).toBe('***');
183
- });
184
-
185
- it('returns 400 for unknown provider', async () => {
186
- const server = makeServer(POST_TMP);
187
- const res = await server.inject({
188
- method: 'POST',
189
- url: '/api/config/providers',
190
- payload: { provider: 'unknown-llm', apiKeyEnvVar: 'SOME_KEY' },
191
- });
192
- expect(res.statusCode).toBe(400);
193
- const body = res.json() as { error: string };
194
- expect(body.error).toBeTruthy();
195
- });
196
- });