diff --git a/.changeset/gold-maps-itch.md b/.changeset/gold-maps-itch.md new file mode 100644 index 000000000000..9c385feb2d22 --- /dev/null +++ b/.changeset/gold-maps-itch.md @@ -0,0 +1,42 @@ +--- +"wrangler": patch +--- + +fix: Add hyperdrive binding support in `getPlatformProxy` + +example: + +```toml +# wrangler.toml +[[hyperdrive]] +binding = "MY_HYPERDRIVE" +id = "000000000000000000000000000000000" +localConnectionString = "postgres://user:pass@127.0.0.1:1234/db" +``` + +```js +// index.mjs + +import postgres from "postgres"; +import { getPlatformProxy } from "wrangler"; + +const { env, dispose } = await getPlatformProxy(); + +try { + const sql = postgres( + // Note: connectionString points to `postgres://user:pass@127.0.0.1:1234/db` not to the actual hyperdrive + // connection string, for more details see the explanation below + env.MY_HYPERDRIVE.connectionString + ); + const results = await sql`SELECT * FROM pg_tables`; + await sql.end(); +} catch (e) { + console.error(e); +} + +await dispose(); +``` + +Note: the returned binding values are no-op/passthrough that can be used inside node.js, meaning +that besides direct connections via the `connect` methods, all the other values point to the +same db connection specified in the user configuration diff --git a/.changeset/slimy-waves-dance.md b/.changeset/slimy-waves-dance.md new file mode 100644 index 000000000000..68a30109f7e3 --- /dev/null +++ b/.changeset/slimy-waves-dance.md @@ -0,0 +1,9 @@ +--- +"miniflare": patch +--- + +fix: add hyperdrive bindings support in `getBindings` + +Note: the returned binding values are no-op/passthrough that can be used inside node.js, meaning +that besides direct connections via the `connect` methods, all the other values point to the +same db connection specified in the user configuration diff --git a/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts b/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts index e43de85fc522..a7c4812d7462 100644 --- a/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts +++ b/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts @@ -1,4 +1,5 @@ import { readdir } from "fs/promises"; +import * as nodeNet from "node:net"; import path from "path"; import { D1Database, @@ -18,7 +19,12 @@ import { import { unstable_dev } from "wrangler"; import { getPlatformProxy } from "./shared"; import type { NamedEntrypoint } from "../workers/rpc-worker"; -import type { KVNamespace, Rpc, Service } from "@cloudflare/workers-types"; +import type { + Hyperdrive, + KVNamespace, + Rpc, + Service, +} from "@cloudflare/workers-types"; import type { UnstableDevWorker } from "wrangler"; type Env = { @@ -35,6 +41,7 @@ type Env = { MY_DO_B: DurableObjectNamespace; MY_BUCKET: R2Bucket; MY_D1: D1Database; + MY_HYPERDRIVE: Hyperdrive; }; const wranglerTomlFilePath = path.join(__dirname, "..", "wrangler.toml"); @@ -303,6 +310,27 @@ describe("getPlatformProxy - env", () => { } }); + // Important: the hyperdrive values are passthrough ones since the workerd specific hyperdrive values only make sense inside + // workerd itself and would simply not work in a node.js process + it("correctly obtains passthrough Hyperdrive bindings", async () => { + const { env, dispose } = await getPlatformProxy({ + configPath: wranglerTomlFilePath, + }); + try { + const { MY_HYPERDRIVE } = env; + expect(MY_HYPERDRIVE.connectionString).toEqual( + "postgres://user:pass@127.0.0.1:1234/db" + ); + expect(MY_HYPERDRIVE.database).toEqual("db"); + expect(MY_HYPERDRIVE.host).toEqual("127.0.0.1"); + expect(MY_HYPERDRIVE.user).toEqual("user"); + expect(MY_HYPERDRIVE.password).toEqual("pass"); + expect(MY_HYPERDRIVE.port).toEqual(1234); + } finally { + await dispose(); + } + }); + describe("with a target environment", () => { it("should provide bindings targeting a specified environment and also inherit top-level ones", async () => { const { env, dispose } = await getPlatformProxy({ diff --git a/fixtures/get-platform-proxy/wrangler.toml b/fixtures/get-platform-proxy/wrangler.toml index b9cc9e7d3a0d..073ac72d415d 100644 --- a/fixtures/get-platform-proxy/wrangler.toml +++ b/fixtures/get-platform-proxy/wrangler.toml @@ -27,6 +27,11 @@ bindings = [ { name = "MY_DO_B", script_name = "do-worker-b", class_name = "DurableObjectClass" } ] +[[hyperdrive]] +binding = "MY_HYPERDRIVE" +id = "000000000000000000000000000000000" +localConnectionString = "postgres://user:pass@127.0.0.1:1234/db" + [[d1_databases]] binding = "MY_D1" database_name = "test-db" diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 26d943b1de8f..ce3f0f51a1d3 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -39,13 +39,13 @@ import { getDirectSocketName, getGlobalServices, HOST_CAPNP_CONNECT, - kProxyNodeBinding, KV_PLUGIN_NAME, normaliseDurableObject, PLUGIN_ENTRIES, Plugins, PluginServicesOptions, ProxyClient, + ProxyNodeBinding, QueueConsumers, QueueProducers, QUEUES_PLUGIN_NAME, @@ -1668,13 +1668,16 @@ export class Miniflare { // missing in other plugins' options. const pluginBindings = await plugin.getNodeBindings(workerOpts[key]); for (const [name, binding] of Object.entries(pluginBindings)) { - if (binding === kProxyNodeBinding) { + if (binding instanceof ProxyNodeBinding) { const proxyBindingName = getProxyBindingName(key, workerName, name); - const proxy = proxyClient.env[proxyBindingName]; + let proxy = proxyClient.env[proxyBindingName]; assert( proxy !== undefined, `Expected ${proxyBindingName} to be bound` ); + if (binding.proxyOverrideHandler) { + proxy = new Proxy(proxy, binding.proxyOverrideHandler); + } bindings[name] = proxy; } else { bindings[name] = binding; diff --git a/packages/miniflare/src/plugins/assets/index.ts b/packages/miniflare/src/plugins/assets/index.ts index 8f5a0284b2e4..e00ee56acf45 100644 --- a/packages/miniflare/src/plugins/assets/index.ts +++ b/packages/miniflare/src/plugins/assets/index.ts @@ -10,7 +10,7 @@ import { z } from "zod"; import { Service } from "../../runtime"; import { SharedBindings } from "../../workers"; import { getUserServiceName } from "../core"; -import { kProxyNodeBinding, Plugin } from "../shared"; +import { Plugin, ProxyNodeBinding } from "../shared"; import { ASSETS_KV_SERVICE_NAME, ASSETS_PLUGIN_NAME, @@ -39,7 +39,7 @@ export const ASSETS_PLUGIN: Plugin = { return {}; } return { - [options.assets.bindingName]: kProxyNodeBinding, + [options.assets.bindingName]: new ProxyNodeBinding(), }; }, diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 5cdb75582d02..22b878dcd8b6 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -36,10 +36,10 @@ import { import { getCacheServiceName } from "../cache"; import { DURABLE_OBJECTS_STORAGE_SERVICE_NAME } from "../do"; import { - kProxyNodeBinding, kUnsafeEphemeralUniqueKey, parseRoutes, Plugin, + ProxyNodeBinding, SERVICE_LOOPBACK, WORKER_BINDING_SERVICE_LOOPBACK, } from "../shared"; @@ -473,7 +473,7 @@ export const CORE_PLUGIN: Plugin< bindingEntries.push( ...Object.keys(options.serviceBindings).map((name) => [ name, - kProxyNodeBinding, + new ProxyNodeBinding(), ]) ); } @@ -481,7 +481,7 @@ export const CORE_PLUGIN: Plugin< bindingEntries.push( ...Object.keys(options.wrappedBindings).map((name) => [ name, - kProxyNodeBinding, + new ProxyNodeBinding(), ]) ); } diff --git a/packages/miniflare/src/plugins/d1/index.ts b/packages/miniflare/src/plugins/d1/index.ts index 057e8c5a1ed2..49089e9046f0 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -10,13 +10,13 @@ import { SharedBindings } from "../../workers"; import { getMiniflareObjectBindings, getPersistPath, - kProxyNodeBinding, migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, PersistenceSchema, Plugin, + ProxyNodeBinding, SERVICE_LOOPBACK, } from "../shared"; @@ -69,7 +69,7 @@ export const D1_PLUGIN: Plugin< getNodeBindings(options) { const databases = namespaceKeys(options.d1Databases); return Object.fromEntries( - databases.map((name) => [name, kProxyNodeBinding]) + databases.map((name) => [name, new ProxyNodeBinding()]) ); }, async getServices({ diff --git a/packages/miniflare/src/plugins/do/index.ts b/packages/miniflare/src/plugins/do/index.ts index 628b98485cb3..e28af3135160 100644 --- a/packages/miniflare/src/plugins/do/index.ts +++ b/packages/miniflare/src/plugins/do/index.ts @@ -4,10 +4,10 @@ import { Worker_Binding } from "../../runtime"; import { getUserServiceName } from "../core"; import { getPersistPath, - kProxyNodeBinding, kUnsafeEphemeralUniqueKey, PersistenceSchema, Plugin, + ProxyNodeBinding, UnsafeUniqueKey, } from "../shared"; @@ -83,7 +83,9 @@ export const DURABLE_OBJECTS_PLUGIN: Plugin< }, getNodeBindings(options) { const objects = Object.keys(options.durableObjects ?? {}); - return Object.fromEntries(objects.map((name) => [name, kProxyNodeBinding])); + return Object.fromEntries( + objects.map((name) => [name, new ProxyNodeBinding()]) + ); }, async getServices({ sharedOptions, diff --git a/packages/miniflare/src/plugins/hyperdrive/index.ts b/packages/miniflare/src/plugins/hyperdrive/index.ts index c61f062d12f8..97d1d90291b3 100644 --- a/packages/miniflare/src/plugins/hyperdrive/index.ts +++ b/packages/miniflare/src/plugins/hyperdrive/index.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { z } from "zod"; import { Service, Worker_Binding } from "../../runtime"; -import { Plugin } from "../shared"; +import { Plugin, ProxyNodeBinding } from "../shared"; export const HYPERDRIVE_PLUGIN_NAME = "hyperdrive"; @@ -90,8 +90,24 @@ export const HYPERDRIVE_PLUGIN: Plugin = { } ); }, - getNodeBindings() { - return {}; + getNodeBindings(options) { + return Object.fromEntries( + Object.entries(options.hyperdrives ?? {}).map(([name, url]) => { + const connectionOverrides: Record = { + connectionString: `${url}`, + port: Number.parseInt(url.port), + host: url.hostname, + }; + const proxyNodeBinding = new ProxyNodeBinding({ + get(target, prop) { + return prop in connectionOverrides + ? connectionOverrides[prop] + : target[prop]; + }, + }); + return [name, proxyNodeBinding]; + }) + ); }, async getServices({ options }) { return Object.entries(options.hyperdrives ?? {}).map( diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index 45e5db787384..8ad964a75d15 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -11,13 +11,13 @@ import { SharedBindings } from "../../workers"; import { getMiniflareObjectBindings, getPersistPath, - kProxyNodeBinding, migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, PersistenceSchema, Plugin, + ProxyNodeBinding, SERVICE_LOOPBACK, } from "../shared"; import { KV_PLUGIN_NAME } from "./constants"; @@ -77,7 +77,7 @@ export const KV_PLUGIN: Plugin< async getNodeBindings(options) { const namespaces = namespaceKeys(options.kvNamespaces); const bindings = Object.fromEntries( - namespaces.map((name) => [name, kProxyNodeBinding]) + namespaces.map((name) => [name, new ProxyNodeBinding()]) ); if (isWorkersSitesEnabled(options)) { diff --git a/packages/miniflare/src/plugins/kv/sites.ts b/packages/miniflare/src/plugins/kv/sites.ts index 8be1482a0e30..e819f5be73f9 100644 --- a/packages/miniflare/src/plugins/kv/sites.ts +++ b/packages/miniflare/src/plugins/kv/sites.ts @@ -12,7 +12,7 @@ import { SiteMatcherRegExps, testSiteRegExps, } from "../../workers"; -import { kProxyNodeBinding } from "../shared"; +import { ProxyNodeBinding } from "../shared"; import { KV_PLUGIN_NAME } from "./constants"; async function* listKeysInDirectoryInner( @@ -97,7 +97,7 @@ export async function getSitesNodeBindings( siteRegExps ); return { - [SiteBindings.KV_NAMESPACE_SITE]: kProxyNodeBinding, + [SiteBindings.KV_NAMESPACE_SITE]: new ProxyNodeBinding(), [SiteBindings.JSON_SITE_MANIFEST]: __STATIC_CONTENT_MANIFEST, }; } diff --git a/packages/miniflare/src/plugins/queues/index.ts b/packages/miniflare/src/plugins/queues/index.ts index 85db2c3b7345..ccca8eaa4814 100644 --- a/packages/miniflare/src/plugins/queues/index.ts +++ b/packages/miniflare/src/plugins/queues/index.ts @@ -15,9 +15,9 @@ import { import { getUserServiceName } from "../core"; import { getMiniflareObjectBindings, - kProxyNodeBinding, objectEntryWorker, Plugin, + ProxyNodeBinding, SERVICE_LOOPBACK, } from "../shared"; @@ -53,7 +53,9 @@ export const QUEUES_PLUGIN: Plugin = { }, getNodeBindings(options) { const queues = bindingKeys(options.queueProducers); - return Object.fromEntries(queues.map((name) => [name, kProxyNodeBinding])); + return Object.fromEntries( + queues.map((name) => [name, new ProxyNodeBinding()]) + ); }, async getServices({ options, diff --git a/packages/miniflare/src/plugins/r2/index.ts b/packages/miniflare/src/plugins/r2/index.ts index f4fc5208e32c..59c60338a6b3 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -10,13 +10,13 @@ import { SharedBindings } from "../../workers"; import { getMiniflareObjectBindings, getPersistPath, - kProxyNodeBinding, migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, PersistenceSchema, Plugin, + ProxyNodeBinding, SERVICE_LOOPBACK, } from "../shared"; @@ -51,7 +51,9 @@ export const R2_PLUGIN: Plugin< }, getNodeBindings(options) { const buckets = namespaceKeys(options.r2Buckets); - return Object.fromEntries(buckets.map((name) => [name, kProxyNodeBinding])); + return Object.fromEntries( + buckets.map((name) => [name, new ProxyNodeBinding()]) + ); }, async getServices({ options, diff --git a/packages/miniflare/src/plugins/ratelimit/index.ts b/packages/miniflare/src/plugins/ratelimit/index.ts index 5c787b2057b6..91b4c55a8267 100644 --- a/packages/miniflare/src/plugins/ratelimit/index.ts +++ b/packages/miniflare/src/plugins/ratelimit/index.ts @@ -1,7 +1,7 @@ import SCRIPT_RATELIMIT_OBJECT from "worker:ratelimit/ratelimit"; import { z } from "zod"; import { Worker_Binding } from "../../runtime"; -import { kProxyNodeBinding, Plugin } from "../shared"; +import { Plugin, ProxyNodeBinding } from "../shared"; export enum PeriodType { TENSECONDS = 10, @@ -57,7 +57,10 @@ export const RATELIMIT_PLUGIN: Plugin = { return {}; } return Object.fromEntries( - Object.keys(options.ratelimits).map((name) => [name, kProxyNodeBinding]) + Object.keys(options.ratelimits).map((name) => [ + name, + new ProxyNodeBinding(), + ]) ); }, async getServices({ options }) { diff --git a/packages/miniflare/src/plugins/shared/index.ts b/packages/miniflare/src/plugins/shared/index.ts index 63942ef34e30..49ec49049779 100644 --- a/packages/miniflare/src/plugins/shared/index.ts +++ b/packages/miniflare/src/plugins/shared/index.ts @@ -116,9 +116,12 @@ export type Plugin< ? { sharedOptions?: undefined } : { sharedOptions: SharedOptions }); -// When this is returned as the binding from `PluginBase#getNodeBindings()`, -// Miniflare will replace it with a proxy to the binding in `workerd` -export const kProxyNodeBinding = Symbol("kProxyNodeBinding"); +// When an instance of this class is returned as the binding from `PluginBase#getNodeBindings()`, +// Miniflare will replace it with a proxy to the binding in `workerd`, alongside applying the +// specified overrides (if there is any) +export class ProxyNodeBinding { + constructor(public proxyOverrideHandler?: ProxyHandler) {} +} export function namespaceKeys( namespaces?: Record | string[] diff --git a/packages/miniflare/test/plugins/hyperdrive/index.spec.ts b/packages/miniflare/test/plugins/hyperdrive/index.spec.ts index 750147e0dda3..4055bf6ebf06 100644 --- a/packages/miniflare/test/plugins/hyperdrive/index.spec.ts +++ b/packages/miniflare/test/plugins/hyperdrive/index.spec.ts @@ -1,5 +1,6 @@ import test from "ava"; import { Miniflare, MiniflareOptions } from "miniflare"; +import type { Hyperdrive } from "@cloudflare/workers-types/experimental"; test("fields match expected", async (t) => { const connectionString = `postgresql://user:password@localhost:5432/database`; @@ -34,6 +35,30 @@ test("fields match expected", async (t) => { t.is(hyperdrive.port, 5432); }); +test("fields in binding proxy match expected", async (t) => { + const connectionString = "postgresql://user:password@localhost:5432/database"; + const mf = new Miniflare({ + modules: true, + script: "export default { fetch() {} }", + hyperdrives: { + HYPERDRIVE: connectionString, + }, + }); + t.teardown(() => mf.dispose()); + const { HYPERDRIVE } = await mf.getBindings<{ HYPERDRIVE: Hyperdrive }>(); + t.is(HYPERDRIVE.user, "user"); + t.is(HYPERDRIVE.password, "password"); + t.is(HYPERDRIVE.database, "database"); + t.is(HYPERDRIVE.port, 5432); + + // Important: the checks below differ from what the worker code would get inside workerd, this is necessary since getting the binding via `getBindings` implies that + // the binding is going to be used inside node.js and not within workerd where the hyperdrive connection is actually set, so the values need need to remain + // the exact same making the hyperdrive binding work as a simple no-op/passthrough (returning the workerd hyperdrive values wouldn't work as those would not + // work/have any meaning in a node.js process) + t.is(HYPERDRIVE.connectionString, connectionString); + t.is(HYPERDRIVE.host, "localhost"); +}); + test("validates config", async (t) => { const opts: MiniflareOptions = { modules: true, script: "" }; const mf = new Miniflare(opts); diff --git a/packages/wrangler/e2e/get-platform-proxy.test.ts b/packages/wrangler/e2e/get-platform-proxy.test.ts index 4a0acabb9125..ad28062205ec 100644 --- a/packages/wrangler/e2e/get-platform-proxy.test.ts +++ b/packages/wrangler/e2e/get-platform-proxy.test.ts @@ -1,57 +1,134 @@ import { execSync } from "child_process"; +import * as nodeNet from "node:net"; import dedent from "ts-dedent"; import { beforeEach, describe, expect, it } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; import { makeRoot, seed } from "./helpers/setup"; import { WRANGLER_IMPORT } from "./helpers/wrangler"; -// TODO(DEVX-1262): re-enable when we have set an API token with the proper AI permissions -describe.skip("getPlatformProxy()", () => { - let root: string; - beforeEach(async () => { - root = await makeRoot(); - - await seed(root, { - "wrangler.toml": dedent` - name = "ai-app" - account_id = "${CLOUDFLARE_ACCOUNT_ID}" - compatibility_date = "2023-01-01" - compatibility_flags = ["nodejs_compat"] - - [ai] - binding = "AI" - `, - "index.mjs": dedent/*javascript*/ ` - import { getPlatformProxy } from "${WRANGLER_IMPORT}" - - const { env } = await getPlatformProxy(); - const messages = [ - { - role: "user", - // Doing snapshot testing against AI responses can be flaky, but this prompt generates the same output relatively reliably - content: "Respond with the exact text 'This is a response from Workers AI.'. Do not include any other text", - }, - ]; - - const content = await env.AI.run("@hf/thebloke/zephyr-7b-beta-awq", { - messages, - }); +describe("getPlatformProxy()", () => { + // TODO(DEVX-1262): re-enable when we have set an API token with the proper AI permissions + describe.skip("Workers AI", () => { + let root: string; + beforeEach(async () => { + root = makeRoot(); + + await seed(root, { + "wrangler.toml": dedent` + name = "ai-app" + account_id = "${CLOUDFLARE_ACCOUNT_ID}" + compatibility_date = "2023-01-01" + compatibility_flags = ["nodejs_compat"] + + [ai] + binding = "AI" + `, + "index.mjs": dedent/*javascript*/ ` + import { getPlatformProxy } from "${WRANGLER_IMPORT}" + + const { env } = await getPlatformProxy(); + const messages = [ + { + role: "user", + // Doing snapshot testing against AI responses can be flaky, but this prompt generates the same output relatively reliably + content: "Respond with the exact text 'This is a response from Workers AI.'. Do not include any other text", + }, + ]; + + const content = await env.AI.run("@hf/thebloke/zephyr-7b-beta-awq", { + messages, + }); - console.log(content.response); - - process.exit(0); - `, - "package.json": dedent` - { - "name": "ai-app", - "version": "0.0.0", - "private": true - } - `, + console.log(content.response); + + process.exit(0); + `, + "package.json": dedent` + { + "name": "ai-app", + "version": "0.0.0", + "private": true + } + `, + }); + }); + it("can run ai inference", async () => { + const stdout = execSync(`node index.mjs`, { + cwd: root, + encoding: "utf-8", + }); + expect(stdout).toContain("Workers AI"); }); }); - it("can run ai inference", async () => { - const stdout = execSync(`node index.mjs`, { cwd: root, encoding: "utf-8" }); - expect(stdout).toContain("Workers AI"); + + describe("Hyperdrive", () => { + let root: string; + let port = 5432; + let server: nodeNet.Server; + + beforeEach(async () => { + server = nodeNet.createServer().listen(); + + if (server.address() && typeof server.address() !== "string") { + port = (server.address() as nodeNet.AddressInfo).port; + } + + root = makeRoot(); + + await seed(root, { + "wrangler.toml": dedent` + name = "hyperdrive-app" + compatibility_date = "2024-08-20" + compatibility_flags = ["nodejs_compat"] + + [[hyperdrive]] + binding = "HYPERDRIVE" + id = "hyperdrive_id" + localConnectionString = "postgresql://user:%21pass@127.0.0.1:${port}/some_db" + `, + "index.mjs": dedent/*javascript*/ ` + import { getPlatformProxy } from "${WRANGLER_IMPORT}"; + + const { env, dispose } = await getPlatformProxy(); + + const conn = env.HYPERDRIVE.connect(); + await conn.writable.getWriter().write(new TextEncoder().encode("test string sent using getPlatformProxy")); + + await dispose(); + `, + "package.json": dedent` + { + "name": "hyperdrive-app", + "version": "0.0.0", + "private": true + } + `, + }); + }); + + it.skipIf( + // in CI this test fails for windows because of ECONNRESET issues + process.platform === "win32" + )( + "can connect to a TCP socket via the hyperdrive connect method", + async () => { + const socketDataMsgPromise = new Promise((resolve, _) => { + server.on("connection", (sock) => { + sock.on("data", (data) => { + resolve(new TextDecoder().decode(data)); + server.close(); + }); + }); + }); + + execSync("node index.mjs", { + cwd: root, + encoding: "utf-8", + }); + expect(await socketDataMsgPromise).toMatchInlineSnapshot( + `"test string sent using getPlatformProxy"` + ); + } + ); }); });