@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.0

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 (78) hide show
  1. package/AUTHORING.md +102 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +100 -28
  4. package/bin/apifuse-check.ts +78 -71
  5. package/bin/apifuse-create.ts +12 -0
  6. package/bin/apifuse-dev.ts +24 -61
  7. package/bin/apifuse-pack-check.ts +47 -0
  8. package/bin/apifuse-perf.ts +33 -32
  9. package/bin/apifuse-record.ts +17 -7
  10. package/bin/apifuse-test.ts +6 -4
  11. package/bin/apifuse.ts +36 -35
  12. package/package.json +28 -9
  13. package/src/ceremonies/index.ts +747 -0
  14. package/src/cli/commands.ts +87 -0
  15. package/src/cli/create.ts +845 -0
  16. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  17. package/src/cli/templates/provider/README.md.tpl +28 -0
  18. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  19. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  20. package/src/cli/templates/provider/index.ts.tpl +54 -0
  21. package/src/cli/templates/provider/start.ts.tpl +5 -0
  22. package/src/composite.ts +43 -0
  23. package/src/define.ts +527 -41
  24. package/src/dev.ts +2 -6
  25. package/src/errors.ts +42 -0
  26. package/src/index.ts +50 -38
  27. package/src/lint.ts +574 -0
  28. package/src/provider.ts +14 -0
  29. package/src/runtime/auth-flow.ts +67 -0
  30. package/src/runtime/credential.ts +95 -0
  31. package/src/runtime/env.ts +13 -0
  32. package/src/runtime/executor.ts +13 -14
  33. package/src/runtime/http.ts +10 -2
  34. package/src/runtime/insights.ts +3 -3
  35. package/src/runtime/key-derivation.ts +122 -0
  36. package/src/runtime/keyring.ts +148 -0
  37. package/src/runtime/namespace.ts +33 -0
  38. package/src/runtime/prevalidate.ts +252 -0
  39. package/src/runtime/tls.ts +20 -5
  40. package/src/runtime/waterfall.ts +0 -1
  41. package/src/schema.ts +77 -0
  42. package/src/serve.ts +1 -664
  43. package/src/server/index.ts +22 -0
  44. package/src/server/serve.ts +610 -0
  45. package/src/server/types.ts +78 -0
  46. package/src/stealth/profiles.ts +10 -93
  47. package/src/testing/run.ts +391 -32
  48. package/src/types.ts +364 -41
  49. package/bin/apifuse-init.ts +0 -387
  50. package/src/__tests__/auth.test.ts +0 -396
  51. package/src/__tests__/browser-auth.test.ts +0 -180
  52. package/src/__tests__/browser.test.ts +0 -632
  53. package/src/__tests__/define.test.ts +0 -225
  54. package/src/__tests__/errors.test.ts +0 -69
  55. package/src/__tests__/executor.test.ts +0 -214
  56. package/src/__tests__/http.test.ts +0 -238
  57. package/src/__tests__/insights.test.ts +0 -210
  58. package/src/__tests__/instrumentation.test.ts +0 -290
  59. package/src/__tests__/otlp.test.ts +0 -141
  60. package/src/__tests__/perf.test.ts +0 -60
  61. package/src/__tests__/providers-yaml.test.ts +0 -135
  62. package/src/__tests__/proxy.test.ts +0 -359
  63. package/src/__tests__/recipes.test.ts +0 -36
  64. package/src/__tests__/serve.test.ts +0 -233
  65. package/src/__tests__/session.test.ts +0 -231
  66. package/src/__tests__/state.test.ts +0 -100
  67. package/src/__tests__/stealth.test.ts +0 -57
  68. package/src/__tests__/testing.test.ts +0 -97
  69. package/src/__tests__/tls.test.ts +0 -345
  70. package/src/__tests__/types.test.ts +0 -142
  71. package/src/__tests__/utils.test.ts +0 -62
  72. package/src/__tests__/waterfall.test.ts +0 -270
  73. package/src/config/providers-yaml.ts +0 -370
  74. package/src/index.test.ts +0 -1
  75. package/src/protocol.ts +0 -183
  76. package/src/runtime/auth.ts +0 -245
  77. package/src/runtime/session.ts +0 -573
  78. package/src/runtime/state.ts +0 -124
