History

Merge pull request #690 from RhysSullivan/codex/real-protocol-testing-first-pass

Add real protocol testing fixtures

Rhys Sullivan committed 6 months ago

Commit 63cdca32 parents b87fecc ac6b02a25 files · +702 −197Browse files

Unified · Split

25 files
  1. .agents/skills/wrdn-effect-raw-fetch-boundary/SKILL.md +58 −0
  2. .oxlintrc.jsonc +1 −0
  3. bun.lock +57 −7
  4. notes/real-protocol-testing-plan.md +22 −25
  5. packages/core/sdk/package.json +8 −1
  6. packages/core/sdk/src/testing.ts +32 −1
  7. packages/core/sdk/tsup.config.ts +1 −0
  8. packages/plugins/graphql/package.json +11 −2
  9. packages/plugins/graphql/src/sdk/introspect.ts +2 −1
  10. packages/plugins/graphql/src/sdk/plugin.test.ts +118 −111
  11. packages/plugins/graphql/src/testing/index.ts +195 −0
  12. packages/plugins/graphql/tsup.config.ts +2 −1
  13. packages/plugins/graphql/vitest.config.ts +5 −0
  14. packages/plugins/mcp/package.json +8 −1
  15. packages/plugins/mcp/src/sdk/elicitation.test.ts +1 −1
  16. packages/plugins/mcp/src/sdk/plugin.test.ts +1 −1
  17. packages/plugins/mcp/src/testing/index.ts +1 −0
  18. packages/plugins/mcp/src/testing/server.ts +0 −6
  19. packages/plugins/mcp/tsup.config.ts +1 −0
  20. packages/plugins/openapi/package.json +8 −1
  21. packages/plugins/openapi/src/sdk/plugin.test.ts +5 −38
  22. packages/plugins/openapi/src/testing/index.ts +86 −0
  23. packages/plugins/openapi/tsup.config.ts +1 −0
  24. scripts/oxlint-plugin-executor.js +2 −0
  25. scripts/oxlint-plugin-executor/rules/no-raw-fetch.js +76 −0
