Files
SubMiner/src/main/runtime/login-shell-path.test.ts
T
sudacode 52fc1b63e9 fix(launcher): validate login shell paths before execution
- Fall back to absolute default shells when configured values are relative
- Reject invalid shells before spawning them
2026-09-24 01:31:27 -07:00

90 lines
3.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import os from 'node:os';
import { applyLoginShellPath, mergePathValues, readLoginShellPath } from './login-shell-path';
const wrap = (value: string) =>
`motd noise\n__SUBMINER_LOGIN_PATH__${value}\n__SUBMINER_LOGIN_PATH__`;
test('readLoginShellPath extracts PATH between markers, ignoring rc-file output', async () => {
let invoked: { shell: string; args: string[] } | null = null;
const value = await readLoginShellPath({
env: { SHELL: '/bin/zsh' },
runShell: async (shell, args) => {
invoked = { shell, args };
return wrap('/Users/me/.local/bin:/usr/bin');
},
});
assert.equal(value, '/Users/me/.local/bin:/usr/bin');
assert.equal(invoked!.shell, '/bin/zsh');
assert.equal(invoked!.args[0], '-ilc');
});
test('readLoginShellPath returns null when markers are missing', async () => {
const value = await readLoginShellPath({ env: {}, shell: '/bin/sh', runShell: async () => '' });
assert.equal(value, null);
});
test('readLoginShellPath rejects relative explicit shells before execution', async () => {
for (const shell of ['zsh', './zsh', '']) {
await assert.rejects(
readLoginShellPath({
env: { SHELL: '/bin/zsh' },
shell,
runShell: async () => assert.fail('Invalid shell must not execute'),
}),
/Login shell must be an absolute path/,
);
}
});
test('readLoginShellPath falls back through absolute default shells', async () => {
const originalUserInfo = os.userInfo;
try {
for (const shell of ['/bin/bash', 'bash', '', null]) {
Object.defineProperty(os, 'userInfo', {
value: () => ({ shell, username: 'test', uid: 1000, gid: 1000, homedir: '/Users/test' }),
});
await readLoginShellPath({
env: { SHELL: './zsh' },
runShell: async (selectedShell) => {
assert.equal(selectedShell, shell === '/bin/bash' ? '/bin/bash' : '/bin/zsh');
return wrap('/usr/bin');
},
});
}
Object.defineProperty(os, 'userInfo', {
value: () => {
throw new Error('User lookup failed');
},
});
await readLoginShellPath({
env: { SHELL: 'zsh' },
runShell: async (shell) => {
assert.equal(shell, '/bin/zsh');
return wrap('/usr/bin');
},
});
} finally {
os.userInfo = originalUserInfo;
}
});
test('mergePathValues puts login entries first and keeps process-only entries', () => {
assert.equal(
mergePathValues('/Users/me/.local/bin:/usr/bin:/bin', '/usr/bin:/bin:/usr/sbin:/sbin'),
'/Users/me/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin',
);
});
test('applyLoginShellPath updates env.PATH so launcher detection sees shell dirs', async () => {
const env: NodeJS.ProcessEnv = { PATH: '/usr/bin:/bin' };
const applied = await applyLoginShellPath({
env,
shell: '/bin/zsh',
runShell: async () => wrap('/Users/me/.local/bin:/usr/bin'),
});
assert.equal(applied, true);
assert.equal(env.PATH, '/Users/me/.local/bin:/usr/bin:/bin');
});