@@ -1,359 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
- import { mkdtemp } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
-
6
- import { loadApiFuseConfig, resolveProxyConfig } from "../config/loader";
7
- import { createHttpClient } from "../runtime/http";
8
-
9
- type MockSessionResponse = {
10
- status: number;
11
- body: string;
12
- headers?: Record<string, string>;
13
- rawHeaders?: Record<string, string>;
14
- usedProtocol?: string;
15
- };
16
-
17
- type MockSessionState = {
18
- getCalls: Array<{ url: string; options?: Record<string, unknown> }>;
19
- options: Record<string, unknown>;
20
- responses: MockSessionResponse[];
21
- closed: boolean;
22
- proxyUrl?: string;
23
- get: (
24
- url: string,
25
- options?: Record<string, unknown>,
26
- ) => Promise<MockSessionResponse>;
27
- close: () => void;
28
- destroySession: () => Promise<void>;
29
- post: (
30
- url: string,
31
- body: string | Buffer | null,
32
- options?: Record<string, unknown>,
33
- ) => Promise<MockSessionResponse>;
34
- put: (
35
- url: string,
36
- body: string | Buffer | null,
37
- options?: Record<string, unknown>,
38
- ) => Promise<MockSessionResponse>;
39
- patch: (
40
- url: string,
41
- body: string | Buffer | null,
42
- options?: Record<string, unknown>,
43
- ) => Promise<MockSessionResponse>;
44
- delete: (
45
- url: string,
46
- options?: Record<string, unknown>,
47
- ) => Promise<MockSessionResponse>;
48
- head: (
49
- url: string,
50
- options?: Record<string, unknown>,
51
- ) => Promise<MockSessionResponse>;
52
- optionsMethod: (
53
- url: string,
54
- options?: Record<string, unknown>,
55
- ) => Promise<MockSessionResponse>;
56
- };
57
-
58
- const tlsState = {
59
- sessions: [] as MockSessionState[],
60
- queuedResponses: [] as MockSessionResponse[],
61
- moduleClients: 0,
62
- };
63
-
64
- class MockModuleClient {
65
- constructor() {
66
- tlsState.moduleClients += 1;
67
- }
68
- }
69
-
70
- class MockSessionClient {
71
- private readonly state: MockSessionState;
72
-
73
- constructor(
74
- _moduleClient: MockModuleClient,
75
- options: Record<string, unknown>,
76
- ) {
77
- this.state = {
78
- closed: false,
79
- getCalls: [],
80
- options,
81
- proxyUrl:
82
- typeof options.proxyUrl === "string" ? options.proxyUrl : undefined,
83
- responses: tlsState.queuedResponses.splice(0),
84
- get: async (url: string, requestOptions?: Record<string, unknown>) => {
85
- this.state.getCalls.push({ url, options: requestOptions });
86
- const response = this.state.responses.shift();
87
- if (!response) {
88
- throw new Error("No queued response for GET");
89
- }
90
- return response;
91
- },
92
- post: async () => {
93
- throw new Error("POST not implemented in proxy test mock");
94
- },
95
- put: async () => {
96
- throw new Error("PUT not implemented in proxy test mock");
97
- },
98
- patch: async () => {
99
- throw new Error("PATCH not implemented in proxy test mock");
100
- },
101
- delete: async () => {
102
- throw new Error("DELETE not implemented in proxy test mock");
103
- },
104
- head: async () => {
105
- throw new Error("HEAD not implemented in proxy test mock");
106
- },
107
- optionsMethod: async () => {
108
- throw new Error("OPTIONS not implemented in proxy test mock");
109
- },
110
- close: () => {
111
- this.state.closed = true;
112
- },
113
- destroySession: async () => {
114
- this.state.closed = true;
115
- },
116
- };
117
-
118
- tlsState.sessions.push(this.state);
119
- }
120
-
121
- async get(url: string, options?: Record<string, unknown>) {
122
- return this.state.get(url, options);
123
- }
124
-
125
- async post(
126
- url: string,
127
- body: string | Buffer | null,
128
- options?: Record<string, unknown>,
129
- ) {
130
- return this.state.post(url, body, options);
131
- }
132
-
133
- async put(
134
- url: string,
135
- body: string | Buffer | null,
136
- options?: Record<string, unknown>,
137
- ) {
138
- return this.state.put(url, body, options);
139
- }
140
-
141
- async patch(
142
- url: string,
143
- body: string | Buffer | null,
144
- options?: Record<string, unknown>,
145
- ) {
146
- return this.state.patch(url, body, options);
147
- }
148
-
149
- async delete(url: string, options?: Record<string, unknown>) {
150
- return this.state.delete(url, options);
151
- }
152
-
153
- async head(url: string, options?: Record<string, unknown>) {
154
- return this.state.head(url, options);
155
- }
156
-
157
- async options(url: string, options?: Record<string, unknown>) {
158
- return this.state.optionsMethod(url, options);
159
- }
160
-
161
- close() {
162
- this.state.close();
163
- }
164
-
165
- async destroySession() {
166
- await this.state.destroySession();
167
- }
168
- }
169
-
170
- mock.module("tlsclientwrapper", () => ({
171
- ModuleClient: MockModuleClient,
172
- SessionClient: MockSessionClient,
173
- }));
174
-
175
- describe("proxy integration", () => {
176
- let originalFetch: typeof fetch;
177
- let originalProxyEnv: string | undefined;
178
-
179
- beforeEach(() => {
180
- originalFetch = global.fetch;
181
- originalProxyEnv = process.env.APIFUSE_PROXY_URL;
182
- delete process.env.APIFUSE_PROXY_URL;
183
- tlsState.sessions.length = 0;
184
- tlsState.queuedResponses.length = 0;
185
- tlsState.moduleClients = 0;
186
- });
187
-
188
- afterEach(() => {
189
- global.fetch = originalFetch;
190
- if (originalProxyEnv) {
191
- process.env.APIFUSE_PROXY_URL = originalProxyEnv;
192
- } else {
193
- delete process.env.APIFUSE_PROXY_URL;
194
- }
195
- });
196
-
197
- it("uses apifuse config proxy when upstream proxy routing is enabled", async () => {
198
- let capturedProxy: string | undefined;
199
-
200
- global.fetch = (async (_url, init) => {
201
- capturedProxy = (init as RequestInit & { proxy?: string })?.proxy;
202
- return new Response(JSON.stringify({ ok: true }), {
203
- status: 200,
204
- headers: { "Content-Type": "application/json" },
205
- });
206
- }) as typeof fetch;
207
-
208
- const http = createHttpClient("https://example.com", {
209
- apifuseConfig: { proxy: { url: "https://config-proxy.example:8443" } },
210
- upstream: { proxy: true },
211
- });
212
-
213
- await http.get("/health");
214
-
215
- expect(capturedProxy).toBe("https://config-proxy.example:8443");
216
- });
217
-
218
- it("uses APIFUSE_PROXY_URL when upstream proxy routing is enabled", async () => {
219
- let capturedProxy: string | undefined;
220
- process.env.APIFUSE_PROXY_URL = "https://env-proxy.example:8443";
221
-
222
- global.fetch = (async (_url, init) => {
223
- capturedProxy = (init as RequestInit & { proxy?: string })?.proxy;
224
- return new Response(JSON.stringify({ ok: true }), {
225
- status: 200,
226
- headers: { "Content-Type": "application/json" },
227
- });
228
- }) as typeof fetch;
229
-
230
- const http = createHttpClient("https://example.com", {
231
- upstream: { proxy: true },
232
- });
233
-
234
- await http.get("/health");
235
-
236
- expect(capturedProxy).toBe("https://env-proxy.example:8443");
237
- });
238
-
239
- it("passes request-level proxy through ctx.http", async () => {
240
- let capturedProxy: string | undefined;
241
-
242
- global.fetch = (async (_url, init) => {
243
- capturedProxy = (init as RequestInit & { proxy?: string })?.proxy;
244
- return new Response(JSON.stringify({ ok: true }), {
245
- status: 200,
246
- headers: { "Content-Type": "application/json" },
247
- });
248
- }) as typeof fetch;
249
-
250
- const http = createHttpClient("https://example.com");
251
-
252
- await http.get("/health", { proxy: "https://request-proxy.example:8443" });
253
-
254
- expect(capturedProxy).toBe("https://request-proxy.example:8443");
255
- });
256
-
257
- it("warns once when proxy routing is required but missing", async () => {
258
- const warnings: string[] = [];
259
-
260
- global.fetch = (async () =>
261
- new Response(JSON.stringify({ ok: true }), {
262
- status: 200,
263
- headers: { "Content-Type": "application/json" },
264
- })) as unknown as typeof fetch;
265
-
266
- const http = createHttpClient("https://example.com", {
267
- upstream: { proxy: true },
268
- warn: (message) => {
269
- warnings.push(message);
270
- },
271
- });
272
-
273
- await http.get("/health");
274
- await http.get("/health");
275
-
276
- expect(warnings).toEqual([
277
- "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.",
278
- ]);
279
- });
280
-
281
- it("passes resolved proxy through ctx.tls session config and request options", async () => {
282
- tlsState.queuedResponses.push({
283
- status: 200,
284
- body: "ok",
285
- headers: { "content-type": "text/plain" },
286
- });
287
-
288
- const { createTlsClient } = await import("../runtime/tls");
289
- const client = createTlsClient("https://example.com", {
290
- apifuseConfig: { proxy: { url: "https://tls-proxy.example:8443" } },
291
- upstream: { proxy: true },
292
- });
293
-
294
- await client.fetch("/health");
295
-
296
- expect(tlsState.sessions[0]?.options).toMatchObject({
297
- proxyUrl: "https://tls-proxy.example:8443",
298
- });
299
- expect(tlsState.sessions[0]?.getCalls[0]?.options).toMatchObject({
300
- proxy: "https://tls-proxy.example:8443",
301
- });
302
- });
303
-
304
- it("passes request-level proxy through ctx.tls", async () => {
305
- tlsState.queuedResponses.push({
306
- status: 200,
307
- body: "ok",
308
- headers: { "content-type": "text/plain" },
309
- });
310
-
311
- const { createTlsClient } = await import("../runtime/tls");
312
- const client = createTlsClient("https://example.com");
313
-
314
- await client.fetch("/health", {
315
- proxy: "https://request-tls-proxy.example:8443",
316
- });
317
-
318
- expect(tlsState.sessions[0]?.options).toMatchObject({
319
- proxyUrl: "https://request-tls-proxy.example:8443",
320
- });
321
- expect(tlsState.sessions[0]?.getCalls[0]?.options).toMatchObject({
322
- proxy: "https://request-tls-proxy.example:8443",
323
- });
324
- });
325
-
326
- it("resolves proxy config from env before config", () => {
327
- process.env.APIFUSE_PROXY_URL = "https://env-proxy.example:8443";
328
-
329
- expect(
330
- resolveProxyConfig({
331
- apifuseConfig: { proxy: { url: "https://config-proxy.example:8443" } },
332
- upstream: { proxy: true },
333
- }),
334
- ).toEqual({
335
- shouldWarn: false,
336
- url: "https://env-proxy.example:8443",
337
- });
338
- });
339
-
340
- it("hydrates APIFUSE_PROXY_URL from apifuse.config.ts when env is unset", async () => {
341
- const directory = await mkdtemp(path.join(tmpdir(), "apifuse-proxy-"));
342
-
343
- await Bun.write(
344
- `${directory}/apifuse.config.ts`,
345
- [
346
- "export default {",
347
- " proxy: { url: 'https://file-proxy.example:8443' },",
348
- "};",
349
- ].join("\n"),
350
- );
351
-
352
- const config = await loadApiFuseConfig(directory);
353
-
354
- expect(config.proxy?.url).toBe("https://file-proxy.example:8443");
355
- expect(process.env.APIFUSE_PROXY_URL).toBe(
356
- "https://file-proxy.example:8443",
357
- );
358
- });
359
- });
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { extractPagination, normalizeErrorResponse } from "../recipes/rest-api";
4
-
5
- describe("rest-api recipe", () => {
6
- it("extracts pagination from standard shape", () => {
7
- expect(extractPagination({ page: 1, per_page: 10, total: 100 })).toEqual({
8
- page: 1,
9
- perPage: 10,
10
- total: 100,
11
- totalPages: 10,
12
- hasNext: true,
13
- hasPrev: false,
14
- });
15
- });
16
-
17
- it("returns null for missing pagination shape", () => {
18
- expect(extractPagination({ foo: "bar" })).toBeNull();
19
- });
20
-
21
- it("normalizes error response from string error", () => {
22
- expect(normalizeErrorResponse({ error: "Not found" })).toEqual({
23
- message: "Not found",
24
- });
25
- });
26
-
27
- it("normalizes error response from message field", () => {
28
- expect(normalizeErrorResponse({ message: "Forbidden" })).toEqual({
29
- message: "Forbidden",
30
- });
31
- });
32
-
33
- it("returns null for empty error object", () => {
34
- expect(normalizeErrorResponse({})).toBeNull();
35
- });
36
- });
@@ -1,233 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { z } from "zod";
3
-
4
- import { createProviderServer } from "../serve";
5
- import type { ProviderDefinition } from "../types";
6
-
7
- process.env.APIFUSE_STATE_SECRET = "test-secret";
8
-
9
- function createTestProvider() {
10
- return {
11
- id: "test-provider",
12
- version: "1.0.0",
13
- runtime: "standard",
14
- meta: {
15
- displayName: "Test Provider",
16
- category: "test",
17
- },
18
- auth: {
19
- mode: "credentials",
20
- fields: [
21
- { name: "username", label: "Username", type: "text", required: true },
22
- { name: "otp", label: "One-time code", type: "otp", deferred: true },
23
- ],
24
- exchange: async (ctx, credentials) => {
25
- await ctx.session.set("username", credentials.username ?? "unknown");
26
- const otp = await ctx.auth.requestField("otp");
27
- await ctx.session.set("otp", otp);
28
- await ctx.session.set(
29
- "accessToken",
30
- `token:${credentials.username}:${otp}`,
31
- );
32
- },
33
- refresh: async (ctx) => {
34
- const token = await ctx.session.get("accessToken");
35
- await ctx.session.set("accessToken", `${token ?? "token"}:refreshed`);
36
- },
37
- disconnect: async (ctx) => {
38
- await ctx.session.delete("accessToken");
39
- },
40
- },
41
- operations: {
42
- echo: {
43
- description: "Echo input",
44
- input: z.object({ value: z.string() }),
45
- output: z.object({ echoed: z.string() }),
46
- hints: { rateLimit: "low" },
47
- handler: async (ctx, input) => {
48
- const typedInput = input as { value: string };
49
- const count = Number((await ctx.session.get("count")) ?? "0") + 1;
50
- await ctx.session.set("count", String(count));
51
- return { echoed: typedInput.value };
52
- },
53
- },
54
- },
55
- } satisfies ProviderDefinition;
56
- }
57
-
58
- describe("createProviderServer", () => {
59
- const provider = createTestProvider();
60
- const { app } = createProviderServer(provider, {
61
- stateSecret: process.env.APIFUSE_STATE_SECRET,
62
- });
63
-
64
- it("executes an operation and returns sessionPatch", async () => {
65
- const response = await app.request("/execute/echo", {
66
- method: "POST",
67
- headers: { "content-type": "application/json" },
68
- body: JSON.stringify({
69
- input: { value: "hello" },
70
- session: { count: "1" },
71
- }),
72
- });
73
-
74
- expect(response.status).toBe(200);
75
- expect(await response.json()).toEqual({
76
- data: { echoed: "hello" },
77
- sessionPatch: { count: "2" },
78
- trace: {
79
- spans: [
80
- expect.objectContaining({
81
- name: "handler:echo",
82
- status: "ok",
83
- }),
84
- ],
85
- },
86
- });
87
- });
88
-
89
- it("returns pending_field for deferred auth", async () => {
90
- const response = await app.request("/connect", {
91
- method: "POST",
92
- headers: { "content-type": "application/json" },
93
- body: JSON.stringify({ credentials: { username: "demo" } }),
94
- });
95
-
96
- expect(response.status).toBe(200);
97
- expect(await response.json()).toEqual({
98
- status: "pending_field",
99
- field: "otp",
100
- fieldLabel: "One-time code",
101
- resumeToken: expect.any(String),
102
- sessionPatch: { username: "demo" },
103
- });
104
- });
105
-
106
- it("resumes deferred auth and returns success with sessionPatch", async () => {
107
- const connectResponse = await app.request("/connect", {
108
- method: "POST",
109
- headers: { "content-type": "application/json" },
110
- body: JSON.stringify({ credentials: { username: "demo" } }),
111
- });
112
- const connectBody = (await connectResponse.json()) as {
113
- status: string;
114
- resumeToken: string;
115
- sessionPatch: Record<string, string>;
116
- };
117
-
118
- const resumeResponse = await app.request("/connect/resume", {
119
- method: "POST",
120
- headers: { "content-type": "application/json" },
121
- body: JSON.stringify({
122
- resumeToken: connectBody.resumeToken,
123
- fieldValue: "123456",
124
- session: connectBody.sessionPatch,
125
- }),
126
- });
127
-
128
- expect(resumeResponse.status).toBe(200);
129
- expect(await resumeResponse.json()).toEqual({
130
- status: "success",
131
- sessionPatch: {
132
- username: "demo",
133
- otp: "123456",
134
- accessToken: "token:demo:123456",
135
- __auth__: expect.any(String),
136
- },
137
- });
138
- });
139
-
140
- it("refreshes auth session", async () => {
141
- const response = await app.request("/refresh", {
142
- method: "POST",
143
- headers: { "content-type": "application/json" },
144
- body: JSON.stringify({
145
- session: { accessToken: "token:demo:123456" },
146
- }),
147
- });
148
-
149
- expect(response.status).toBe(200);
150
- expect(await response.json()).toEqual({
151
- status: "success",
152
- sessionPatch: {
153
- accessToken: "token:demo:123456:refreshed",
154
- __auth__: expect.any(String),
155
- },
156
- });
157
- });
158
-
159
- it("disconnects auth session", async () => {
160
- const response = await app.request("/disconnect", {
161
- method: "POST",
162
- headers: { "content-type": "application/json" },
163
- body: JSON.stringify({
164
- session: { accessToken: "token:demo:123456" },
165
- }),
166
- });
167
-
168
- expect(response.status).toBe(200);
169
- expect(await response.json()).toEqual({ status: "success" });
170
- });
171
-
172
- it("returns health status", async () => {
173
- const response = await app.request("/health");
174
-
175
- expect(response.status).toBe(200);
176
- expect(await response.json()).toEqual({
177
- status: "ok",
178
- provider: "test-provider",
179
- version: "1.0.0",
180
- uptime: expect.any(Number),
181
- });
182
- });
183
-
184
- it("returns operation schema", async () => {
185
- const response = await app.request("/schema/echo");
186
- const body = (await response.json()) as {
187
- operationId: string;
188
- description: string;
189
- hints: Record<string, string>;
190
- input: {
191
- type: string;
192
- properties: Record<string, unknown>;
193
- required: string[];
194
- };
195
- output: {
196
- type: string;
197
- properties: Record<string, unknown>;
198
- required: string[];
199
- };
200
- };
201
-
202
- expect(response.status).toBe(200);
203
- expect(body.operationId).toBe("echo");
204
- expect(body.description).toBe("Echo input");
205
- expect(body.hints).toEqual({ rateLimit: "low" });
206
- expect(body.input.type).toBe("object");
207
- expect(body.input.properties.value).toEqual(
208
- expect.objectContaining({ type: "string" }),
209
- );
210
- expect(body.input.required).toContain("value");
211
- expect(body.output.type).toBe("object");
212
- expect(body.output.properties.echoed).toEqual(
213
- expect.objectContaining({ type: "string" }),
214
- );
215
- });
216
-
217
- it("returns 404 for unknown operation", async () => {
218
- const response = await app.request("/execute/missing", {
219
- method: "POST",
220
- headers: { "content-type": "application/json" },
221
- body: JSON.stringify({ input: {}, session: {} }),
222
- });
223
-
224
- expect(response.status).toBe(404);
225
- expect(await response.json()).toEqual({
226
- error: {
227
- code: "NOT_FOUND",
228
- message: "Unknown operation: test-provider/missing",
229
- details: { fix: "Valid operations: echo" },
230
- },
231
- });
232
- });
233
- });