.agents/skills/wrdn-effect-raw-fetch-boundary/SKILL.md+58−0
@@ -0,0 +1,58 @@
1---
2name: wrdn-effect-raw-fetch-boundary
3description: Route HTTP through Effect boundaries instead of raw fetch. Use when lint flags executor/no-raw-fetch or when adding networked protocol/provider code.
4allowed-tools: Read Grep Glob Bash
5---
6
7HTTP in core SDK and protocol plugins should go through Effect services so tests
8can replace real networks with local protocol fixtures or mock `HttpClient`
9layers.
10
11## Fix Shape
12
13- Prefer `effect/unstable/http` `HttpClient` and `HttpClientRequest` for
14 ordinary HTTP calls.
15- Accept a `Layer.Layer<HttpClient.HttpClient>` option on plugin/provider code
16 when callers need to inject a test client.
17- In tests, use real local servers plus `FetchHttpClient.layer` or a captured
18 `HttpClient` service from the test layer.
19- Do not add fetch-shaped abstractions in SDK or plugin seams. If a third-party
20 library truly only accepts `fetch`, keep the adapter in the owning package,
21 name the forced boundary explicitly, and delegate internally to Effect
22 `HttpClient`.
23- Do not type new protocol/plugin seams as `typeof globalThis.fetch`; keep the
24 ambient runtime boundary out of domain and test APIs.
25- Do not patch `globalThis.fetch`. Replace those tests with a local server,
26 `HttpClient` layer, or the approved Effect-backed adapter.
27- Do not add a broad allowlist entry unless the file is a platform entrypoint
28 or a temporary migration target.
29
30## Approved Boundaries
31
32- Worker/handler objects whose public API must expose a `fetch` method.
33- Test calls to a worker or Miniflare binding's `.fetch(...)` method.
34- Small adapters for libraries that only accept `fetch`, if the implementation
35 delegates to Effect `HttpClient`.
36- Browser UI event handlers may remain raw only until app-side boundaries are
37 classified; prefer SDK/client APIs where available.
38
39## Bad
40
41```ts
42const response = await fetch(url);
43```
44
45```ts
46const fetchImpl = options.fetch ?? globalThis.fetch;
47```
48
49## Good
50
51```ts
52const client = yield * HttpClient.HttpClient;
53const response = yield * client.execute(HttpClientRequest.get(url));
54```
55
56```ts
57const plugin = graphqlPlugin({ httpClientLayer: testHttpClientLayer });
58```
.oxlintrc.jsonc+1−0
@@ -17,6 +17,7 @@ · 16 unmodified lines
17 "executor/no-manual-tag-check": "error",17 "executor/no-manual-tag-check": "error",
18 "executor/no-promise-catch": "error",18 "executor/no-promise-catch": "error",
19 "executor/no-promise-reject": "error",19 "executor/no-promise-reject": "error",
20 "executor/no-raw-fetch": "error",
20 "executor/no-redundant-primitive-cast": "error",21 "executor/no-redundant-primitive-cast": "error",
21 "executor/no-redundant-error-factory": "error",22 "executor/no-redundant-error-factory": "error",
22 "executor/no-ts-nocheck": "error",23 "executor/no-ts-nocheck": "error",
bun.lock+57−7
@@ -601,6 +601,8 @@ · 600 unmodified lines
601 "@executor-js/config": "workspace:*",601 "@executor-js/config": "workspace:*",
602 "@executor-js/sdk": "workspace:*",602 "@executor-js/sdk": "workspace:*",
603 "effect": "catalog:",603 "effect": "catalog:",
604 "graphql": "^16.12.0",
605 "graphql-yoga": "^5.17.0",
604 },606 },
605 "devDependencies": {607 "devDependencies": {
606 "@effect/atom-react": "catalog:",608 "@effect/atom-react": "catalog:",
@@ -1157,6 +1159,12 @@ · 550 unmodified lines
1157 1159
1158 "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],1160 "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
1159 1161
1162 "@envelop/core": ["@envelop/core@5.5.1", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw=="],
1163
1164 "@envelop/instrumentation": ["@envelop/instrumentation@1.0.0", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.2.1", "tslib": "^2.5.0" } }, "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw=="],
1165
1166 "@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="],
1167
1160 "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],1168 "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
1161 1169
1162 "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],1170 "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
@@ -1277,6 +1285,8 @@ · 114 unmodified lines
1277 1285
1278 "@executor-js/vite-plugin": ["@executor-js/vite-plugin@workspace:packages/core/vite-plugin"],1286 "@executor-js/vite-plugin": ["@executor-js/vite-plugin@workspace:packages/core/vite-plugin"],
1279 1287
1288 "@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
1289
1280 "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],1290 "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
1281 1291
1282 "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],1292 "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
@@ -1289,6 +1299,22 @@ · 6 unmodified lines
1289 1299
1290 "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="],1300 "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="],
1291 1301
1302 "@graphql-tools/executor": ["@graphql-tools/executor@1.5.3", "", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA=="],
1303
1304 "@graphql-tools/merge": ["@graphql-tools/merge@9.1.9", "", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q=="],
1305
1306 "@graphql-tools/schema": ["@graphql-tools/schema@10.0.33", "", { "dependencies": { "@graphql-tools/merge": "^9.1.9", "@graphql-tools/utils": "^11.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-O6P3RIftO0jafnSsFAqpjurUuUxJ43s/AdPVLQsBkI6y4Ic/tKm4C1Qm1KKQsCDTOxXPJClh/v3g7k7yLKCFBQ=="],
1307
1308 "@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="],
1309
1310 "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
1311
1312 "@graphql-yoga/logger": ["@graphql-yoga/logger@2.0.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA=="],
1313
1314 "@graphql-yoga/subscription": ["@graphql-yoga/subscription@5.0.5", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/events": "^0.1.0", "tslib": "^2.8.1" } }, "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw=="],
1315
1316 "@graphql-yoga/typed-event-target": ["@graphql-yoga/typed-event-target@3.0.2", "", { "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.8.1" } }, "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA=="],
1317
1292 "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],1318 "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
1293 1319
1294 "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],1320 "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
@@ -1939,6 +1965,8 @@ · 644 unmodified lines
1939 1965
1940 "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],1966 "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
1941 1967
1968 "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="],
1969
1942 "@rhyssul/portless": ["@rhyssul/portless@0.13.3", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-oPvwXJIIkRg2i1CUEUsz8sAdFSf0Fzzv+NmaacAsi6ZZTHxDOofbO6fGInVnbESIFOu4k8a/rXOZRF0aqLAUgw=="],1970 "@rhyssul/portless": ["@rhyssul/portless@0.13.3", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-oPvwXJIIkRg2i1CUEUsz8sAdFSf0Fzzv+NmaacAsi6ZZTHxDOofbO6fGInVnbESIFOu4k8a/rXOZRF0aqLAUgw=="],
1943 1971
1944 "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],1972 "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],
@@ -2337,6 +2365,18 @@ · 392 unmodified lines
2337 2365
2338 "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="],2366 "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="],
2339 2367
2368 "@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
2369
2370 "@whatwg-node/events": ["@whatwg-node/events@0.1.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ=="],
2371
2372 "@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.13", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.8.3", "urlpattern-polyfill": "^10.0.0" } }, "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q=="],
2373
2374 "@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.8.5", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-4xzCl/zphPqlp9tASLVeUhB5+WJHbuWGYpfoC2q1qh5dw0AqZBW7L27V5roxYWijPxj4sspRAAoOH3d2ztaHUQ=="],
2375
2376 "@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@1.3.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA=="],
2377
2378 "@whatwg-node/server": ["@whatwg-node/server@0.10.18", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/fetch": "^0.10.13", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-kMwLlxUbduttIgaPdSkmEarFpP+mSY8FEm+QWMBRJwxOHWkri+cxd8KZHO9EMrB9vgUuz+5WEaCawaL5wGVoXg=="],
2379
2340 "@workos-inc/node": ["@workos-inc/node@8.13.0", "", { "dependencies": { "eventemitter3": "^5.0.4" } }, "sha512-NgQKHpwh8AbT4KvAsW91Y+4f4jja2IvFPQ5atcy5NUxUMVRgXzRFEee3erawfXrTmiCVqJjd9PljHySKBXmHKQ=="],2380 "@workos-inc/node": ["@workos-inc/node@8.13.0", "", { "dependencies": { "eventemitter3": "^5.0.4" } }, "sha512-NgQKHpwh8AbT4KvAsW91Y+4f4jja2IvFPQ5atcy5NUxUMVRgXzRFEee3erawfXrTmiCVqJjd9PljHySKBXmHKQ=="],
2341 2381
2342 "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],2382 "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
@@ -2637,6 +2677,8 @@ · 294 unmodified lines
2637 2677
2638 "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],2678 "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
2639 2679
2680 "cross-inspect": ["cross-inspect@1.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A=="],
2681
2640 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],2682 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
2641 2683
2642 "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],2684 "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
@@ -3083,6 +3125,10 @@ · 440 unmodified lines
3083 3125
3084 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],3126 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
3085 3127
3128 "graphql": ["graphql@16.14.0", "", {}, "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q=="],
3129
3130 "graphql-yoga": ["graphql-yoga@5.21.0", "", { "dependencies": { "@envelop/core": "^5.5.1", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^1.5.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.11.0", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.3.2", "@whatwg-node/server": "^0.10.14", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0" } }, "sha512-PS37UoDihx8209RRl1ogttzWevNYDnGvP7beHkwHzUpUdfZTHsVRTVe1ysGXre1EjwUAePbpez302YSrq70Ngw=="],
3131
3086 "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],3132 "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
3087 3133
3088 "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="],3134 "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="],
@@ -3431,7 +3477,7 @@ · 342 unmodified lines
3431 3477
3432 "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],3478 "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
3433 3479
3434 "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],3480 "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
3435 3481
3436 "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],3482 "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
3437 3483
@@ -4417,6 +4463,8 @@ · 979 unmodified lines
4417 4463
4418 "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],4464 "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],
4419 4465
4466 "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
4467
4420 "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],4468 "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
4421 4469
4422 "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="],4470 "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="],
@@ -4645,6 +4693,12 @@ · 222 unmodified lines
4645 4693
4646 "@executor-js/storage-postgres/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],4694 "@executor-js/storage-postgres/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
4647 4695
4696 "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="],
4697
4698 "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="],
4699
4700 "@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="],
4701
4648 "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],4702 "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
4649 4703
4650 "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],4704 "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -4679,8 +4733,6 @@ · 28 unmodified lines
4679 4733
4680 "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],4734 "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
4681 4735
4682 "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
4683
4684 "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],4736 "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],
4685 4737
4686 "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],4738 "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
@@ -4885,8 +4937,6 @@ · 198 unmodified lines
4885 4937
4886 "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],4938 "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
4887 4939
4888 "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
4889
4890 "cacache/p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],4940 "cacache/p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
4891 4941
4892 "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],4942 "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
@@ -5027,8 +5077,6 @@ · 134 unmodified lines
5027 5077
5028 "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],5078 "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
5029 5079
5030 "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
5031
5032 "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],5080 "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
5033 5081
5034 "posthog-js/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="],5082 "posthog-js/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="],
@@ -5101,6 +5149,8 @@ · 66 unmodified lines
5101 5149
5102 "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],5150 "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
5103 5151
5152 "unstorage/lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
5153
5104 "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],5154 "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
5105 5155
5106 "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],5156 "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
notes/real-protocol-testing-plan.md+22−25
@@ -46,7 +46,7 @@ Testing services belong behind explicit `./testing` subpath exports: · 45 unmodified lines
46 46
47The plugin packages own the protocol details. Core SDK testing owns only boring47The plugin packages own the protocol details. Core SDK testing owns only boring
48Executor fixtures such as memory adapters, memory secrets, auto-accept48Executor fixtures such as memory adapters, memory secrets, auto-accept
49elicitation, and fetch-shaped adapters that delegate to Effect HTTP.49elicitation, and other Effect-native test primitives.
50 50
51Use `TestLayer` / `TestLayers` naming for realistic local test services.51Use `TestLayer` / `TestLayers` naming for realistic local test services.
52Reserve `LiveLayer` / `LiveLayers` for production wiring. These servers are52Reserve `LiveLayer` / `LiveLayers` for production wiring. These servers are
@@ -124,7 +124,6 @@ Suggested file layout: · 71 unmodified lines
124```txt124```txt
125packages/core/sdk/src/testing.ts125packages/core/sdk/src/testing.ts
126packages/core/sdk/src/testing/126packages/core/sdk/src/testing/
127 fetch.ts
128 memory-secrets.ts127 memory-secrets.ts
129 128
130packages/plugins/openapi/src/testing/129packages/plugins/openapi/src/testing/
@@ -191,9 +190,6 @@ Useful additions: · 60 unmodified lines
191 in-test secret provider definitions.190 in-test secret provider definitions.
192- `makeExecutorTestLayer(options)`, if repeated `createExecutor(makeTestConfig)`191- `makeExecutorTestLayer(options)`, if repeated `createExecutor(makeTestConfig)`
193 setup becomes noisy.192 setup becomes noisy.
194- `makeFetchFromHttpClient` or `FetchTestAdapter`, for third-party APIs that
195 require a fetch-shaped callback while still routing requests through Effect's
196 HTTP client.
197- Small request/response capture primitives built on `Ref`, only if multiple193- Small request/response capture primitives built on `Ref`, only if multiple
198 plugins need the same shape.194 plugins need the same shape.
199 195
@@ -243,8 +239,8 @@ Work: · 43 unmodified lines
243- Support a fresh `McpServer` factory per session, matching the current helper.239- Support a fresh `McpServer` factory per session, matching the current helper.
244- Keep malformed/non-MCP HTTP endpoints as explicit test layers for probe240- Keep malformed/non-MCP HTTP endpoints as explicit test layers for probe
245 behavior.241 behavior.
246- Replace fetch-injected probe tests with real local HTTP servers and the242- Replace fetch-injected probe tests with real local HTTP servers and
247 Effect-backed fetch adapter.243 Effect-native HTTP boundaries.
248 244
249Representative scenarios:245Representative scenarios:
250 246
@@ -261,10 +257,11 @@ Representative scenarios: · 10 unmodified lines
261GraphQL should gain a real executable schema server. This is the place where a257GraphQL should gain a real executable schema server. This is the place where a
262new dependency may be justified.258new dependency may be justified.
263 259
264Preferred first dependency is the core `graphql` package if it is enough:260Use `graphql-yoga` for the test server. The extra dependency is justified
265define a schema, execute introspection and operations with `graphql(...)`, and261because the fixture needs to exercise a real JSON-over-HTTP GraphQL endpoint,
266serve the standard JSON-over-HTTP GraphQL endpoint through Effect/node HTTP.262not only direct `graphql(...)` execution. Keep Vitest configured to inline the
267Use `graphql-yoga` only if we need a fuller server feature set later.263Yoga/GraphQL dependency chain so executable schemas and server execution share
264one GraphQL module realm.
268 265
269Work:266Work:
270 267
@@ -292,19 +289,19 @@ Representative scenarios: · 21 unmodified lines
292## Fetch Boundary289## Fetch Boundary
293 290
294The target rule is: product and plugin code should not call raw `fetch`291The target rule is: product and plugin code should not call raw `fetch`
295directly. HTTP should go through Effect HTTP or a small approved adapter that292directly. HTTP should go through Effect HTTP. If a third-party library truly
296turns an Effect `HttpClient` into a fetch-shaped function for libraries that293forces a fetch-shaped callback, the adapter should live at that owning package's
297force that API.294boundary rather than in shared SDK testing.
298 295
299Boundary files may exist, but they should be explicit and scarce. Examples:296Boundary files may exist, but they should be explicit and scarce. Examples:
300 297
301- a SDK adapter for `oauth4webapi` custom fetch298- a core OAuth adapter for `oauth4webapi` custom fetch, if that library forces it
302- a MCP transport adapter if the upstream MCP SDK requires fetch-shaped input299- a MCP transport adapter if the upstream MCP SDK requires fetch-shaped input
303- platform entrypoints that must implement a `fetch(request, env, ctx)` method300- platform entrypoints that must implement a `fetch(request, env, ctx)` method
304- test harnesses that intentionally call a Worker binding's `fetch` method301- test harnesses that intentionally call a Worker binding's `fetch` method
305 302
306Everything else should use Effect HTTP, Executor client APIs, or the approved303Everything else should use Effect HTTP, Executor client APIs, or an explicit
307adapter.304package-local boundary.
308 305
309Lint plan:306Lint plan:
310 307
@@ -319,17 +316,17 @@ Lint plan: · 8 unmodified lines
319- Start with a narrow allowlist for known boundary files and remove entries as316- Start with a narrow allowlist for known boundary files and remove entries as
320 migrations land.317 migrations land.
321- Include error messages that tell the author which Effect HTTP or approved318- Include error messages that tell the author which Effect HTTP or approved
322 adapter to use.319 package-local boundary to use.
323 320
324The lint rule should land after the first adapter exists. Otherwise every fix321The lint rule can land once the core test primitives and at least one real
325has to invent its own local escape hatch.322protocol fixture exist. Fetch-shaped adapters should be added only at forced
323library boundaries during migration.
326 324
327## Migration Order325## Migration Order
328 326
3291. Add `./testing` exports and SDK test primitives.3271. Add `./testing` exports and SDK test primitives.
330 - Expose `@executor-js/sdk/testing`.328 - Expose `@executor-js/sdk/testing`.
331 - Add memory secrets test support.329 - Add memory secrets test support.
332 - Add the Effect HTTP to fetch-shaped adapter.
333 - Keep `makeTestConfig` available from the root SDK.330 - Keep `makeTestConfig` available from the root SDK.
334 331
3352. Build the GraphQL vertical slice.3322. Build the GraphQL vertical slice.
@@ -344,8 +341,8 @@ has to invent its own local escape hatch. · 8 unmodified lines
344 - Migrate current tests to import from the new public testing subpaths.341 - Migrate current tests to import from the new public testing subpaths.
345 342
3464. Replace global fetch patching.3434. Replace global fetch patching.
347 - Convert core OAuth discovery/helper tests to real local servers or the344 - Convert core OAuth discovery/helper tests to real local servers or
348 Effect-backed fetch adapter.345 package-local Effect HTTP boundaries.
349 - Convert OpenAPI OAuth tests to the OAuth test server.346 - Convert OpenAPI OAuth tests to the OAuth test server.
350 - Convert MCP probe tests to real local HTTP server layers.347 - Convert MCP probe tests to real local HTTP server layers.
351 - Convert Google Discovery tests using the same boundary pattern.348 - Convert Google Discovery tests using the same boundary pattern.
@@ -403,8 +400,8 @@ Turbo. Do not use `bun test`. · 51 unmodified lines
403 400
404- Should `@executor-js/sdk/testing` be a new subpath around the existing401- Should `@executor-js/sdk/testing` be a new subpath around the existing
405 `src/testing.ts`, or should `src/testing.ts` become `src/testing/index.ts`?402 `src/testing.ts`, or should `src/testing.ts` become `src/testing/index.ts`?
406- Should GraphQL use only the `graphql` package, or should we adopt403- How much GraphQL HTTP behavior should live in the shared fixture versus
407 `graphql-yoga` once tests need more HTTP behavior?404 scenario-specific schemas in individual tests?
408- Which app-level raw fetch calls should remain approved platform boundaries405- Which app-level raw fetch calls should remain approved platform boundaries
409 versus being migrated to Effect HTTP?406 versus being migrated to Effect HTTP?
410- Should the raw fetch lint rule become repo-wide immediately with allowlists,407- Should the raw fetch lint rule become repo-wide immediately with allowlists,
packages/core/sdk/package.json+8−1
@@ -19,7 +19,8 @@ · 18 unmodified lines
19 ".": "./src/index.ts",19 ".": "./src/index.ts",
20 "./core": "./src/index.ts",20 "./core": "./src/index.ts",
21 "./promise": "./src/promise.ts",21 "./promise": "./src/promise.ts",
22 "./client": "./src/client.ts"22 "./client": "./src/client.ts",
23 "./testing": "./src/testing.ts"
23 },24 },
24 "publishConfig": {25 "publishConfig": {
25 "access": "public",26 "access": "public",
@@ -41,6 +42,12 @@ · 15 unmodified lines
41 "types": "./dist/client.d.ts",42 "types": "./dist/client.d.ts",
42 "default": "./dist/client.js"43 "default": "./dist/client.js"
43 }44 }
45 },
46 "./testing": {
47 "import": {
48 "types": "./dist/testing.d.ts",
49 "default": "./dist/testing.js"
50 }
44 }51 }
45 }52 }
46 },53 },
packages/core/sdk/src/testing.ts+32−1
@@ -1,11 +1,14 @@
1import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory";1import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory";
2 2
3import { Effect } from "effect";
4
3import { makeInMemoryBlobStore } from "./blob";5import { makeInMemoryBlobStore } from "./blob";
4import type { ExecutorConfig } from "./executor";6import type { ExecutorConfig } from "./executor";
5import { collectSchemas } from "./executor";7import { collectSchemas } from "./executor";
6import { ScopeId } from "./ids";8import { ScopeId } from "./ids";
7import type { AnyPlugin } from "./plugin";9import { definePlugin, type AnyPlugin } from "./plugin";
8import { Scope } from "./scope";10import { Scope } from "./scope";
11import type { SecretProvider } from "./secrets";
9 12
10// ---------------------------------------------------------------------------13// ---------------------------------------------------------------------------
11// makeTestConfig — build an ExecutorConfig backed by in-memory adapter +14// makeTestConfig — build an ExecutorConfig backed by in-memory adapter +
@@ -42,3 +45,31 @@ export const makeTestConfig = <const TPlugins extends readonly AnyPlugin[] = []> · 30 unmodified lines
42 onElicitation: "accept-all",45 onElicitation: "accept-all",
43 };46 };
44};47};
48
49export const memorySecretsPlugin = definePlugin(() => {
50 const store = new Map<string, string>();
51
52 const provider: SecretProvider = {
53 key: "memory",
54 writable: true,
55 get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null),
56 set: (id, value, scope) =>
57 Effect.sync(() => {
58 store.set(`${scope}\u0000${id}`, value);
59 }),
60 delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)),
61 list: () =>
62 Effect.sync(() =>
63 Array.from(store.keys()).map((key) => {
64 const name = key.split("\u0000", 2)[1] ?? key;
65 return { id: name, name };
66 }),
67 ),
68 };
69
70 return {
71 id: "memory-secrets" as const,
72 storage: () => ({}),
73 secretProviders: [provider],
74 };
75});
packages/core/sdk/tsup.config.ts+1−0
@@ -5,6 +5,7 @@ export default defineConfig({ · 4 unmodified lines
5 index: "src/promise.ts",5 index: "src/promise.ts",
6 core: "src/index.ts",6 core: "src/index.ts",
7 client: "src/client.ts",7 client: "src/client.ts",
8 testing: "src/testing.ts",
8 },9 },
9 format: ["esm"],10 format: ["esm"],
10 dts: false,11 dts: false,
packages/plugins/graphql/package.json+11−2
@@ -21,7 +21,8 @@ · 20 unmodified lines
21 "./api": "./src/api/index.ts",21 "./api": "./src/api/index.ts",
22 "./react": "./src/react/index.ts",22 "./react": "./src/react/index.ts",
23 "./presets": "./src/sdk/presets.ts",23 "./presets": "./src/sdk/presets.ts",
24 "./client": "./src/react/plugin-client.tsx"24 "./client": "./src/react/plugin-client.tsx",
25 "./testing": "./src/testing/index.ts"
25 },26 },
26 "publishConfig": {27 "publishConfig": {
27 "access": "public",28 "access": "public",
@@ -37,6 +38,12 @@ · 9 unmodified lines
37 "types": "./dist/sdk/index.d.ts",38 "types": "./dist/sdk/index.d.ts",
38 "default": "./dist/core.js"39 "default": "./dist/core.js"
39 }40 }
41 },
42 "./testing": {
43 "import": {
44 "types": "./dist/testing/index.d.ts",
45 "default": "./dist/testing.js"
46 }
40 }47 }
41 }48 }
42 },49 },
@@ -51,7 +58,9 @@ · 8 unmodified lines
51 "@effect/platform-node": "catalog:",58 "@effect/platform-node": "catalog:",
52 "@executor-js/config": "workspace:*",59 "@executor-js/config": "workspace:*",
53 "@executor-js/sdk": "workspace:*",60 "@executor-js/sdk": "workspace:*",
54 "effect": "catalog:"61 "effect": "catalog:",
62 "graphql": "^16.12.0",
63 "graphql-yoga": "^5.17.0"
55 },64 },
56 "devDependencies": {65 "devDependencies": {
57 "@effect/atom-react": "catalog:",66 "@effect/atom-react": "catalog:",
packages/plugins/graphql/src/sdk/introspect.ts+2−1
@@ -220,8 +220,9 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 219 unmodified lines
220 );220 );
221 221
222 if (response.status !== 200) {222 if (response.status !== 200) {
223 const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed("<unreadable>")));
223 return yield* new GraphqlIntrospectionError({224 return yield* new GraphqlIntrospectionError({
224 message: `Introspection failed with status ${response.status}`,225 message: `Introspection failed with status ${response.status}: ${body.slice(0, 1_000)}`,
225 });226 });
226 }227 }
227 228
packages/plugins/graphql/src/sdk/plugin.test.ts+118−111
@@ -1,26 +1,24 @@
1import { describe, it, expect } from "@effect/vitest";1import { describe, it, expect } from "@effect/vitest";
2import { Data, Effect } from "effect";2import { Effect } from "effect";
3 3
4import {4import {
5 ConnectionId,5 ConnectionId,
6 CreateConnectionInput,6 CreateConnectionInput,
7 createExecutor,7 createExecutor,
8 definePlugin,
9 makeTestConfig,8 makeTestConfig,
10 Scope,9 Scope,
11 ScopeId,10 ScopeId,
12 SecretId,11 SecretId,
13 type SecretProvider,
14 TokenMaterial,12 TokenMaterial,
15} from "@executor-js/sdk";13} from "@executor-js/sdk";
14import { memorySecretsPlugin } from "@executor-js/sdk/testing";
16 15
17import { graphqlPlugin } from "./plugin";16import { graphqlPlugin } from "./plugin";
18import type { IntrospectionResult } from "./introspect";17import type { IntrospectionResult } from "./introspect";
18import { makeGreetingGraphqlSchema, serveGraphqlTestServer } from "../testing";
19 19
20const TEST_SCOPE = "test-scope";20const TEST_SCOPE = "test-scope";
21 21
22class TestServerAddressError extends Data.TaggedError("TestServerAddressError")<{}> {}
23
24// ---------------------------------------------------------------------------22// ---------------------------------------------------------------------------
25// Mock introspection response23// Mock introspection response
26// ---------------------------------------------------------------------------24// ---------------------------------------------------------------------------
@@ -92,31 +90,119 @@ const introspectionResult: IntrospectionResult = { · 65 unmodified lines
92 90
93const introspectionJson = JSON.stringify({ data: introspectionResult });91const introspectionJson = JSON.stringify({ data: introspectionResult });
94 92
95const makeMemorySecretsPlugin = () => {93describe("graphqlPlugin real protocol server", () => {
96 const store = new Map<string, string>();94 const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() });
97 const provider: SecretProvider = {95
98 key: "memory",96 it.effect("adds a source by introspecting the live GraphQL endpoint", () =>
99 writable: true,97 Effect.gen(function* () {
100 get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null),98 const server = yield* serveGreetingServer;
101 set: (id, value, scope) =>99 const executor = yield* createExecutor(
102 Effect.sync(() => {100 makeTestConfig({ plugins: [graphqlPlugin()] as const }),
103 store.set(`${scope}\u0000${id}`, value);101 );
104 }),102
105 delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)),103 const result = yield* executor.graphql.addSource({
106 list: () =>104 endpoint: server.endpoint,
107 Effect.sync(() =>105 scope: TEST_SCOPE,
108 Array.from(store.keys()).map((key) => {106 namespace: "live_graph",
109 const name = key.split("\u0000", 2)[1] ?? key;107 });
110 return { id: name, name };108
109 expect(result).toEqual({ toolCount: 2, namespace: "live_graph" });
110
111 const tools = yield* executor.tools.list();
112 expect(tools.map((tool) => tool.id)).toEqual(
113 expect.arrayContaining(["live_graph.query.hello", "live_graph.mutation.setGreeting"]),
114 );
115
116 const requests = yield* server.requests;
117 expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true);
118 }),
119 );
120
121 it.effect("invokes a live query with headers and query params", () =>
122 Effect.gen(function* () {
123 const server = yield* serveGreetingServer;
124 const executor = yield* createExecutor(
125 makeTestConfig({ plugins: [graphqlPlugin()] as const }),
126 );
127
128 yield* executor.graphql.addSource({
129 endpoint: server.endpoint,
130 scope: TEST_SCOPE,
131 namespace: "live_invoke",
132 headers: { "x-static": "abc" },
133 queryParams: { token: "qp-token" },
134 });
135 yield* server.clearRequests;
136
137 const result = yield* executor.tools.invoke("live_invoke.query.hello", {
138 name: "Ada",
139 });
140
141 expect(result).toEqual({
142 status: 200,
143 data: { hello: "Hello Ada" },
144 errors: null,
145 });
146
147 const requests = yield* server.requests;
148 expect(requests.length).toBe(1);
149 expect(requests[0]?.headers["x-static"]).toBe("abc");
150 expect(new URL(requests[0]!.url).searchParams.get("token")).toBe("qp-token");
151 expect(requests[0]?.payload.variables).toEqual({ name: "Ada" });
152 }),
153 );
154
155 it.effect("invokes OAuth-backed sources with a bearer token", () =>
156 Effect.gen(function* () {
157 const server = yield* serveGreetingServer;
158 const executor = yield* createExecutor(
159 makeTestConfig({
160 plugins: [memorySecretsPlugin(), graphqlPlugin()] as const,
111 }),161 }),
112 ),162 );
113 };163
114 return definePlugin(() => ({164 const connectionId = ConnectionId.make("graphql-oauth2-test");
115 id: "memory-secrets" as const,165 yield* executor.connections.create(
116 storage: () => ({}),166 new CreateConnectionInput({
117 secretProviders: [provider],167 id: connectionId,
118 }));168 scope: ScopeId.make(TEST_SCOPE),
119};169 provider: "oauth2",
170 identityLabel: "GraphQL Test",
171 accessToken: new TokenMaterial({
172 secretId: SecretId.make(`${connectionId}.access_token`),
173 name: "GraphQL Access Token",
174 value: "secret-token",
175 }),
176 refreshToken: null,
177 expiresAt: null,
178 oauthScope: null,
179 providerState: null,
180 }),
181 );
182
183 yield* executor.graphql.addSource({
184 endpoint: server.endpoint,
185 scope: TEST_SCOPE,
186 namespace: "oauth_graph",
187 auth: { kind: "oauth2", connectionId },
188 });
189 yield* server.clearRequests;
190
191 const result = yield* executor.tools.invoke("oauth_graph.query.hello", {
192 name: "Ada",
193 });
194
195 expect(result).toEqual({
196 status: 200,
197 data: { hello: "Hello Ada" },
198 errors: null,
199 });
200
201 const requests = yield* server.requests;
202 expect(requests[0]?.headers.authorization).toBe("Bearer secret-token");
203 }),
204 );
205});
120 206
121describe("graphqlPlugin", () => {207describe("graphqlPlugin", () => {
122 it.effect("registers tools from introspection JSON", () =>208 it.effect("registers tools from introspection JSON", () =>
@@ -256,85 +342,6 @@ describe("graphqlPlugin", () => { · 133 unmodified lines
256 }),342 }),
257 );343 );
258 344
259 it.effect("invokes OAuth-backed sources with a bearer token", () =>
260 Effect.gen(function* () {
261 const http = yield* Effect.promise(() => import("node:http"));
262 let authorizationHeader: string | null = null;
263 const server = yield* Effect.acquireRelease(
264 Effect.callback<
265 {
266 readonly server: ReturnType<typeof http.createServer>;
267 readonly port: number;
268 },
269 Error | TestServerAddressError
270 >((resume) => {
271 const server = http.createServer((req, res) => {
272 authorizationHeader = req.headers.authorization ?? null;
273 res.setHeader("content-type", "application/json");
274 res.end(JSON.stringify({ data: { hello: "Hello Ada" } }));
275 });
276 server.listen(0, "127.0.0.1", () => {
277 const address = server.address();
278 if (!address || typeof address === "string") {
279 resume(Effect.fail(new TestServerAddressError()));
280 return;
281 }
282 resume(Effect.succeed({ server, port: address.port }));
283 });
284 server.once("error", (error) => resume(Effect.fail(error)));
285 }),
286 ({ server }) =>
287 Effect.callback<void>((resume) => {
288 server.close(() => resume(Effect.void));
289 }).pipe(Effect.ignore),
290 );
291
292 const executor = yield* createExecutor(
293 makeTestConfig({
294 plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const,
295 }),
296 );
297
298 const connectionId = ConnectionId.make("graphql-oauth2-test");
299 yield* executor.connections.create(
300 new CreateConnectionInput({
301 id: connectionId,
302 scope: ScopeId.make(TEST_SCOPE),
303 provider: "oauth2",
304 identityLabel: "GraphQL Test",
305 accessToken: new TokenMaterial({
306 secretId: SecretId.make(`${connectionId}.access_token`),
307 name: "GraphQL Access Token",
308 value: "secret-token",
309 }),
310 refreshToken: null,
311 expiresAt: null,
312 oauthScope: null,
313 providerState: null,
314 }),
315 );
316
317 yield* executor.graphql.addSource({
318 endpoint: `http://127.0.0.1:${server.port}/graphql`,
319 scope: TEST_SCOPE,
320 introspectionJson,
321 namespace: "oauth_graph",
322 auth: { kind: "oauth2", connectionId },
323 });
324
325 const result = yield* executor.tools.invoke("oauth_graph.query.hello", {
326 name: "Ada",
327 });
328
329 expect(result).toEqual({
330 status: 200,
331 data: { hello: "Hello Ada" },
332 errors: null,
333 });
334 expect(authorizationHeader).toBe("Bearer secret-token");
335 }),
336 );
337
338 it.effect("static graphql.addSource delegates to extension", () =>345 it.effect("static graphql.addSource delegates to extension", () =>
339 Effect.gen(function* () {346 Effect.gen(function* () {
340 const executor = yield* createExecutor(347 const executor = yield* createExecutor(
@@ -506,7 +513,7 @@ describe("graphqlPlugin", () => { · 165 unmodified lines
506 Effect.gen(function* () {513 Effect.gen(function* () {
507 const executor = yield* createExecutor(514 const executor = yield* createExecutor(
508 makeTestConfig({515 makeTestConfig({
509 plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const,516 plugins: [memorySecretsPlugin(), graphqlPlugin()] as const,
510 }),517 }),
511 );518 );
512 519
@@ -543,7 +550,7 @@ describe("graphqlPlugin", () => { · 30 unmodified lines
543 Effect.gen(function* () {550 Effect.gen(function* () {
544 const executor = yield* createExecutor(551 const executor = yield* createExecutor(
545 makeTestConfig({552 makeTestConfig({
546 plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const,553 plugins: [memorySecretsPlugin(), graphqlPlugin()] as const,
547 }),554 }),
548 );555 );
549 556
@@ -579,7 +586,7 @@ describe("graphqlPlugin", () => { · 29 unmodified lines
579 Effect.gen(function* () {586 Effect.gen(function* () {
580 const executor = yield* createExecutor(587 const executor = yield* createExecutor(
581 makeTestConfig({588 makeTestConfig({
582 plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const,589 plugins: [memorySecretsPlugin(), graphqlPlugin()] as const,
583 }),590 }),
584 );591 );
585 592
packages/plugins/graphql/src/testing/index.ts+195−0
@@ -0,0 +1,195 @@
1import { createServer, type IncomingHttpHeaders, type Server } from "node:http";
2
3import { Context, Data, Effect, Layer, Ref, Schema as EffectSchema, Scope } from "effect";
4import { GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString } from "graphql";
5import { createYoga, type GraphQLParams, type YogaInitialContext } from "graphql-yoga";
6
7const GraphqlRequestPayload = EffectSchema.Struct({
8 query: EffectSchema.optional(EffectSchema.String),
9 variables: EffectSchema.optional(EffectSchema.Record(EffectSchema.String, EffectSchema.Unknown)),
10 operationName: EffectSchema.optional(EffectSchema.NullOr(EffectSchema.String)),
11});
12
13type GraphqlRequestPayload = typeof GraphqlRequestPayload.Type;
14
15export interface GraphqlTestRequest {
16 readonly url: string;
17 readonly method: string;
18 readonly path: string;
19 readonly headers: Readonly<Record<string, string>>;
20 readonly payload: GraphqlRequestPayload;
21}
22
23export interface GraphqlTestContext {
24 readonly request: GraphqlTestRequest;
25}
26
27export interface GraphqlTestServerOptions {
28 readonly schema: GraphQLSchema;
29 readonly path?: string;
30}
31
32export interface GraphqlTestServerShape {
33 readonly endpoint: string;
34 readonly schema: GraphQLSchema;
35 readonly requests: Effect.Effect<readonly GraphqlTestRequest[]>;
36 readonly clearRequests: Effect.Effect<void>;
37}
38
39class GraphqlTestServerAddressError extends Data.TaggedError("GraphqlTestServerAddressError")<{
40 readonly address: unknown;
41}> {}
42
43class GraphqlTestServerListenError extends Data.TaggedError("GraphqlTestServerListenError")<{
44 readonly cause: unknown;
45}> {}
46
47const headersFromRequest = (
48 headers: Headers | IncomingHttpHeaders,
49): Readonly<Record<string, string>> => {
50 if ("entries" in headers && typeof headers.entries === "function") {
51 return Object.fromEntries(headers.entries());
52 }
53 return Object.fromEntries(
54 Object.entries(headers).flatMap(([name, value]) => {
55 if (value === undefined) return [];
56 return [[name, Array.isArray(value) ? value.join(", ") : value]];
57 }),
58 );
59};
60
61const payloadFromParams = (params: GraphQLParams): GraphqlRequestPayload => ({
62 query: params.query,
63 variables:
64 typeof params.variables === "object" && params.variables !== null
65 ? params.variables
66 : undefined,
67 operationName: params.operationName ?? null,
68});
69
70const captureRequest = (
71 initial: YogaInitialContext,
72 requests: Ref.Ref<readonly GraphqlTestRequest[]>,
73) => {
74 const url = new URL(initial.request.url);
75 const captured: GraphqlTestRequest = {
76 url: initial.request.url,
77 method: initial.request.method,
78 path: url.pathname,
79 headers: headersFromRequest(initial.request.headers),
80 payload: payloadFromParams(initial.params),
81 };
82 return Effect.runPromise(
83 Ref.update(requests, (all) => [...all, captured]).pipe(Effect.as(captured)),
84 );
85};
86
87const closeServer = (server: Server): Effect.Effect<void> =>
88 Effect.callback<void>((resume) => {
89 server.close(() => resume(Effect.void));
90 });
91
92export const serveGraphqlTestServer = (
93 options: GraphqlTestServerOptions,
94): Effect.Effect<
95 GraphqlTestServerShape,
96 GraphqlTestServerAddressError | GraphqlTestServerListenError,
97 Scope.Scope
98> =>
99 Effect.gen(function* () {
100 const requests = yield* Ref.make<readonly GraphqlTestRequest[]>([]);
101 const path = options.path ?? "/graphql";
102
103 const yoga = createYoga<Record<string, never>, GraphqlTestContext>({
104 schema: options.schema,
105 graphqlEndpoint: path,
106 graphiql: false,
107 landingPage: false,
108 logging: false,
109 maskedErrors: false,
110 context: (initial) =>
111 captureRequest(initial, requests).then((request) => ({
112 request,
113 })),
114 });
115 const server = createServer(yoga);
116
117 const port = yield* Effect.acquireRelease(
118 Effect.callback<number, GraphqlTestServerAddressError | GraphqlTestServerListenError>(
119 (resume) => {
120 const onError = (cause: unknown) =>
121 resume(Effect.fail(new GraphqlTestServerListenError({ cause })));
122 server.once("error", onError);
123 server.listen(0, "127.0.0.1", () => {
124 server.off("error", onError);
125 const address = server.address();
126 if (!address || typeof address === "string") {
127 resume(Effect.fail(new GraphqlTestServerAddressError({ address })));
128 return;
129 }
130 resume(Effect.succeed(address.port));
131 });
132 },
133 ),
134 () => closeServer(server),
135 );
136
137 return {
138 endpoint: `http://127.0.0.1:${port}${path}`,
139 schema: options.schema,
140 requests: Ref.get(requests),
141 clearRequests: Ref.set(requests, []),
142 };
143 });
144
145export class GraphqlTestServer extends Context.Service<GraphqlTestServer, GraphqlTestServerShape>()(
146 "@executor-js/plugin-graphql/testing/GraphqlTestServer",
147) {
148 static readonly layer = (options: GraphqlTestServerOptions) =>
149 Layer.effect(GraphqlTestServer, serveGraphqlTestServer(options));
150}
151
152const stringArgument = (
153 args: Readonly<Record<string, unknown>>,
154 key: string,
155 fallback: string,
156): string => {
157 const value = args[key];
158 return typeof value === "string" ? value : fallback;
159};
160
161export const makeGreetingGraphqlSchema = (): GraphQLSchema => {
162 const Query = new GraphQLObjectType<unknown, GraphqlTestContext>({
163 name: "Query",
164 fields: {
165 hello: {
166 type: GraphQLString,
167 description: "Say hello",
168 args: {
169 name: { type: GraphQLString },
170 },
171 resolve: (_source, args) => `Hello ${stringArgument(args, "name", "world")}`,
172 },
173 },
174 });
175
176 const Mutation = new GraphQLObjectType<unknown, GraphqlTestContext>({
177 name: "Mutation",
178 fields: {
179 setGreeting: {
180 type: GraphQLString,
181 description: "Set greeting message",
182 args: {
183 message: { type: new GraphQLNonNull(GraphQLString) },
184 },
185 resolve: (_source, args) => stringArgument(args, "message", ""),
186 },
187 },
188 });
189
190 return new GraphQLSchema({ query: Query, mutation: Mutation });
191};
192
193export const TestLayers = {
194 greeting: () => GraphqlTestServer.layer({ schema: makeGreetingGraphqlSchema() }),
195};
packages/plugins/graphql/tsup.config.ts+2−1
@@ -4,10 +4,11 @@ export default defineConfig({ · 3 unmodified lines
4 entry: {4 entry: {
5 index: "src/promise.ts",5 index: "src/promise.ts",
6 core: "src/sdk/index.ts",6 core: "src/sdk/index.ts",
7 testing: "src/testing/index.ts",
7 },8 },
8 format: ["esm"],9 format: ["esm"],
9 dts: false,10 dts: false,
10 sourcemap: true,11 sourcemap: true,
11 clean: true,12 clean: true,
12 external: [/^@executor-js\//, /^effect/, /^@effect\//],13 external: [/^@executor-js\//, /^effect/, /^@effect\//, /^graphql/, /^graphql-yoga/],
13});14});
packages/plugins/graphql/vitest.config.ts+5−0
@@ -3,5 +3,10 @@ import { defineConfig } from "vitest/config"; · 2 unmodified lines
3export default defineConfig({3export default defineConfig({
4 test: {4 test: {
5 include: ["src/**/*.test.ts"],5 include: ["src/**/*.test.ts"],
6 server: {
7 deps: {
8 inline: ["graphql", "graphql-yoga", "@graphql-tools", "@envelop", "@whatwg-node"],
9 },
10 },
6 },11 },
7});12});
packages/plugins/mcp/package.json+8−1
@@ -21,7 +21,8 @@ · 20 unmodified lines
21 "./api": "./src/api/index.ts",21 "./api": "./src/api/index.ts",
22 "./react": "./src/react/index.ts",22 "./react": "./src/react/index.ts",
23 "./presets": "./src/sdk/presets.ts",23 "./presets": "./src/sdk/presets.ts",
24 "./client": "./src/react/plugin-client.tsx"24 "./client": "./src/react/plugin-client.tsx",
25 "./testing": "./src/testing/index.ts"
25 },26 },
26 "publishConfig": {27 "publishConfig": {
27 "access": "public",28 "access": "public",
@@ -37,6 +38,12 @@ · 9 unmodified lines
37 "types": "./dist/sdk/index.d.ts",38 "types": "./dist/sdk/index.d.ts",
38 "default": "./dist/core.js"39 "default": "./dist/core.js"
39 }40 }
41 },
42 "./testing": {
43 "import": {
44 "types": "./dist/testing/index.d.ts",
45 "default": "./dist/testing.js"
46 }
40 }47 }
41 }48 }
42 },49 },
packages/plugins/mcp/src/sdk/elicitation.test.ts+1−1
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
12} from "@executor-js/sdk";12} from "@executor-js/sdk";
13 13
14import { mcpPlugin } from "./plugin";14import { mcpPlugin } from "./plugin";
15import { serveMcpServer } from "./test-utils";15import { serveMcpServer } from "../testing";
16 16
17// ---------------------------------------------------------------------------17// ---------------------------------------------------------------------------
18// Test MCP server on a real HTTP port18// Test MCP server on a real HTTP port
packages/plugins/mcp/src/sdk/plugin.test.ts+1−1
@@ -18,7 +18,7 @@ import { · 17 unmodified lines
18 18
19import { mcpPlugin } from "./plugin";19import { mcpPlugin } from "./plugin";
20import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest";20import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest";
21import { serveMcpServer } from "./test-utils";21import { serveMcpServer } from "../testing";
22 22
23// ---------------------------------------------------------------------------23// ---------------------------------------------------------------------------
24// Memory secrets plugin — without a writable provider in the stack,24// Memory secrets plugin — without a writable provider in the stack,
packages/plugins/mcp/src/testing/index.ts+1−0
@@ -0,0 +1 @@
1export { serveMcpServer, type McpTestServer } from "./server";
packages/plugins/mcp/src/testing/server.tsrenamed from packages/plugins/mcp/src/sdk/test-utils.ts+0−6
@@ -10,12 +10,6 @@ export type McpTestServer = { · 9 unmodified lines
10 readonly sessionCount: () => number;10 readonly sessionCount: () => number;
11};11};
12 12
13/**
14 * Spin up a real HTTP server backed by a fresh `McpServer` per session,
15 * with proper session routing via `mcp-session-id`. The factory is invoked
16 * once per new session, so tools registered inside it are isolated per
17 * connection (matching the SDK's own server/transport coupling).
18 */
19export const serveMcpServer = (factory: () => McpServer) =>13export const serveMcpServer = (factory: () => McpServer) =>
20 Effect.acquireRelease(14 Effect.acquireRelease(
21 Effect.callback<McpTestServer, Error>((resume) => {15 Effect.callback<McpTestServer, Error>((resume) => {
packages/plugins/mcp/tsup.config.ts+1−0
@@ -4,6 +4,7 @@ export default defineConfig({ · 3 unmodified lines
4 entry: {4 entry: {
5 index: "src/promise.ts",5 index: "src/promise.ts",
6 core: "src/sdk/index.ts",6 core: "src/sdk/index.ts",
7 testing: "src/testing/index.ts",
7 },8 },
8 format: ["esm"],9 format: ["esm"],
9 dts: false,10 dts: false,
packages/plugins/openapi/package.json+8−1
@@ -21,7 +21,8 @@ · 20 unmodified lines
21 "./api": "./src/api/index.ts",21 "./api": "./src/api/index.ts",
22 "./react": "./src/react/index.ts",22 "./react": "./src/react/index.ts",
23 "./client": "./src/react/plugin-client.tsx",23 "./client": "./src/react/plugin-client.tsx",
24 "./presets": "./src/sdk/presets.ts"24 "./presets": "./src/sdk/presets.ts",
25 "./testing": "./src/testing/index.ts"
25 },26 },
26 "publishConfig": {27 "publishConfig": {
27 "access": "public",28 "access": "public",
@@ -37,6 +38,12 @@ · 9 unmodified lines
37 "types": "./dist/sdk/index.d.ts",38 "types": "./dist/sdk/index.d.ts",
38 "default": "./dist/core.js"39 "default": "./dist/core.js"
39 }40 }
41 },
42 "./testing": {
43 "import": {
44 "types": "./dist/testing/index.d.ts",
45 "default": "./dist/testing.js"
46 }
40 }47 }
41 }48 }
42 },49 },
packages/plugins/openapi/src/sdk/plugin.test.ts+5−38
@@ -13,20 +13,20 @@ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; · 12 unmodified lines
13import {13import {
14 createExecutor,14 createExecutor,
15 type DBAdapter,15 type DBAdapter,
16 definePlugin,
17 makeTestConfig,16 makeTestConfig,
18 Scope,17 Scope,
19 ScopeId,18 ScopeId,
20 SecretId,19 SecretId,
21 SetSecretInput,20 SetSecretInput,
22 type InvokeOptions,21 type InvokeOptions,
23 type SecretProvider,
24 type Where,22 type Where,
25} from "@executor-js/sdk";23} from "@executor-js/sdk";
24import { memorySecretsPlugin } from "@executor-js/sdk/testing";
26 25
27const TEST_SCOPE = "test-scope";26const TEST_SCOPE = "test-scope";
28import { openApiPlugin } from "./plugin";27import { openApiPlugin } from "./plugin";
29import { OAuth2Auth, OpenApiSourceBindingInput } from "./types";28import { OAuth2Auth, OpenApiSourceBindingInput } from "./types";
29import { makeOpenApiTestServer } from "../testing";
30 30
31const autoApprove: InvokeOptions = { onElicitation: "accept-all" };31const autoApprove: InvokeOptions = { onElicitation: "accept-all" };
32 32
@@ -53,38 +53,6 @@ const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => · 20 unmodified lines
53 ),53 ),
54});54});
55 55
56// ---------------------------------------------------------------------------
57// In-memory secrets provider plugin — registered alongside openapi so
58// executor.secrets.set/get work in tests.
59// ---------------------------------------------------------------------------
60
61const memoryProvider: SecretProvider = (() => {
62 const store = new Map<string, string>();
63 return {
64 key: "memory",
65 writable: true,
66 get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null),
67 set: (id, value, scope) =>
68 Effect.sync(() => {
69 store.set(`${scope}\u0000${id}`, value);
70 }),
71 delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)),
72 list: () =>
73 Effect.sync(() =>
74 Array.from(store.keys()).map((k) => {
75 const name = k.split("\u0000", 2)[1] ?? k;
76 return { id: name, name };
77 }),
78 ),
79 };
80})();
81
82const memorySecretsPlugin = definePlugin(() => ({
83 id: "memory-secrets" as const,
84 storage: () => ({}),
85 secretProviders: [memoryProvider],
86}));
87
88// ---------------------------------------------------------------------------56// ---------------------------------------------------------------------------
89// Define a test API with Effect HttpApi57// Define a test API with Effect HttpApi
90// ---------------------------------------------------------------------------58// ---------------------------------------------------------------------------
@@ -186,19 +154,18 @@ const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLog · 95 unmodified lines
186layer(TestLayer)("OpenAPI Plugin", (it) => {154layer(TestLayer)("OpenAPI Plugin", (it) => {
187 it.effect("previewSpec returns metadata and header presets", () =>155 it.effect("previewSpec returns metadata and header presets", () =>
188 Effect.gen(function* () {156 Effect.gen(function* () {
189 const httpClient = yield* HttpClient.HttpClient;157 const server = yield* makeOpenApiTestServer({ spec });
190 const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);
191 158
192 const executor = yield* createExecutor(159 const executor = yield* createExecutor(
193 makeTestConfig({160 makeTestConfig({
194 plugins: [161 plugins: [
195 openApiPlugin({ httpClientLayer: clientLayer }),162 openApiPlugin({ httpClientLayer: server.httpClientLayer }),
196 memorySecretsPlugin(),163 memorySecretsPlugin(),
197 ] as const,164 ] as const,
198 }),165 }),
199 );166 );
200 167
201 const preview = yield* executor.openapi.previewSpec(specJson);168 const preview = yield* executor.openapi.previewSpec(server.specJson);
202 169
203 expect(preview.operationCount).toBeGreaterThanOrEqual(2);170 expect(preview.operationCount).toBeGreaterThanOrEqual(2);
204 expect(preview.servers).toBeDefined();171 expect(preview.servers).toBeDefined();
packages/plugins/openapi/src/testing/index.ts+86−0
@@ -0,0 +1,86 @@
1import { Context, Data, Effect, Layer, Predicate, Schema } from "effect";
2import { HttpClient, HttpServer } from "effect/unstable/http";
3
4export class OpenApiTestServerAddressError extends Data.TaggedError(
5 "OpenApiTestServerAddressError",
6)<{
7 readonly address: unknown;
8}> {}
9
10export class OpenApiTestServerSpecError extends Data.TaggedError("OpenApiTestServerSpecError")<{
11 readonly cause: unknown;
12}> {}
13
14export interface OpenApiTestServerOptions {
15 readonly spec: unknown;
16}
17
18export interface OpenApiTestServerShape {
19 readonly baseUrl: string;
20 readonly specJson: string;
21 readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>;
22}
23
24const decodeJsonSpec = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
25
26const isJsonObject = (value: unknown): value is Readonly<Record<string, unknown>> =>
27 typeof value === "object" && value !== null && !Array.isArray(value);
28
29export const openApiSpecJsonWithServer = (
30 spec: unknown,
31 baseUrl: string,
32): Effect.Effect<string, OpenApiTestServerSpecError> =>
33 Effect.gen(function* () {
34 const parsed =
35 typeof spec === "string"
36 ? yield* decodeJsonSpec(spec).pipe(
37 Effect.mapError((cause) => new OpenApiTestServerSpecError({ cause })),
38 )
39 : spec;
40 const withServer = isJsonObject(parsed)
41 ? {
42 ...parsed,
43 servers: [{ url: baseUrl }],
44 }
45 : parsed;
46 return yield* Effect.try({
47 try: () => JSON.stringify(withServer),
48 catch: (cause) => new OpenApiTestServerSpecError({ cause }),
49 });
50 });
51
52export const makeOpenApiTestServer = (
53 options: OpenApiTestServerOptions,
54): Effect.Effect<
55 OpenApiTestServerShape,
56 OpenApiTestServerAddressError | OpenApiTestServerSpecError,
57 HttpClient.HttpClient | HttpServer.HttpServer
58> =>
59 Effect.gen(function* () {
60 const server = yield* HttpServer.HttpServer;
61 const address = server.address;
62 if (!Predicate.isTagged(address, "TcpAddress")) {
63 return yield* new OpenApiTestServerAddressError({ address });
64 }
65
66 const client = yield* HttpClient.HttpClient;
67 const baseUrl = `http://127.0.0.1:${address.port}`;
68 const specJson = yield* openApiSpecJsonWithServer(options.spec, baseUrl);
69
70 return {
71 baseUrl,
72 specJson,
73 httpClientLayer: Layer.succeed(HttpClient.HttpClient, client),
74 };
75 });
76
77export class OpenApiTestServer extends Context.Service<OpenApiTestServer, OpenApiTestServerShape>()(
78 "@executor-js/plugin-openapi/testing/OpenApiTestServer",
79) {
80 static readonly layer = (options: OpenApiTestServerOptions) =>
81 Layer.effect(OpenApiTestServer, makeOpenApiTestServer(options));
82}
83
84export const TestLayers = {
85 server: OpenApiTestServer.layer,
86};
packages/plugins/openapi/tsup.config.ts+1−0
@@ -4,6 +4,7 @@ export default defineConfig({ · 3 unmodified lines
4 entry: {4 entry: {
5 index: "src/promise.ts",5 index: "src/promise.ts",
6 core: "src/sdk/index.ts",6 core: "src/sdk/index.ts",
7 testing: "src/testing/index.ts",
7 },8 },
8 format: ["esm"],9 format: ["esm"],
9 dts: false,10 dts: false,
scripts/oxlint-plugin-executor.js+2−0
@@ -12,6 +12,7 @@ import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check · 11 unmodified lines
12import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js";12import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js";
13import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";13import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";
14import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js";14import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js";
15import noRawFetch from "./oxlint-plugin-executor/rules/no-raw-fetch.js";
15import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";16import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";
16import noRedundantPrimitiveCast from "./oxlint-plugin-executor/rules/no-redundant-primitive-cast.js";17import noRedundantPrimitiveCast from "./oxlint-plugin-executor/rules/no-redundant-primitive-cast.js";
17import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";18import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";
@@ -48,6 +49,7 @@ export default { · 30 unmodified lines
48 "no-promise-catch": noPromiseCatch,49 "no-promise-catch": noPromiseCatch,
49 "no-promise-client-surface": noPromiseClientSurface,50 "no-promise-client-surface": noPromiseClientSurface,
50 "no-promise-reject": noPromiseReject,51 "no-promise-reject": noPromiseReject,
52 "no-raw-fetch": noRawFetch,
51 "no-raw-error-throw": noRawErrorThrow,53 "no-raw-error-throw": noRawErrorThrow,
52 "no-redundant-primitive-cast": noRedundantPrimitiveCast,54 "no-redundant-primitive-cast": noRedundantPrimitiveCast,
53 "no-redundant-error-factory": noRedundantErrorFactory,55 "no-redundant-error-factory": noRedundantErrorFactory,
scripts/oxlint-plugin-executor/rules/no-raw-fetch.js+76−0
@@ -0,0 +1,76 @@
1import {
2 getPropertyName,
3 isDeclarationFile,
4 isIdentifier,
5 toRepoRelative,
6 unwrapExpression,
7} from "../utils.js";
8
9const message =
10 "Do not call raw fetch in core/plugin code. Route HTTP through Effect HttpClient or an explicit package-local boundary. Skill: wrdn-effect-raw-fetch-boundary.";
11
12const checkedPrefixes = [
13 "packages/core/sdk/src/",
14 "packages/plugins/graphql/src/",
15 "packages/plugins/mcp/src/",
16 "packages/plugins/openapi/src/",
17];
18
19const temporaryAllowedFiles = new Set([
20 "packages/core/sdk/src/oauth-discovery.ts",
21 "packages/core/sdk/src/oauth-discovery.test.ts",
22 "packages/core/sdk/src/oauth-helpers.test.ts",
23 "packages/core/sdk/src/oauth-service.ts",
24 "packages/plugins/mcp/src/sdk/probe-shape.ts",
25 "packages/plugins/openapi/src/sdk/client-credentials-oauth.test.ts",
26 "packages/plugins/openapi/src/sdk/multi-scope-oauth.test.ts",
27 "packages/plugins/openapi/src/sdk/oauth-refresh.test.ts",
28]);
29
30const shouldCheck = (filename) => {
31 const normalized = toRepoRelative(filename);
32 if (isDeclarationFile(filename)) return false;
33 if (temporaryAllowedFiles.has(normalized)) return false;
34 return checkedPrefixes.some((prefix) => normalized.startsWith(prefix));
35};
36
37const isGlobalFetchMember = (node) => {
38 const expression = unwrapExpression(node);
39 if (expression?.type !== "MemberExpression") return false;
40 const object = unwrapExpression(expression.object);
41 const property = getPropertyName(expression.property);
42 return (
43 property === "fetch" &&
44 (isIdentifier(object, "globalThis") ||
45 isIdentifier(object, "window") ||
46 isIdentifier(object, "self"))
47 );
48};
49
50const isBareFetchCall = (node) => isIdentifier(unwrapExpression(node), "fetch");
51
52export default {
53 meta: {
54 type: "problem",
55 docs: {
56 description: "Disallow raw fetch outside approved Effect HTTP boundaries.",
57 },
58 },
59 create(context) {
60 if (!shouldCheck(context.filename)) return {};
61
62 return {
63 CallExpression(node) {
64 if (isBareFetchCall(node.callee) || isGlobalFetchMember(node.callee)) {
65 context.report({ node: node.callee, message });
66 }
67 },
68 MemberExpression(node) {
69 if (node.parent?.type === "CallExpression" && node.parent.callee === node) return;
70 if (isGlobalFetchMember(node)) {
71 context.report({ node, message });
72 }
73 },
74 };
75 },
76};