From 3018c7270064f6bd8159f34d698e9af5bc6afbbb Mon Sep 17 00:00:00 2001 From: weixiaoing <1537476031@qq.com> Date: Thu, 10 Sep 2026 21:55:42 +0800 Subject: [PATCH] fix(scripting): flush Node output before exit Signed-off-by: weixiaoing <1537476031@qq.com> --- package.json | 1 + src/lib/scriptEngine.ts | 14 ++++--- tests/node-host-wrapper.test.mjs | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 tests/node-host-wrapper.test.mjs diff --git a/package.json b/package.json index ef0146c..b1a7b37 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "main": "./src/plugin.ts", "scripts": { "build": "node build.mjs", + "test": "node --test tests/*.test.mjs", "release": "node build.mjs && node generate-manifest.mjs" }, "peerDependencies": { diff --git a/src/lib/scriptEngine.ts b/src/lib/scriptEngine.ts index bb54d4e..e1c1a14 100644 --- a/src/lib/scriptEngine.ts +++ b/src/lib/scriptEngine.ts @@ -165,6 +165,11 @@ export const nodeHostWrapperSource = ` 'use strict'; var Worker = require('worker_threads').Worker; var _chunks = []; +function _writeResult(result, exitCode) { + process.stdout.write(JSON.stringify(result) + '\\n', function() { + process.exit(exitCode); + }); +} process.stdin.on('data', function(c) { _chunks.push(c); }); process.stdin.on('end', function() { var _input = JSON.parse(Buffer.concat(_chunks).toString('utf-8')); @@ -183,23 +188,20 @@ process.stdin.on('end', function() { var _timeout = setTimeout(function() { _worker.terminate(); - process.stdout.write(JSON.stringify({ success: false, logs: [], error: 'Script execution timed out after 10000ms', cancelled: false, modifiedVariables: {} }) + '\\n'); - process.exit(1); + _writeResult({ success: false, logs: [], error: 'Script execution timed out after 10000ms', cancelled: false, modifiedVariables: {} }, 1); }, 10000); _worker.on('message', function(msg) { if (msg.type === 'done') { clearTimeout(_timeout); - process.stdout.write(JSON.stringify({ success: msg.success, logs: msg.logs || [], assertions: msg.assertions || [], cancelled: msg.cancelled || false, error: msg.error, modifiedRequest: msg.modifiedRequest, modifiedResponse: msg.modifiedResponse, modifiedVariables: msg.modifiedVariables || {} }) + '\\n'); _worker.terminate(); - process.exit(msg.success ? 0 : 1); + _writeResult({ success: msg.success, logs: msg.logs || [], assertions: msg.assertions || [], cancelled: msg.cancelled || false, error: msg.error, modifiedRequest: msg.modifiedRequest, modifiedResponse: msg.modifiedResponse, modifiedVariables: msg.modifiedVariables || {} }, msg.success ? 0 : 1); } }); _worker.on('error', function(err) { clearTimeout(_timeout); - process.stdout.write(JSON.stringify({ success: false, logs: [], error: err.stack || err.message || String(err), cancelled: false, modifiedVariables: {} }) + '\\n'); - process.exit(1); + _writeResult({ success: false, logs: [], error: err.stack || err.message || String(err), cancelled: false, modifiedVariables: {} }, 1); }); _worker.postMessage({ type: 'start', script: _input.scriptBody, request: _input.request || {}, response: _input.response || null, envData: _envData, variablesData: _variablesData }); diff --git a/tests/node-host-wrapper.test.mjs b/tests/node-host-wrapper.test.mjs new file mode 100644 index 0000000..c82c953 --- /dev/null +++ b/tests/node-host-wrapper.test.mjs @@ -0,0 +1,72 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +import { transform } from "esbuild"; + +async function loadScriptEngineSources() { + const source = await readFile(new URL("../src/lib/scriptEngine.ts", import.meta.url), "utf8"); + const { code } = await transform(source, { + format: "esm", + loader: "ts", + target: "node20", + }); + const moduleUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`; + return import(moduleUrl); +} + +function runNodeWrapper(nodeHostWrapperSource, payload) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ["-e", nodeHostWrapperSource], { + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (chunk) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + child.on("error", reject); + child.on("close", (code) => resolve({ code, stdout, stderr })); + child.stdin.end(JSON.stringify(payload)); + }); +} + +test("node worker flushes large JSON results before exiting", async () => { + const { nodeHostWrapperSource, workerSource } = await loadScriptEngineSources(); + const body = "A".repeat(4 * 1024 * 1024); + const result = await runNodeWrapper(nodeHostWrapperSource, { + workerSource, + scriptBody: "voiden.request.body = voiden.request.body;", + request: { body }, + response: null, + envVars: {}, + variables: {}, + }); + + assert.equal(result.code, 0, result.stderr); + const parsed = JSON.parse(result.stdout.trim()); + assert.equal(parsed.success, true); + assert.equal(parsed.modifiedRequest.body, body); +}); + +test("node worker preserves the failure exit code after flushing", async () => { + const { nodeHostWrapperSource, workerSource } = await loadScriptEngineSources(); + const result = await runNodeWrapper(nodeHostWrapperSource, { + workerSource, + scriptBody: 'throw new Error("boom");', + request: {}, + response: null, + envVars: {}, + variables: {}, + }); + + assert.equal(result.code, 1); + const parsed = JSON.parse(result.stdout.trim()); + assert.equal(parsed.success, false); + assert.match(parsed.error, /boom/); +});