@neurocode-ai/codemode 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -0
- package/README.md +369 -0
- package/codemode.md +176 -0
- package/package.json +25 -0
- package/src/codemode.ts +159 -0
- package/src/index.ts +4 -0
- package/src/interpreter/model.ts +201 -0
- package/src/interpreter/runtime.ts +3465 -0
- package/src/openapi/TODO.md +19 -0
- package/src/openapi/index.ts +130 -0
- package/src/openapi/runtime.ts +326 -0
- package/src/openapi/spec.ts +511 -0
- package/src/openapi/types.ts +112 -0
- package/src/stdlib/collections.ts +51 -0
- package/src/stdlib/console.ts +4 -0
- package/src/stdlib/date.ts +94 -0
- package/src/stdlib/json.ts +42 -0
- package/src/stdlib/math.ts +65 -0
- package/src/stdlib/number.ts +66 -0
- package/src/stdlib/object.ts +77 -0
- package/src/stdlib/promise.ts +6 -0
- package/src/stdlib/regexp.ts +74 -0
- package/src/stdlib/string.ts +52 -0
- package/src/stdlib/url.ts +90 -0
- package/src/stdlib/value.ts +86 -0
- package/src/tool-error.ts +11 -0
- package/src/tool-runtime.ts +806 -0
- package/src/tool-schema.ts +301 -0
- package/src/tool.ts +96 -0
- package/src/values.ts +49 -0
- package/sst-env.d.ts +10 -0
- package/test/codemode.test.ts +1163 -0
- package/test/enumeration.test.ts +159 -0
- package/test/fixtures/openapi-happy-path.json +230 -0
- package/test/fixtures/opencode-v2-openapi.json +23730 -0
- package/test/openapi.test.ts +964 -0
- package/test/parity.test.ts +425 -0
- package/test/promise.test.ts +456 -0
- package/test/signature.test.ts +449 -0
- package/test/stdlib.test.ts +715 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect, Layer, Option } from "effect"
|
|
3
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
|
4
|
+
import { CodeMode, OpenAPI, Tool } from "../src/index.js"
|
|
5
|
+
import { inputTypeScript, outputTypeScript } from "../src/tool-schema.js"
|
|
6
|
+
|
|
7
|
+
const baseUrl = "http://localhost:4096"
|
|
8
|
+
type Document = OpenAPI.Document
|
|
9
|
+
|
|
10
|
+
type Recorded = {
|
|
11
|
+
readonly method: string
|
|
12
|
+
readonly url: string
|
|
13
|
+
readonly headers: Record<string, string>
|
|
14
|
+
readonly body: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const neurocodeSpec = async (): Promise<Document> => {
|
|
18
|
+
return Bun.file(new URL("./fixtures/neurocode-v2-openapi.json", import.meta.url)).json() as Promise<Document>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const happyPathSpec = async (): Promise<Document> => {
|
|
22
|
+
return Bun.file(new URL("./fixtures/openapi-happy-path.json", import.meta.url)).json() as Promise<Document>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
26
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
27
|
+
|
|
28
|
+
const toolAt = (tools: unknown, name: string) =>
|
|
29
|
+
name.split(".").reduce<unknown>((current, segment) => (isRecord(current) ? current[segment] : undefined), tools)
|
|
30
|
+
|
|
31
|
+
const recordingClient = (respond: (request: HttpClientRequest.HttpClientRequest) => Response) => {
|
|
32
|
+
const requests: Array<Recorded> = []
|
|
33
|
+
const layer = Layer.succeed(HttpClient.HttpClient)(
|
|
34
|
+
HttpClient.make((request) =>
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const body =
|
|
37
|
+
request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined
|
|
38
|
+
const url = Option.map(HttpClientRequest.toUrl(request), (resolved) => resolved.toString())
|
|
39
|
+
requests.push({
|
|
40
|
+
method: request.method,
|
|
41
|
+
url: Option.getOrElse(url, () => request.url),
|
|
42
|
+
headers: { ...request.headers },
|
|
43
|
+
body,
|
|
44
|
+
})
|
|
45
|
+
return HttpClientResponse.fromWeb(request, respond(request))
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
return { requests, layer }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const json = (value: unknown, status = 200) =>
|
|
53
|
+
new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } })
|
|
54
|
+
|
|
55
|
+
const singleOperation = (operation: Record<string, unknown>, method = "get"): Document => ({
|
|
56
|
+
openapi: "3.1.0",
|
|
57
|
+
paths: {
|
|
58
|
+
"/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } },
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("OpenAPI.fromSpec", () => {
|
|
63
|
+
test("covers a representative API from generation through execution", async () => {
|
|
64
|
+
const resolutions: Array<string> = []
|
|
65
|
+
const client = recordingClient((request) => {
|
|
66
|
+
const url = Option.getOrElse(HttpClientRequest.toUrl(request), () => new URL(request.url))
|
|
67
|
+
if (request.method === "POST") {
|
|
68
|
+
return new Response(
|
|
69
|
+
JSON.stringify({ id: "user-2", name: "Grace", email: "grace@example.test", role: "admin" }),
|
|
70
|
+
{ status: 201, headers: { "content-type": "application/vnd.example+json" } },
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
if (request.method === "DELETE") return new Response(null, { status: 204 })
|
|
74
|
+
if (url.pathname === "/search") {
|
|
75
|
+
return new Response("2 matches", { headers: { "content-type": "text/plain" } })
|
|
76
|
+
}
|
|
77
|
+
return json({ id: "user-1", name: "Ada", email: "ada@example.test", role: "member" })
|
|
78
|
+
})
|
|
79
|
+
const api = OpenAPI.fromSpec({
|
|
80
|
+
spec: await happyPathSpec(),
|
|
81
|
+
baseUrl,
|
|
82
|
+
auth: {
|
|
83
|
+
resolve: ({ name }) => {
|
|
84
|
+
resolutions.push(name)
|
|
85
|
+
return Effect.succeed(
|
|
86
|
+
name === "BearerAuth"
|
|
87
|
+
? { type: "bearer", token: "bearer-secret" }
|
|
88
|
+
: { type: "apiKey", value: "api-secret" },
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
const get = toolAt(api.tools, "users.get")
|
|
94
|
+
const create = toolAt(api.tools, "users.create")
|
|
95
|
+
const search = toolAt(api.tools, "search.run")
|
|
96
|
+
const remove = toolAt(api.tools, "users.remove")
|
|
97
|
+
|
|
98
|
+
expect(api.skipped).toEqual([])
|
|
99
|
+
if (
|
|
100
|
+
!Tool.isDefinition(get) ||
|
|
101
|
+
!Tool.isDefinition(create) ||
|
|
102
|
+
!Tool.isDefinition(search) ||
|
|
103
|
+
!Tool.isDefinition(remove)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error("happy-path fixture did not generate every operation")
|
|
106
|
+
}
|
|
107
|
+
expect(inputTypeScript(get)).toBe(
|
|
108
|
+
'{ userId: string; include?: Array<string>; verbose?: boolean; "X-Trace-ID"?: string }',
|
|
109
|
+
)
|
|
110
|
+
expect(inputTypeScript(create)).toBe('{ name: string; email: string; role?: "admin" | "member" }')
|
|
111
|
+
expect(inputTypeScript(search)).toBe("{ filter?: { query: string; page?: number }; tags?: Array<string> }")
|
|
112
|
+
expect(inputTypeScript(remove)).toBe("{ userId: string }")
|
|
113
|
+
expect(outputTypeScript(get)).toContain("id: string")
|
|
114
|
+
expect(outputTypeScript(create)).toContain('role?: "admin" | "member"')
|
|
115
|
+
expect(outputTypeScript(search)).toBe("string")
|
|
116
|
+
expect(outputTypeScript(remove)).toBe("null")
|
|
117
|
+
|
|
118
|
+
const result = await Effect.runPromise(
|
|
119
|
+
CodeMode.make({ tools: { api: api.tools } })
|
|
120
|
+
.execute(
|
|
121
|
+
`
|
|
122
|
+
const user = await tools.api.users.get({
|
|
123
|
+
userId: "user-1",
|
|
124
|
+
include: ["profile", "permissions"],
|
|
125
|
+
verbose: true,
|
|
126
|
+
"X-Trace-ID": "trace-1",
|
|
127
|
+
})
|
|
128
|
+
const created = await tools.api.users.create({
|
|
129
|
+
name: "Grace",
|
|
130
|
+
email: "grace@example.test",
|
|
131
|
+
role: "admin",
|
|
132
|
+
})
|
|
133
|
+
const summary = await tools.api.search.run({
|
|
134
|
+
filter: { query: "effect", page: 2 },
|
|
135
|
+
tags: ["typescript", "runtime"],
|
|
136
|
+
})
|
|
137
|
+
const removed = await tools.api.users.remove({ userId: "user-1" })
|
|
138
|
+
return { user, created, summary, removed }
|
|
139
|
+
`,
|
|
140
|
+
)
|
|
141
|
+
.pipe(Effect.provide(client.layer)),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
expect(result).toMatchObject({
|
|
145
|
+
ok: true,
|
|
146
|
+
value: {
|
|
147
|
+
user: { id: "user-1", name: "Ada" },
|
|
148
|
+
created: { id: "user-2", name: "Grace" },
|
|
149
|
+
summary: "2 matches",
|
|
150
|
+
removed: null,
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
expect(resolutions).toEqual(["BearerAuth", "ApiKey", "BearerAuth"])
|
|
154
|
+
expect(client.requests).toHaveLength(4)
|
|
155
|
+
|
|
156
|
+
const getUrl = new URL(client.requests[0]!.url)
|
|
157
|
+
expect(getUrl.pathname).toBe("/users/user-1")
|
|
158
|
+
expect(getUrl.searchParams.get("include")).toBe("profile,permissions")
|
|
159
|
+
expect(getUrl.searchParams.get("verbose")).toBe("true")
|
|
160
|
+
expect(client.requests[0]!.headers["x-trace-id"]).toBe("trace-1")
|
|
161
|
+
expect(client.requests[0]!.headers.authorization).toBe("Bearer bearer-secret")
|
|
162
|
+
|
|
163
|
+
const createUrl = new URL(client.requests[1]!.url)
|
|
164
|
+
expect(createUrl.searchParams.get("api_key")).toBe("api-secret")
|
|
165
|
+
expect(client.requests[1]!.body).toEqual({ name: "Grace", email: "grace@example.test", role: "admin" })
|
|
166
|
+
|
|
167
|
+
const searchUrl = new URL(client.requests[2]!.url)
|
|
168
|
+
expect(searchUrl.searchParams.get("filter[query]")).toBe("effect")
|
|
169
|
+
expect(searchUrl.searchParams.get("filter[page]")).toBe("2")
|
|
170
|
+
expect(searchUrl.searchParams.getAll("tags")).toEqual(["typescript", "runtime"])
|
|
171
|
+
expect(client.requests[2]!.headers.authorization).toBeUndefined()
|
|
172
|
+
expect(new URL(client.requests[3]!.url).pathname).toBe("/users/user-1")
|
|
173
|
+
expect(client.requests[3]!.headers.authorization).toBe("Bearer bearer-secret")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("converts representative neurocode operations into the expected tool shape", async () => {
|
|
177
|
+
const spec = await neurocodeSpec()
|
|
178
|
+
const result = OpenAPI.fromSpec({ spec, baseUrl })
|
|
179
|
+
|
|
180
|
+
expect(result.skipped).toHaveLength(5)
|
|
181
|
+
expect(result.skipped).toContainEqual({
|
|
182
|
+
method: "GET",
|
|
183
|
+
path: "/api/pty/{ptyID}/connect",
|
|
184
|
+
reason: "WebSocket operations are not supported",
|
|
185
|
+
})
|
|
186
|
+
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3)
|
|
187
|
+
expect(result.skipped).toContainEqual({
|
|
188
|
+
method: "GET",
|
|
189
|
+
path: "/api/fs/read/*",
|
|
190
|
+
reason: "binary responses are not supported",
|
|
191
|
+
})
|
|
192
|
+
expect(toolAt(result.tools, "v2.health.get")).not.toBeUndefined()
|
|
193
|
+
expect(toolAt(result.tools, "v2.session.get")).not.toBeUndefined()
|
|
194
|
+
expect(toolAt(result.tools, "v2.session.create")).not.toBeUndefined()
|
|
195
|
+
|
|
196
|
+
const sessionGet = toolAt(result.tools, "v2.session.get")
|
|
197
|
+
expect(Tool.isDefinition(sessionGet)).toBe(true)
|
|
198
|
+
if (!Tool.isDefinition(sessionGet)) throw new Error("v2.session.get was not generated")
|
|
199
|
+
expect(inputTypeScript(sessionGet)).toBe("{ sessionID: string }")
|
|
200
|
+
expect(outputTypeScript(sessionGet)).toContain("id: string")
|
|
201
|
+
expect(outputTypeScript(sessionGet)).toContain("additions: number")
|
|
202
|
+
|
|
203
|
+
const switchAgent = toolAt(result.tools, "v2.session.switchAgent")
|
|
204
|
+
expect(Tool.isDefinition(switchAgent)).toBe(true)
|
|
205
|
+
if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated")
|
|
206
|
+
expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }")
|
|
207
|
+
|
|
208
|
+
const contextEntryPut = toolAt(result.tools, "v2.session.contextEntry.put")
|
|
209
|
+
expect(Tool.isDefinition(contextEntryPut)).toBe(true)
|
|
210
|
+
if (!Tool.isDefinition(contextEntryPut)) throw new Error("v2.session.contextEntry.put was not generated")
|
|
211
|
+
expect(inputTypeScript(contextEntryPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
|
212
|
+
expect(toolAt(result.tools, "v2_session_context_entry_put_2")).toBeUndefined()
|
|
213
|
+
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined()
|
|
214
|
+
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
|
|
215
|
+
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
|
|
216
|
+
expect(toolAt(result.tools, "v2.event.changes")).toBeUndefined()
|
|
217
|
+
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
|
|
218
|
+
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test("preserves operation path sanitization and collision handling", () => {
|
|
222
|
+
const response = { responses: { 200: { description: "Success" } } }
|
|
223
|
+
const result = OpenAPI.fromSpec({
|
|
224
|
+
baseUrl,
|
|
225
|
+
spec: {
|
|
226
|
+
openapi: "3.1.0",
|
|
227
|
+
paths: {
|
|
228
|
+
"/first": { get: { ...response, operationId: "group.item" } },
|
|
229
|
+
"/second": { get: { ...response, operationId: "group.item" } },
|
|
230
|
+
"/third": { get: { ...response, operationId: "group..other" } },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(Tool.isDefinition(toolAt(result.tools, "group.item"))).toBe(true)
|
|
236
|
+
expect(Tool.isDefinition(toolAt(result.tools, "group_item_2"))).toBe(true)
|
|
237
|
+
expect(Tool.isDefinition(toolAt(result.tools, "group.operation.other"))).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test("synthesizes flat operation IDs from methods and paths", () => {
|
|
241
|
+
const response = { responses: { 200: { description: "Success" } } }
|
|
242
|
+
const tools = OpenAPI.fromSpec({
|
|
243
|
+
baseUrl,
|
|
244
|
+
spec: {
|
|
245
|
+
openapi: "3.1.0",
|
|
246
|
+
paths: {
|
|
247
|
+
"/users": { get: response, post: response },
|
|
248
|
+
"/users/{id}": { get: response, patch: response, delete: response },
|
|
249
|
+
"/organizations/{organizationId}/users/{id}": { get: response },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
}).tools
|
|
253
|
+
|
|
254
|
+
for (const path of [
|
|
255
|
+
"getUsers",
|
|
256
|
+
"postUsers",
|
|
257
|
+
"getUsersById",
|
|
258
|
+
"patchUsersById",
|
|
259
|
+
"deleteUsersById",
|
|
260
|
+
"getOrganizationsByOrganizationidUsersById",
|
|
261
|
+
]) {
|
|
262
|
+
expect(Tool.isDefinition(toolAt(tools, path))).toBe(true)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test("lets operation parameters override matching path parameters", () => {
|
|
267
|
+
const tool = toolAt(
|
|
268
|
+
OpenAPI.fromSpec({
|
|
269
|
+
baseUrl,
|
|
270
|
+
spec: {
|
|
271
|
+
openapi: "3.1.0",
|
|
272
|
+
paths: {
|
|
273
|
+
"/test": {
|
|
274
|
+
parameters: [{ name: "limit", in: "query", schema: { type: "string" } }],
|
|
275
|
+
get: {
|
|
276
|
+
operationId: "test",
|
|
277
|
+
parameters: [{ name: "limit", in: "query", required: true, schema: { type: "number" } }],
|
|
278
|
+
responses: { 200: { description: "Success" } },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
}).tools,
|
|
284
|
+
"test",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
288
|
+
expect(inputTypeScript(tool)).toBe("{ limit: number }")
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test("normalizes OpenAPI 3.0 schemas with Effect", () => {
|
|
292
|
+
const result = OpenAPI.fromSpec({
|
|
293
|
+
baseUrl,
|
|
294
|
+
spec: {
|
|
295
|
+
openapi: "3.0.3",
|
|
296
|
+
paths: {
|
|
297
|
+
"/search": {
|
|
298
|
+
get: {
|
|
299
|
+
operationId: "search",
|
|
300
|
+
parameters: [
|
|
301
|
+
{
|
|
302
|
+
in: "query",
|
|
303
|
+
name: "value",
|
|
304
|
+
schema: { type: "string", nullable: true, minLength: 2 },
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
responses: { 200: { description: "Success" } },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
const search = toolAt(result.tools, "search")
|
|
314
|
+
|
|
315
|
+
expect(Tool.isDefinition(search)).toBe(true)
|
|
316
|
+
if (!Tool.isDefinition(search)) throw new Error("search was not generated")
|
|
317
|
+
expect(inputTypeScript(search)).toBe("{ value?: string | null }")
|
|
318
|
+
const schema: unknown = search.input
|
|
319
|
+
const input = isRecord(schema) ? schema : {}
|
|
320
|
+
const properties = isRecord(input.properties) ? input.properties : {}
|
|
321
|
+
const value = isRecord(properties.value) ? properties.value : {}
|
|
322
|
+
expect(value.minLength).toBe(2)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test("preserves schema-local definitions alongside component definitions", () => {
|
|
326
|
+
const tool = toolAt(
|
|
327
|
+
OpenAPI.fromSpec({
|
|
328
|
+
baseUrl,
|
|
329
|
+
spec: {
|
|
330
|
+
openapi: "3.1.0",
|
|
331
|
+
paths: {
|
|
332
|
+
"/test": {
|
|
333
|
+
get: {
|
|
334
|
+
operationId: "test",
|
|
335
|
+
responses: {
|
|
336
|
+
200: {
|
|
337
|
+
description: "Success",
|
|
338
|
+
content: {
|
|
339
|
+
"application/json": {
|
|
340
|
+
schema: { $ref: "#/$defs/Local", $defs: { Local: { type: "string" } } },
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
components: { schemas: { Global: { type: "number" } } },
|
|
349
|
+
},
|
|
350
|
+
}).tools,
|
|
351
|
+
"test",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if (!Tool.isDefinition(tool) || !isRecord(tool.output)) throw new Error("test output was not generated")
|
|
355
|
+
expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } })
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test("documents that the neurocode fixture is unauthenticated", async () => {
|
|
359
|
+
const spec = await neurocodeSpec()
|
|
360
|
+
const components = isRecord(spec.components) ? spec.components : {}
|
|
361
|
+
const result = OpenAPI.fromSpec({ spec, baseUrl })
|
|
362
|
+
|
|
363
|
+
expect(spec.security).toStrictEqual([])
|
|
364
|
+
expect(isRecord(components.securitySchemes) ? Object.keys(components.securitySchemes) : []).toStrictEqual([])
|
|
365
|
+
const health = toolAt(result.tools, "v2.health.get")
|
|
366
|
+
const healthInput = isRecord(health) ? health.input : undefined
|
|
367
|
+
expect(healthInput).toMatchObject({ type: "object", properties: {} })
|
|
368
|
+
const input = isRecord(healthInput) ? healthInput : {}
|
|
369
|
+
expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual([])
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test("exposes real neurocode operations through CodeMode discovery", async () => {
|
|
373
|
+
const { layer } = recordingClient(() => json({}))
|
|
374
|
+
const runtime = CodeMode.make({
|
|
375
|
+
tools: { neurocode: OpenAPI.fromSpec({ spec: await neurocodeSpec(), baseUrl }).tools },
|
|
376
|
+
})
|
|
377
|
+
const result = await Effect.runPromise(
|
|
378
|
+
runtime
|
|
379
|
+
.execute(
|
|
380
|
+
`
|
|
381
|
+
return await tools.$codemode.search({ query: "global health", namespace: "neurocode", limit: 1 })
|
|
382
|
+
`,
|
|
383
|
+
)
|
|
384
|
+
.pipe(Effect.provide(layer)),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
expect(result).toMatchObject({ ok: true })
|
|
388
|
+
if (!result.ok) return
|
|
389
|
+
expect(result.value).toMatchObject({
|
|
390
|
+
items: [
|
|
391
|
+
{
|
|
392
|
+
path: "tools.neurocode.v2.health.get",
|
|
393
|
+
description: "Check whether the API server is ready to accept requests.",
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
})
|
|
397
|
+
expect(JSON.stringify(result.value)).toContain("healthy: true")
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test("invokes real neurocode path parameters and JSON request bodies", async () => {
|
|
401
|
+
const { requests, layer } = recordingClient((request) => {
|
|
402
|
+
if (request.method === "GET") return json({ id: "ses_123" })
|
|
403
|
+
return json({ id: "ses_456" })
|
|
404
|
+
})
|
|
405
|
+
const runtime = CodeMode.make({
|
|
406
|
+
tools: { neurocode: OpenAPI.fromSpec({ spec: await neurocodeSpec(), baseUrl }).tools },
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const result = await Effect.runPromise(
|
|
410
|
+
runtime
|
|
411
|
+
.execute(
|
|
412
|
+
`
|
|
413
|
+
const existing = await tools.neurocode.v2.session.get({ sessionID: "ses_123" })
|
|
414
|
+
const created = await tools.neurocode.v2.session.create({ id: "ses_456" })
|
|
415
|
+
return { existing, created }
|
|
416
|
+
`,
|
|
417
|
+
)
|
|
418
|
+
.pipe(Effect.provide(layer)),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
expect(result).toMatchObject({ ok: true })
|
|
422
|
+
expect(requests).toHaveLength(2)
|
|
423
|
+
expect(requests[0]).toMatchObject({ method: "GET", body: undefined })
|
|
424
|
+
expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_123")
|
|
425
|
+
expect(requests[1]).toMatchObject({
|
|
426
|
+
method: "POST",
|
|
427
|
+
url: "http://localhost:4096/api/session",
|
|
428
|
+
body: { id: "ses_456" },
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test("serializes deep-object query parameters from the neurocode fixture", async () => {
|
|
433
|
+
const client = recordingClient(() => json({ directory: "/tmp" }))
|
|
434
|
+
const location = toolAt(OpenAPI.fromSpec({ spec: await neurocodeSpec(), baseUrl }).tools, "v2.location.get")
|
|
435
|
+
if (!Tool.isDefinition(location)) throw new Error("v2.location.get was not generated")
|
|
436
|
+
|
|
437
|
+
await Effect.runPromise(
|
|
438
|
+
location.run({ location: { directory: "/tmp", workspace: "workspace-1" } }).pipe(Effect.provide(client.layer)),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
const url = new URL(client.requests[0]!.url)
|
|
442
|
+
expect(url.searchParams.get("location[directory]")).toBe("/tmp")
|
|
443
|
+
expect(url.searchParams.get("location[workspace]")).toBe("workspace-1")
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test("serializes supported simple and form parameter shapes", async () => {
|
|
447
|
+
const client = recordingClient(() => json({ ok: true }))
|
|
448
|
+
const result = OpenAPI.fromSpec({
|
|
449
|
+
baseUrl,
|
|
450
|
+
spec: {
|
|
451
|
+
openapi: "3.1.0",
|
|
452
|
+
paths: {
|
|
453
|
+
"/items/{keys}": {
|
|
454
|
+
get: {
|
|
455
|
+
operationId: "items",
|
|
456
|
+
parameters: [
|
|
457
|
+
{ name: "keys", in: "path", required: true, schema: { type: "array", items: { type: "string" } } },
|
|
458
|
+
{ name: "tags", in: "query", style: "form", explode: false, schema: { type: "array" } },
|
|
459
|
+
{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } },
|
|
460
|
+
{ name: "nullable", in: "query", required: true, schema: { type: ["string", "null"] } },
|
|
461
|
+
{ name: "constructor", in: "query", schema: { type: "string" } },
|
|
462
|
+
{ name: "meta", in: "header", style: "simple", explode: true, schema: { type: "object" } },
|
|
463
|
+
],
|
|
464
|
+
responses: { 200: { description: "Success" } },
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
})
|
|
470
|
+
const tool = toolAt(result.tools, "items")
|
|
471
|
+
if (!Tool.isDefinition(tool)) throw new Error("items was not generated")
|
|
472
|
+
|
|
473
|
+
await Effect.runPromise(
|
|
474
|
+
tool
|
|
475
|
+
.run({
|
|
476
|
+
keys: ["a!", "b*"],
|
|
477
|
+
tags: ["x", "y"],
|
|
478
|
+
filter: { state: "open", page: 2 },
|
|
479
|
+
nullable: null,
|
|
480
|
+
constructor_2: "safe",
|
|
481
|
+
meta: { a: "b", c: "d" },
|
|
482
|
+
})
|
|
483
|
+
.pipe(Effect.provide(client.layer)),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
const url = new URL(client.requests[0]!.url)
|
|
487
|
+
expect(url.pathname).toBe("/items/a%21,b%2A")
|
|
488
|
+
expect(url.searchParams.get("tags")).toBe("x,y")
|
|
489
|
+
expect(url.searchParams.get("state")).toBe("open")
|
|
490
|
+
expect(url.searchParams.get("page")).toBe("2")
|
|
491
|
+
expect(url.searchParams.get("nullable")).toBe("null")
|
|
492
|
+
expect(url.searchParams.get("constructor")).toBe("safe")
|
|
493
|
+
expect(client.requests[0]!.headers.meta).toBe("a=b,c=d")
|
|
494
|
+
await expect(Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
|
495
|
+
"unsupported nested value",
|
|
496
|
+
)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test("skips unsupported parameter encodings and malformed security", () => {
|
|
500
|
+
const result = OpenAPI.fromSpec({
|
|
501
|
+
baseUrl,
|
|
502
|
+
spec: {
|
|
503
|
+
openapi: "3.1.0",
|
|
504
|
+
security: [{ bearer: [] }],
|
|
505
|
+
paths: {
|
|
506
|
+
"/cookie": {
|
|
507
|
+
get: {
|
|
508
|
+
operationId: "cookie",
|
|
509
|
+
parameters: [{ name: "session", in: "cookie", schema: { type: "string" } }],
|
|
510
|
+
responses: { 200: { description: "Success" } },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
"/reserved": {
|
|
514
|
+
get: {
|
|
515
|
+
operationId: "reserved",
|
|
516
|
+
parameters: [{ name: "query", in: "query", allowReserved: true, schema: { type: "string" } }],
|
|
517
|
+
responses: { 200: { description: "Success" } },
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
"/invalid-style": {
|
|
521
|
+
get: {
|
|
522
|
+
operationId: "invalidStyle",
|
|
523
|
+
parameters: [{ name: "query", in: "query", style: 42, schema: { type: "string" } }],
|
|
524
|
+
responses: { 200: { description: "Success" } },
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
"/security": {
|
|
528
|
+
get: { operationId: "security", security: null, responses: { 200: { description: "Success" } } },
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
expect(result.tools).toEqual({})
|
|
535
|
+
expect(result.skipped.map((item) => item.reason)).toEqual([
|
|
536
|
+
"cookie parameter 'session' is not supported",
|
|
537
|
+
"parameter 'query' uses unsupported allowReserved encoding",
|
|
538
|
+
"parameter 'query' has an invalid style",
|
|
539
|
+
"security declaration is not an array",
|
|
540
|
+
])
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
test("fails closed on prototype-named missing security schemes", () => {
|
|
544
|
+
const result = OpenAPI.fromSpec({
|
|
545
|
+
baseUrl,
|
|
546
|
+
spec: singleOperation({ security: [JSON.parse('{"__proto__":[]}')] }),
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
expect(result.tools).toEqual({})
|
|
550
|
+
expect(result.skipped[0]?.reason).toBe("security requirement references missing or malformed scheme: __proto__")
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
test("resolves bearer authentication without exposing it as input", async () => {
|
|
554
|
+
const contexts: Array<Parameters<OpenAPI.AuthResolver>[0]> = []
|
|
555
|
+
const client = recordingClient(() => json({ ok: true }))
|
|
556
|
+
const spec = {
|
|
557
|
+
...singleOperation({ operationId: undefined }),
|
|
558
|
+
security: [{ bearer: [] }],
|
|
559
|
+
components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } },
|
|
560
|
+
} satisfies Document
|
|
561
|
+
const tool = toolAt(
|
|
562
|
+
OpenAPI.fromSpec({
|
|
563
|
+
baseUrl,
|
|
564
|
+
spec,
|
|
565
|
+
auth: {
|
|
566
|
+
resolve: (context) => {
|
|
567
|
+
contexts.push(context)
|
|
568
|
+
return Effect.succeed({ type: "bearer", token: "secret" })
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
}).tools,
|
|
572
|
+
"getTest",
|
|
573
|
+
)
|
|
574
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
575
|
+
|
|
576
|
+
await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))
|
|
577
|
+
|
|
578
|
+
expect(inputTypeScript(tool)).toBe("{}")
|
|
579
|
+
expect(client.requests[0]!.headers.authorization).toBe("Bearer secret")
|
|
580
|
+
expect(contexts).toEqual([
|
|
581
|
+
{
|
|
582
|
+
name: "bearer",
|
|
583
|
+
definition: { type: "http", scheme: "bearer" },
|
|
584
|
+
scopes: [],
|
|
585
|
+
operation: {
|
|
586
|
+
operationId: undefined,
|
|
587
|
+
method: "GET",
|
|
588
|
+
path: "/test",
|
|
589
|
+
summary: undefined,
|
|
590
|
+
description: undefined,
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
])
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test("applies authentication carriers without prototype or collision loss", async () => {
|
|
597
|
+
const client = recordingClient(() => json({ ok: true }))
|
|
598
|
+
const authenticated = (
|
|
599
|
+
security: ReadonlyArray<Record<string, ReadonlyArray<string>>>,
|
|
600
|
+
schemes: Record<string, unknown>,
|
|
601
|
+
) =>
|
|
602
|
+
OpenAPI.fromSpec({
|
|
603
|
+
baseUrl,
|
|
604
|
+
spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } },
|
|
605
|
+
auth: { resolve: () => Effect.succeed({ type: "apiKey", value: "secret" }) },
|
|
606
|
+
})
|
|
607
|
+
const prototype = toolAt(
|
|
608
|
+
authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools,
|
|
609
|
+
"test",
|
|
610
|
+
)
|
|
611
|
+
if (!Tool.isDefinition(prototype)) throw new Error("prototype auth tool was not generated")
|
|
612
|
+
|
|
613
|
+
await Effect.runPromise(prototype.run({}).pipe(Effect.provide(client.layer)))
|
|
614
|
+
expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret")
|
|
615
|
+
|
|
616
|
+
const duplicate = toolAt(
|
|
617
|
+
authenticated([{ first: [], second: [] }], {
|
|
618
|
+
first: { type: "apiKey", in: "header", name: "x-key" },
|
|
619
|
+
second: { type: "apiKey", in: "header", name: "x-key" },
|
|
620
|
+
}).tools,
|
|
621
|
+
"test",
|
|
622
|
+
)
|
|
623
|
+
if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated")
|
|
624
|
+
await expect(Effect.runPromise(duplicate.run({}).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
|
625
|
+
"multiple credentials",
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
const cookie = authenticated([{ key: [] }], { key: { type: "apiKey", in: "cookie", name: "session" } })
|
|
629
|
+
expect(cookie.tools).toEqual({})
|
|
630
|
+
expect(cookie.skipped[0]?.reason).toBe("cookie authentication 'key' is not supported")
|
|
631
|
+
|
|
632
|
+
const alternative = OpenAPI.fromSpec({
|
|
633
|
+
baseUrl,
|
|
634
|
+
spec: {
|
|
635
|
+
...singleOperation({}),
|
|
636
|
+
security: [{ cookie: [] }, { bearer: [] }],
|
|
637
|
+
components: {
|
|
638
|
+
securitySchemes: {
|
|
639
|
+
cookie: { type: "apiKey", in: "cookie", name: "session" },
|
|
640
|
+
bearer: { type: "http", scheme: "bearer" },
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
auth: {
|
|
645
|
+
resolve: ({ name }) => Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined),
|
|
646
|
+
},
|
|
647
|
+
})
|
|
648
|
+
const alternativeTool = toolAt(alternative.tools, "test")
|
|
649
|
+
if (!Tool.isDefinition(alternativeTool)) throw new Error("supported auth alternative was not generated")
|
|
650
|
+
await Effect.runPromise(alternativeTool.run({}).pipe(Effect.provide(client.layer)))
|
|
651
|
+
expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret")
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test("honors server precedence and rejects ambiguous base URLs", async () => {
|
|
655
|
+
const client = recordingClient(() => json({ ok: true }))
|
|
656
|
+
const spec = {
|
|
657
|
+
...singleOperation({ servers: [{ url: "https://operation.example/v1" }] }),
|
|
658
|
+
servers: [{ url: "https://document.example" }],
|
|
659
|
+
} satisfies Document
|
|
660
|
+
const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test")
|
|
661
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
662
|
+
|
|
663
|
+
await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))
|
|
664
|
+
expect(client.requests[0]?.url).toBe("https://operation.example/v1/test")
|
|
665
|
+
|
|
666
|
+
const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" })
|
|
667
|
+
expect(invalid.tools).toEqual({})
|
|
668
|
+
expect(invalid.skipped[0]?.reason).toContain("unsupported query string or fragment")
|
|
669
|
+
|
|
670
|
+
const malformed = OpenAPI.fromSpec({ spec, baseUrl: "https:/example.com" })
|
|
671
|
+
expect(malformed.tools).toEqual({})
|
|
672
|
+
expect(malformed.skipped[0]?.reason).toContain("not an absolute HTTP(S) URL")
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
test("resolves chained response refs before detecting unsupported transports", () => {
|
|
676
|
+
const result = OpenAPI.fromSpec({
|
|
677
|
+
baseUrl,
|
|
678
|
+
spec: {
|
|
679
|
+
...singleOperation({ responses: { 200: { $ref: "#/components/responses/First" } } }),
|
|
680
|
+
components: {
|
|
681
|
+
responses: {
|
|
682
|
+
First: { $ref: "#/components/responses/Stream" },
|
|
683
|
+
Stream: { content: { "text/event-stream": { schema: { type: "string" } } } },
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
expect(result.tools).toEqual({})
|
|
690
|
+
expect(result.skipped[0]?.reason).toBe("SSE operations are not supported")
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
test("resolves response schemas before detecting binary output", () => {
|
|
694
|
+
const result = OpenAPI.fromSpec({
|
|
695
|
+
baseUrl,
|
|
696
|
+
spec: {
|
|
697
|
+
...singleOperation({
|
|
698
|
+
responses: {
|
|
699
|
+
200: {
|
|
700
|
+
content: { "text/plain": { schema: { $ref: "#/components/schemas/File" } } },
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
}),
|
|
704
|
+
components: { schemas: { File: { type: "string", format: "binary" } } },
|
|
705
|
+
},
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
expect(result.tools).toEqual({})
|
|
709
|
+
expect(result.skipped[0]?.reason).toBe("binary responses are not supported")
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test("validates composite parameters before resolving auth", async () => {
|
|
713
|
+
const resolutions: Array<string> = []
|
|
714
|
+
const client = recordingClient(() => json({ ok: true }))
|
|
715
|
+
const tool = toolAt(
|
|
716
|
+
OpenAPI.fromSpec({
|
|
717
|
+
baseUrl,
|
|
718
|
+
spec: {
|
|
719
|
+
...singleOperation({
|
|
720
|
+
parameters: [{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }],
|
|
721
|
+
}),
|
|
722
|
+
security: [{ bearer: [] }],
|
|
723
|
+
components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } },
|
|
724
|
+
},
|
|
725
|
+
auth: {
|
|
726
|
+
resolve: ({ name }) => {
|
|
727
|
+
resolutions.push(name)
|
|
728
|
+
return Effect.succeed({ type: "bearer", token: "secret" })
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
}).tools,
|
|
732
|
+
"test",
|
|
733
|
+
)
|
|
734
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
735
|
+
|
|
736
|
+
await expect(
|
|
737
|
+
Effect.runPromise(tool.run({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))),
|
|
738
|
+
).rejects.toThrow("unsupported nested value")
|
|
739
|
+
expect(resolutions).toEqual([])
|
|
740
|
+
expect(client.requests).toEqual([])
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
test("preserves JSON media types and rejects unencodable bodies", async () => {
|
|
744
|
+
const client = recordingClient(() => json({ ok: true }))
|
|
745
|
+
const tool = toolAt(
|
|
746
|
+
OpenAPI.fromSpec({
|
|
747
|
+
baseUrl,
|
|
748
|
+
spec: singleOperation(
|
|
749
|
+
{
|
|
750
|
+
requestBody: {
|
|
751
|
+
required: true,
|
|
752
|
+
content: { "application/merge-patch+json": { schema: { type: "object" } } },
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
"post",
|
|
756
|
+
),
|
|
757
|
+
}).tools,
|
|
758
|
+
"test",
|
|
759
|
+
)
|
|
760
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
761
|
+
|
|
762
|
+
await Effect.runPromise(tool.run({ body: { name: "updated" } }).pipe(Effect.provide(client.layer)))
|
|
763
|
+
expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json")
|
|
764
|
+
const cyclic: Record<string, unknown> = {}
|
|
765
|
+
cyclic.self = cyclic
|
|
766
|
+
await expect(Effect.runPromise(tool.run({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
|
767
|
+
"Invalid JSON body",
|
|
768
|
+
)
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
test("rejects oversized and malformed JSON responses", async () => {
|
|
772
|
+
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test")
|
|
773
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
774
|
+
const oversized = recordingClient(
|
|
775
|
+
() => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }),
|
|
776
|
+
)
|
|
777
|
+
const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } }))
|
|
778
|
+
const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1)))
|
|
779
|
+
|
|
780
|
+
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow(
|
|
781
|
+
"response exceeds 50 MiB",
|
|
782
|
+
)
|
|
783
|
+
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow(
|
|
784
|
+
"returned malformed JSON",
|
|
785
|
+
)
|
|
786
|
+
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow(
|
|
787
|
+
"response exceeds 50 MiB",
|
|
788
|
+
)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
test("keeps non-JSON responses raw and unions every success output", async () => {
|
|
792
|
+
const spec = singleOperation({
|
|
793
|
+
responses: {
|
|
794
|
+
200: { description: "Text", content: { "text/plain": { schema: { type: "string" } } } },
|
|
795
|
+
204: { description: "Empty" },
|
|
796
|
+
},
|
|
797
|
+
})
|
|
798
|
+
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test")
|
|
799
|
+
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
|
800
|
+
const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } }))
|
|
801
|
+
|
|
802
|
+
expect(outputTypeScript(tool)).toBe("string | null")
|
|
803
|
+
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123")
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
test("fails missing required parameters before auth and network", async () => {
|
|
807
|
+
const { requests, layer } = recordingClient(() => json({}))
|
|
808
|
+
const runtime = CodeMode.make({
|
|
809
|
+
tools: { neurocode: OpenAPI.fromSpec({ spec: await neurocodeSpec(), baseUrl }).tools },
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
const result = await Effect.runPromise(
|
|
813
|
+
runtime.execute("return await tools.neurocode.v2.session.get({})").pipe(Effect.provide(layer)),
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
expect(result).toMatchObject({ ok: false })
|
|
817
|
+
expect(JSON.stringify(result)).toContain("Missing required path parameter 'sessionID'")
|
|
818
|
+
expect(requests).toHaveLength(0)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
test("prefixes cross-location collisions and reconstructs the HTTP request", async () => {
|
|
822
|
+
const spec = {
|
|
823
|
+
openapi: "3.1.0",
|
|
824
|
+
info: { title: "collision", version: "1.0.0" },
|
|
825
|
+
paths: {
|
|
826
|
+
"/echo": {
|
|
827
|
+
post: {
|
|
828
|
+
operationId: "echo",
|
|
829
|
+
requestBody: {
|
|
830
|
+
required: true,
|
|
831
|
+
content: { "application/json": { schema: { type: "string" } } },
|
|
832
|
+
},
|
|
833
|
+
responses: { "204": { description: "Echoed" } },
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
"/things/{id}": {
|
|
837
|
+
post: {
|
|
838
|
+
operationId: "things.update",
|
|
839
|
+
parameters: [
|
|
840
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
841
|
+
{ name: "id", in: "query", required: true, schema: { type: "string" } },
|
|
842
|
+
{ name: "path_id", in: "query", schema: { type: "string" } },
|
|
843
|
+
{ name: "id", in: "header", required: true, schema: { type: "string" } },
|
|
844
|
+
],
|
|
845
|
+
requestBody: {
|
|
846
|
+
required: true,
|
|
847
|
+
content: {
|
|
848
|
+
"application/json": {
|
|
849
|
+
schema: {
|
|
850
|
+
type: "object",
|
|
851
|
+
properties: { id: { type: "string" } },
|
|
852
|
+
required: ["id"],
|
|
853
|
+
additionalProperties: false,
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
responses: { "204": { description: "Updated" } },
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
} satisfies Document
|
|
863
|
+
const { requests, layer } = recordingClient(() => new Response(null, { status: 204 }))
|
|
864
|
+
const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools
|
|
865
|
+
const update = toolAt(tools, "things.update")
|
|
866
|
+
const echo = toolAt(tools, "echo")
|
|
867
|
+
|
|
868
|
+
expect(Tool.isDefinition(update)).toBe(true)
|
|
869
|
+
if (!Tool.isDefinition(update)) throw new Error("things.update was not generated")
|
|
870
|
+
expect(inputTypeScript(update)).toBe(
|
|
871
|
+
"{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }",
|
|
872
|
+
)
|
|
873
|
+
expect(Tool.isDefinition(echo)).toBe(true)
|
|
874
|
+
if (!Tool.isDefinition(echo)) throw new Error("echo was not generated")
|
|
875
|
+
expect(inputTypeScript(echo)).toBe("{ body: string }")
|
|
876
|
+
|
|
877
|
+
const runtime = CodeMode.make({ tools })
|
|
878
|
+
const result = await Effect.runPromise(
|
|
879
|
+
runtime
|
|
880
|
+
.execute(
|
|
881
|
+
`
|
|
882
|
+
const updated = await tools.things.update({ path_id: "path", query_id: "query", path_id_2: "literal", header_id: "header", body_id: "body" })
|
|
883
|
+
const echoed = await tools.echo({ body: "hello" })
|
|
884
|
+
return { updated, echoed }
|
|
885
|
+
`,
|
|
886
|
+
)
|
|
887
|
+
.pipe(Effect.provide(layer)),
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
expect(result).toMatchObject({ ok: true })
|
|
891
|
+
expect(requests).toHaveLength(2)
|
|
892
|
+
expect(new URL(requests[0]!.url).pathname).toBe("/things/path")
|
|
893
|
+
expect(new URL(requests[0]!.url).searchParams.get("id")).toBe("query")
|
|
894
|
+
expect(new URL(requests[0]!.url).searchParams.get("path_id")).toBe("literal")
|
|
895
|
+
expect(requests[0]!.headers.id).toBe("header")
|
|
896
|
+
expect(requests[0]!.body).toStrictEqual({ id: "body" })
|
|
897
|
+
expect(requests[1]!.body).toBe("hello")
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
test("keeps bodies nested when flattening would lose schema semantics", () => {
|
|
901
|
+
const body = (schema: Record<string, unknown>, required = true) => ({
|
|
902
|
+
required,
|
|
903
|
+
content: { "application/json": { schema } },
|
|
904
|
+
})
|
|
905
|
+
const spec = {
|
|
906
|
+
openapi: "3.1.0",
|
|
907
|
+
info: { title: "bodies", version: "1.0.0" },
|
|
908
|
+
paths: Object.fromEntries(
|
|
909
|
+
[
|
|
910
|
+
[
|
|
911
|
+
"optional",
|
|
912
|
+
body(
|
|
913
|
+
{
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: { name: { type: "string" } },
|
|
916
|
+
required: ["name"],
|
|
917
|
+
additionalProperties: false,
|
|
918
|
+
},
|
|
919
|
+
false,
|
|
920
|
+
),
|
|
921
|
+
],
|
|
922
|
+
["dictionary", body({ type: "object", additionalProperties: { type: "string" } })],
|
|
923
|
+
[
|
|
924
|
+
"composed",
|
|
925
|
+
body({
|
|
926
|
+
type: "object",
|
|
927
|
+
allOf: [{ type: "object", properties: { name: { type: "string" } }, required: ["name"] }],
|
|
928
|
+
additionalProperties: false,
|
|
929
|
+
}),
|
|
930
|
+
],
|
|
931
|
+
[
|
|
932
|
+
"nullable",
|
|
933
|
+
body({
|
|
934
|
+
type: ["object", "null"],
|
|
935
|
+
properties: { name: { type: "string" } },
|
|
936
|
+
additionalProperties: false,
|
|
937
|
+
}),
|
|
938
|
+
],
|
|
939
|
+
].map(([name, requestBody]) => [
|
|
940
|
+
`/body/${name}`,
|
|
941
|
+
{
|
|
942
|
+
post: {
|
|
943
|
+
operationId: `body.${name}`,
|
|
944
|
+
requestBody,
|
|
945
|
+
responses: { "204": { description: "Accepted" } },
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
]),
|
|
949
|
+
),
|
|
950
|
+
} satisfies Document
|
|
951
|
+
const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools
|
|
952
|
+
|
|
953
|
+
for (const name of ["optional", "dictionary", "composed", "nullable"]) {
|
|
954
|
+
const tool = toolAt(tools, `body.${name}`)
|
|
955
|
+
expect(Tool.isDefinition(tool)).toBe(true)
|
|
956
|
+
if (!Tool.isDefinition(tool)) throw new Error(`body.${name} was not generated`)
|
|
957
|
+
const input = isRecord(tool.input) ? tool.input : {}
|
|
958
|
+
expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"])
|
|
959
|
+
}
|
|
960
|
+
const optional = toolAt(tools, "body.optional")
|
|
961
|
+
if (!Tool.isDefinition(optional)) throw new Error("body.optional was not generated")
|
|
962
|
+
expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }")
|
|
963
|
+
})
|
|
964
|
+
})
|