|
7 | 7 | */ |
8 | 8 |
|
9 | 9 | import { describe, it, expect, afterEach } from 'vitest'; |
| 10 | +import { mkdirSync, writeFileSync, rmSync } from 'node:fs'; |
| 11 | +import { join } from 'node:path'; |
| 12 | +import { tmpdir } from 'node:os'; |
10 | 13 | import { createNodeRuntime } from '../src/kernel-runtime.ts'; |
11 | 14 | import type { NodeRuntimeOptions } from '../src/kernel-runtime.ts'; |
12 | 15 | import { createKernel } from '@secure-exec/core'; |
@@ -766,4 +769,115 @@ describe('Node RuntimeDriver', () => { |
766 | 769 | expect(output).toContain('STEP:3'); |
767 | 770 | }); |
768 | 771 | }); |
| 772 | + |
| 773 | + describe('bare command resolution from node_modules/.bin', () => { |
| 774 | + let kernel: Kernel; |
| 775 | + let tmpDir: string; |
| 776 | + |
| 777 | + function createMockBinDir() { |
| 778 | + tmpDir = join(tmpdir(), `se-bin-test-${Date.now()}`); |
| 779 | + const binDir = join(tmpDir, 'node_modules', '.bin'); |
| 780 | + const pkgDir = join(tmpDir, 'node_modules', 'my-tool', 'dist'); |
| 781 | + mkdirSync(binDir, { recursive: true }); |
| 782 | + mkdirSync(pkgDir, { recursive: true }); |
| 783 | + // Create a real JS entry file |
| 784 | + writeFileSync( |
| 785 | + join(pkgDir, 'cli.js'), |
| 786 | + 'console.log("hello from bare command");', |
| 787 | + ); |
| 788 | + // Create a pnpm-style shell wrapper in .bin |
| 789 | + writeFileSync( |
| 790 | + join(binDir, 'my-tool'), |
| 791 | + [ |
| 792 | + '#!/bin/sh', |
| 793 | + 'basedir=$(dirname "$(echo "$0" | sed -e \'s,\\\\,/,g\')")', |
| 794 | + 'exec node "$basedir/../my-tool/dist/cli.js" "$@"', |
| 795 | + ].join('\n'), |
| 796 | + { mode: 0o755 }, |
| 797 | + ); |
| 798 | + return tmpDir; |
| 799 | + } |
| 800 | + |
| 801 | + afterEach(async () => { |
| 802 | + await kernel?.dispose(); |
| 803 | + if (tmpDir) { |
| 804 | + try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} |
| 805 | + } |
| 806 | + }); |
| 807 | + |
| 808 | + it('tryResolve returns true for bare command in node_modules/.bin', () => { |
| 809 | + createMockBinDir(); |
| 810 | + const driver = createNodeRuntime({ moduleAccessCwd: tmpDir }); |
| 811 | + expect(driver.tryResolve!('my-tool')).toBe(true); |
| 812 | + }); |
| 813 | + |
| 814 | + it('tryResolve returns false for unknown bare command', () => { |
| 815 | + createMockBinDir(); |
| 816 | + const driver = createNodeRuntime({ moduleAccessCwd: tmpDir }); |
| 817 | + expect(driver.tryResolve!('nonexistent-tool')).toBe(false); |
| 818 | + }); |
| 819 | + |
| 820 | + it('tryResolve returns false when moduleAccessCwd is not set', () => { |
| 821 | + const driver = createNodeRuntime(); |
| 822 | + expect(driver.tryResolve!('my-tool')).toBe(false); |
| 823 | + }); |
| 824 | + |
| 825 | + it('bare command executes the resolved JS entry point', async () => { |
| 826 | + createMockBinDir(); |
| 827 | + const vfs = new SimpleVFS(); |
| 828 | + kernel = createKernel({ filesystem: vfs as any }); |
| 829 | + await kernel.mount(createNodeRuntime({ moduleAccessCwd: tmpDir })); |
| 830 | + |
| 831 | + const chunks: Uint8Array[] = []; |
| 832 | + const proc = kernel.spawn('my-tool', [], { |
| 833 | + onStdout: (data) => chunks.push(data), |
| 834 | + }); |
| 835 | + const code = await proc.wait(); |
| 836 | + expect(code).toBe(0); |
| 837 | + |
| 838 | + const output = chunks.map(c => new TextDecoder().decode(c)).join(''); |
| 839 | + expect(output).toContain('hello from bare command'); |
| 840 | + }); |
| 841 | + |
| 842 | + it('bare command runs successfully even when args are passed', async () => { |
| 843 | + createMockBinDir(); |
| 844 | + const vfs = new SimpleVFS(); |
| 845 | + kernel = createKernel({ filesystem: vfs as any }); |
| 846 | + await kernel.mount(createNodeRuntime({ moduleAccessCwd: tmpDir })); |
| 847 | + |
| 848 | + // Spawn with extra args — should not crash |
| 849 | + const chunks: Uint8Array[] = []; |
| 850 | + const proc = kernel.spawn('my-tool', ['--flag', 'value'], { |
| 851 | + onStdout: (data) => chunks.push(data), |
| 852 | + }); |
| 853 | + const code = await proc.wait(); |
| 854 | + expect(code).toBe(0); |
| 855 | + |
| 856 | + const output = chunks.map(c => new TextDecoder().decode(c)).join(''); |
| 857 | + expect(output).toContain('hello from bare command'); |
| 858 | + }); |
| 859 | + |
| 860 | + it('handles direct node shebang scripts (npm/yarn symlink style)', async () => { |
| 861 | + createMockBinDir(); |
| 862 | + // Replace the shell wrapper with a direct node script |
| 863 | + writeFileSync( |
| 864 | + join(tmpDir, 'node_modules', '.bin', 'my-tool'), |
| 865 | + '#!/usr/bin/env node\nconsole.log("direct node script");', |
| 866 | + { mode: 0o755 }, |
| 867 | + ); |
| 868 | + const vfs = new SimpleVFS(); |
| 869 | + kernel = createKernel({ filesystem: vfs as any }); |
| 870 | + await kernel.mount(createNodeRuntime({ moduleAccessCwd: tmpDir })); |
| 871 | + |
| 872 | + const chunks: Uint8Array[] = []; |
| 873 | + const proc = kernel.spawn('my-tool', [], { |
| 874 | + onStdout: (data) => chunks.push(data), |
| 875 | + }); |
| 876 | + const code = await proc.wait(); |
| 877 | + expect(code).toBe(0); |
| 878 | + |
| 879 | + const output = chunks.map(c => new TextDecoder().decode(c)).join(''); |
| 880 | + expect(output).toContain('direct node script'); |
| 881 | + }); |
| 882 | + }); |
769 | 883 | }); |
0 commit comments