@metacall/faas
Version:
Reimplementation of MetaCall FaaS platform written in TypeScript.
189 lines (188 loc) • 8.85 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = require("assert");
const child_process_1 = require("child_process");
const path_1 = __importDefault(require("path"));
// Helper: build the envStringified object the same way deployProcess does.
// This is a pure-function extraction of the logic we fixed, so we can unit-test
// it without starting an actual metacall process.
function buildEnv(env) {
const envStringified = {
...process.env
};
if (env) {
for (const [key, value] of Object.entries(env)) {
envStringified[key] = String(value);
}
}
return envStringified;
}
// Helper: invoke a function that may be sync or async, just as the worker does.
async function safeInvoke(fn, args) {
try {
const result = await fn(...args);
return { result };
}
catch (err) {
return { error: String(err) };
}
}
// Fix: Environment Variable Injection
// Ensures all user-supplied env values are stringified so spawn() never throws.
describe('Fix: Environment Variable Injection', function () {
it('should cast boolean env values to strings', () => {
const input = {};
input['FEATURE_FLAG'] = String(true);
const env = buildEnv(input);
assert_1.strict.strictEqual(typeof env['FEATURE_FLAG'], 'string');
assert_1.strict.strictEqual(env['FEATURE_FLAG'], 'true');
});
it('should cast numeric env values to strings', () => {
const input = {};
input['PORT'] = String(8080);
const env = buildEnv(input);
assert_1.strict.strictEqual(typeof env['PORT'], 'string');
assert_1.strict.strictEqual(env['PORT'], '8080');
});
it('should preserve string env values unchanged', () => {
const input = {};
input['MY_SECRET'] = 'abc123';
const env = buildEnv(input);
assert_1.strict.strictEqual(env['MY_SECRET'], 'abc123');
});
it('should merge user env on top of process.env without dropping keys', () => {
const input = {};
input['TEST_VAR'] = 'hello';
const env = buildEnv(input);
// process.env keys must still be present
assert_1.strict.ok('PATH' in env || 'HOME' in env || 'USER' in env);
// User key takes priority
assert_1.strict.strictEqual(env['TEST_VAR'], 'hello');
});
it('should result in a plain object with only string values (no undefined)', () => {
const input = {};
input['A'] = 'x';
input['B'] = String(42);
const env = buildEnv(input);
for (const v of Object.values(env)) {
assert_1.strict.notStrictEqual(v, undefined);
assert_1.strict.strictEqual(typeof v, 'string');
}
});
});
// Fix: Package Resolution (cwd)
// Ensures that a child process spawned with cwd set to the deployment directory
// correctly resolves require() calls from that directory.
// We test this without metacall by spawning a vanilla node process.
describe('Fix: Package Resolution (cwd)', function () {
this.timeout(10000); // allow time for child process
const testAppDir = path_1.default.resolve(__dirname, '../../test/data/nodejs-base-app');
it('should resolve require() correctly when cwd is the deployment path', done => {
const proc = child_process_1.spawn(process.execPath, [
'-e',
'const m = require("./index.js"); process.exit(m && typeof m.isPalindrome === "function" ? 0 : 1)'
], {
cwd: testAppDir,
stdio: ['pipe', 'pipe', 'pipe']
});
proc.on('exit', code => {
assert_1.strict.strictEqual(code, 0, 'Child process should exit 0 when cwd is correct');
done();
});
proc.stderr?.on('data', (d) => {
const msg = d.toString();
if (msg.includes('Error:') &&
!msg.includes('ExperimentalWarning')) {
done(new Error(`stderr: ${msg}`));
}
});
});
it('should fail to resolve require() when cwd is wrong (proving the fix is necessary)', done => {
const wrongCwd = path_1.default.resolve(__dirname, '../../..');
const proc = child_process_1.spawn(process.execPath, ['-e', 'require("./index.js")'], {
cwd: wrongCwd,
stdio: ['pipe', 'pipe', 'pipe']
});
proc.on('exit', code => {
assert_1.strict.notStrictEqual(code, 0, 'Should fail with wrong cwd');
done();
});
});
});
// Fix: Asynchronous Function Execution
// Ensures that async functions (returning Promises) are properly awaited so
// the IPC message contains the resolved value, not a pending Promise object.
describe('Fix: Asynchronous Function Execution', function () {
it('should await and return the resolved value of an async function', async () => {
const asyncFn = (x) => Promise.resolve(x * 2);
const { result, error } = await safeInvoke(asyncFn, [21]);
assert_1.strict.strictEqual(error, undefined);
assert_1.strict.strictEqual(result, 42);
});
it('should NOT return a Promise object for async functions (old bug)', async () => {
const asyncFn = () => Promise.resolve('hello');
const { result } = await safeInvoke(asyncFn, []);
assert_1.strict.strictEqual(result, 'hello');
});
it('should handle sync functions transparently', async () => {
const syncFn = (a, b) => a + b;
const { result, error } = await safeInvoke(syncFn, [3, 4]);
assert_1.strict.strictEqual(error, undefined);
assert_1.strict.strictEqual(result, 7);
});
it('should catch and return errors from async functions instead of crashing', async () => {
const failingAsync = () => Promise.reject(new Error('intentional failure'));
const { result, error } = await safeInvoke(failingAsync, []);
assert_1.strict.ok(error !== undefined, 'error should be defined');
assert_1.strict.ok(error?.includes('intentional failure'), `error should include the message, got: ${error ?? ''}`);
assert_1.strict.strictEqual(result, undefined);
});
it('should catch and return errors from sync functions instead of crashing', async () => {
const failingSync = () => {
throw new TypeError('sync type error');
};
const { result, error } = await safeInvoke(failingSync, []);
assert_1.strict.ok(error !== undefined, 'error should be defined');
assert_1.strict.ok(error?.includes('TypeError'), `error should mention TypeError, got: ${error ?? ''}`);
assert_1.strict.strictEqual(result, undefined);
});
it('should handle complex async function with multiple awaits', async () => {
const complexFn = (input) => new Promise(resolve => setTimeout(() => resolve(input.split('').reverse().join('')), 10));
const { result, error } = await safeInvoke(complexFn, ['madam']);
assert_1.strict.strictEqual(error, undefined);
assert_1.strict.strictEqual(result, 'madam');
});
it('should correctly execute isPalindrome for sync functions (nodejs-base-app)', async () => {
const indexPath = path_1.default.resolve(__dirname, '../../test/data/nodejs-base-app/index.js');
const loaded = (await Promise.resolve().then(() => __importStar(require(indexPath))));
const isPalindrome = loaded.isPalindrome ??
loaded.default?.isPalindrome;
assert_1.strict.ok(typeof isPalindrome === 'function', 'isPalindrome must be a function');
const r1 = await safeInvoke(isPalindrome, ['madam']);
assert_1.strict.strictEqual(r1.result, true);
const r2 = await safeInvoke(isPalindrome, ['world']);
assert_1.strict.strictEqual(r2.result, false);
});
});