Files
SnowLuma/packages/bridge/tests/injector.test.ts
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

427 lines
16 KiB
TypeScript

import path from 'path';
import { fileURLToPath } from 'url';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const { existsSyncMock, listPidsMock } = vi.hoisted(() => ({
existsSyncMock: vi.fn<(target: string) => boolean>(() => false),
listPidsMock: vi.fn(() => new Set<number>()),
}));
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
...actual,
existsSync: (target: Parameters<typeof actual.existsSync>[0]) =>
existsSyncMock(String(target)),
};
});
vi.mock('../src/qq-hook-client', () => ({
listSnowlumaPipePidsSync: listPidsMock,
}));
const NATIVE_PATH_DIR = path.dirname(
fileURLToPath(new URL('../../common/src/native-path.ts', import.meta.url)),
);
const ORIGINAL_PLATFORM = process.platform;
const ORIGINAL_ARCH = process.arch;
const HANDLE = {
base: 0x7fff0000n,
entry: 0x7fff1234n,
exceptionTable: 0x7fff5678n,
size: 8192,
};
function setPlatform(platform: NodeJS.Platform, arch: string): void {
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
Object.defineProperty(process, 'arch', { value: arch, configurable: true });
}
function restorePlatform(): void {
Object.defineProperty(process, 'platform', { value: ORIGINAL_PLATFORM, configurable: true });
Object.defineProperty(process, 'arch', { value: ORIGINAL_ARCH, configurable: true });
}
function nativeSearchDirs(): string[] {
return [
path.resolve(NATIVE_PATH_DIR, 'native'),
path.resolve(NATIVE_PATH_DIR, '..', '..', 'runtime', 'native'),
];
}
function nativeCandidate(fileName: string, dirIndex: number): string {
return path.join(nativeSearchDirs()[dirIndex]!, fileName);
}
function allowExisting(paths: readonly string[]): void {
const allowed = new Set(paths);
existsSyncMock.mockImplementation((target) => allowed.has(target));
}
function fakeAddon(
overrides: {
getAllMainProcessAsync?: number[];
loadModuleManualAsync?: (pid: number, dylibPath: string) => Promise<typeof HANDLE>;
unloadModuleManualAsync?: (pid: number, handle: typeof HANDLE) => Promise<void>;
} = {},
) {
return {
getAllMainProcessAsync: vi.fn(async () => overrides.getAllMainProcessAsync ?? []),
loadModuleManualAsync: vi.fn(overrides.loadModuleManualAsync ?? (async () => HANDLE)),
unloadModuleManualAsync: vi.fn(
overrides.unloadModuleManualAsync ?? (async () => undefined),
),
};
}
function installDlopen(addon: object) {
return vi.spyOn(process, 'dlopen').mockImplementation((mod) => {
(mod as { exports: object }).exports = addon;
});
}
async function loadInjector() {
return import('../src/injector');
}
beforeEach(() => {
existsSyncMock.mockReset();
existsSyncMock.mockReturnValue(false);
listPidsMock.mockReset();
listPidsMock.mockReturnValue(new Set());
vi.resetModules();
});
afterEach(() => {
restorePlatform();
vi.restoreAllMocks();
});
describe('resolveHookNativePath', () => {
it('returns null after probing every search directory', async () => {
setPlatform('linux', 'x64');
const probed: string[] = [];
existsSyncMock.mockImplementation((target) => {
probed.push(target);
return false;
});
const { resolveHookNativePath } = await loadInjector();
expect(resolveHookNativePath('node')).toBeNull();
expect(probed).toEqual([
nativeCandidate('snowluma-linux-x64.node', 0),
nativeCandidate('snowluma-linux-x64.node', 1),
]);
});
it('returns the first existing candidate and stops scanning', async () => {
setPlatform('linux', 'x64');
const winner = nativeCandidate('snowluma-linux-x64.node', 0);
const later = nativeCandidate('snowluma-linux-x64.node', 1);
const probed: string[] = [];
existsSyncMock.mockImplementation((target) => {
probed.push(target);
return target === winner || target === later;
});
const { resolveHookNativePath } = await loadInjector();
expect(resolveHookNativePath('node')).toBe(winner);
expect(probed).toEqual([winner]);
});
it('skips missing earlier directories for the injectable .so', async () => {
setPlatform('linux', 'arm64');
const winner = nativeCandidate('snowluma-linux-arm64.so', 1);
allowExisting([winner]);
const { resolveHookNativePath } = await loadInjector();
expect(resolveHookNativePath('so')).toBe(winner);
});
it('resolves snowluma-win32-x64.dll on win32 x64', async () => {
setPlatform('win32', 'x64');
const winner = nativeCandidate('snowluma-win32-x64.dll', 1);
allowExisting([winner]);
const { resolveHookNativePath } = await loadInjector();
expect(resolveHookNativePath('dll')).toBe(winner);
});
it('uses the generic platform-arch name on win32 arm64', async () => {
setPlatform('win32', 'arm64');
const winner = nativeCandidate('snowluma-win32-arm64.node', 1);
allowExisting([winner]);
const { resolveHookNativePath } = await loadInjector();
expect(resolveHookNativePath('node')).toBe(winner);
});
it('does not load an unrelated native addon from the working directory', async () => {
setPlatform('darwin', 'arm64');
vi.spyOn(process, 'cwd').mockReturnValue('/tmp/sl-injector-cwd');
const winner = path.join(
'/tmp/sl-injector-cwd',
'dist',
'native',
'snowluma-darwin-arm64.node',
);
allowExisting([winner]);
const { resolveHookNativePath } = await loadInjector();
expect(resolveHookNativePath('node')).toBeNull();
});
});
describe('getNativeHookAddon / getNativeHookLoadError', () => {
it('starts with a null load error before any probe', async () => {
const { getNativeHookLoadError } = await loadInjector();
expect(getNativeHookLoadError()).toBeNull();
});
it('returns null and records a missing-binary error', async () => {
setPlatform('linux', 'x64');
const dlopen = vi.spyOn(process, 'dlopen');
const { getNativeHookAddon, getNativeHookLoadError } = await loadInjector();
expect(getNativeHookAddon()).toBeNull();
expect(getNativeHookLoadError()).toBe('No hook native addon found for linux-x64');
expect(dlopen).not.toHaveBeenCalled();
});
it('dlopens the resolved .node once and caches the addon', async () => {
setPlatform('linux', 'x64');
const nodePath = nativeCandidate('snowluma-linux-x64.node', 0);
allowExisting([nodePath]);
const addon = fakeAddon();
const dlopen = installDlopen(addon);
const { getNativeHookAddon, getNativeHookLoadError } = await loadInjector();
const first = getNativeHookAddon();
const second = getNativeHookAddon();
expect(first).toBe(addon);
expect(second).toBe(addon);
expect(getNativeHookLoadError()).toBeNull();
expect(dlopen).toHaveBeenCalledTimes(1);
expect(dlopen.mock.calls[0]![1]).toBe(nodePath);
});
it('stores Error.message when process.dlopen throws', async () => {
setPlatform('darwin', 'arm64');
allowExisting([nativeCandidate('snowluma-darwin-arm64.node', 0)]);
vi.spyOn(process, 'dlopen').mockImplementation(() => {
throw new Error('not a valid Node addon');
});
const { getNativeHookAddon, getNativeHookLoadError } = await loadInjector();
expect(getNativeHookAddon()).toBeNull();
expect(getNativeHookLoadError()).toBe('not a valid Node addon');
});
it('stringifies a non-Error dlopen failure', async () => {
setPlatform('linux', 'x64');
allowExisting([nativeCandidate('snowluma-linux-x64.node', 0)]);
vi.spyOn(process, 'dlopen').mockImplementation(() => {
throw 42;
});
const { getNativeHookAddon, getNativeHookLoadError } = await loadInjector();
expect(getNativeHookAddon()).toBeNull();
expect(getNativeHookLoadError()).toBe('42');
});
it('retries after a failed load when a binary later appears', async () => {
setPlatform('linux', 'x64');
const nodePath = nativeCandidate('snowluma-linux-x64.node', 1);
const addon = fakeAddon();
installDlopen(addon);
const { getNativeHookAddon, getNativeHookLoadError } = await loadInjector();
expect(getNativeHookAddon()).toBeNull();
expect(getNativeHookLoadError()).toBe('No hook native addon found for linux-x64');
allowExisting([nodePath]);
expect(getNativeHookAddon()).toBe(addon);
expect(getNativeHookLoadError()).toBeNull();
});
});
describe('listHookProcesses', () => {
it('rejects an obsolete addon without calling its synchronous enumerator', async () => {
setPlatform('win32', 'x64');
allowExisting([nativeCandidate('snowluma-win32-x64.node', 0)]);
const getAllMainProcess = vi.fn(() => [9]);
installDlopen({ getAllMainProcess });
const { listHookProcesses } = await loadInjector();
await expect(listHookProcesses()).rejects.toThrow('getAllMainProcessAsync');
expect(getAllMainProcess).not.toHaveBeenCalled();
});
it('on darwin maps sorted pipe pids without opening the native addon', async () => {
setPlatform('darwin', 'arm64');
listPidsMock.mockReturnValue(new Set([42, 7, 15]));
allowExisting([nativeCandidate('snowluma-darwin-arm64.node', 0)]);
const dlopen = installDlopen(fakeAddon({ getAllMainProcessAsync: [99] }));
const { listHookProcesses } = await loadInjector();
expect(await listHookProcesses()).toEqual([
{ pid: 7, name: 'qq', path: '' },
{ pid: 15, name: 'qq', path: '' },
{ pid: 42, name: 'qq', path: '' },
]);
expect(listPidsMock).toHaveBeenCalledTimes(1);
expect(dlopen).not.toHaveBeenCalled();
});
it('on darwin returns an empty list when no control sockets exist', async () => {
setPlatform('darwin', 'arm64');
listPidsMock.mockReturnValue(new Set());
const { listHookProcesses } = await loadInjector();
expect(await listHookProcesses()).toEqual([]);
});
it('reports a missing addon instead of an empty process set', async () => {
setPlatform('linux', 'x64');
const { listHookProcesses } = await loadInjector();
await expect(listHookProcesses()).rejects.toThrow(
'No hook native addon found for linux-x64',
);
});
it('on linux dedupes, drops non-positive/non-integer pids, and sorts', async () => {
setPlatform('linux', 'x64');
const nodePath = nativeCandidate('snowluma-linux-x64.node', 0);
allowExisting([nodePath]);
installDlopen(
fakeAddon({
getAllMainProcessAsync: [8, 0, 8, -1, 3.5, 2, Number.NaN, 2],
}),
);
const { listHookProcesses } = await loadInjector();
expect(await listHookProcesses()).toEqual([
{ pid: 2, name: 'qq', path: '' },
{ pid: 8, name: 'qq', path: '' },
]);
});
it('on win32 names processes QQ.exe', async () => {
setPlatform('win32', 'x64');
allowExisting([nativeCandidate('snowluma-win32-x64.node', 0)]);
installDlopen(fakeAddon({ getAllMainProcessAsync: [9] }));
const { listHookProcesses } = await loadInjector();
expect(await listHookProcesses()).toEqual([{ pid: 9, name: 'QQ.exe', path: '' }]);
});
});
describe('injectHookProcess', () => {
it('throws the missing-addon load error', async () => {
setPlatform('linux', 'x64');
const { injectHookProcess } = await loadInjector();
await expect(injectHookProcess(4242)).rejects.toThrow(
'No hook native addon found for linux-x64',
);
});
it('throws when the addon exists but the injectable is missing', async () => {
setPlatform('linux', 'x64');
allowExisting([nativeCandidate('snowluma-linux-x64.node', 0)]);
installDlopen(fakeAddon());
const { injectHookProcess } = await loadInjector();
await expect(injectHookProcess(4242)).rejects.toThrow('No hook so found for linux-x64');
});
it('awaits native asynchronous injection', async () => {
setPlatform('linux', 'x64');
const nodePath = nativeCandidate('snowluma-linux-x64.node', 0);
const soPath = nativeCandidate('snowluma-linux-x64.so', 0);
allowExisting([nodePath, soPath]);
const addon = fakeAddon({
loadModuleManualAsync: () => Promise.resolve(HANDLE),
});
installDlopen(addon);
const { injectHookProcess } = await loadInjector();
await expect(injectHookProcess(4242)).resolves.toEqual({
method: 'loadModuleManual',
handle: HANDLE,
});
expect(addon.loadModuleManualAsync).toHaveBeenCalledTimes(1);
expect(addon.loadModuleManualAsync).toHaveBeenCalledWith(4242, soPath);
});
it('injects snowluma-win32-x64.dll on win32 x64', async () => {
setPlatform('win32', 'x64');
const nodePath = nativeCandidate('snowluma-win32-x64.node', 0);
const dllPath = nativeCandidate('snowluma-win32-x64.dll', 0);
allowExisting([nodePath, dllPath]);
const addon = fakeAddon({ loadModuleManualAsync: async () => HANDLE });
installDlopen(addon);
const { injectHookProcess } = await loadInjector();
await expect(injectHookProcess(5150)).resolves.toEqual({
method: 'loadModuleManual',
handle: HANDLE,
});
expect(addon.loadModuleManualAsync).toHaveBeenCalledWith(5150, dllPath);
});
it('propagates a rejected loadModuleManual', async () => {
setPlatform('linux', 'x64');
allowExisting([
nativeCandidate('snowluma-linux-x64.node', 0),
nativeCandidate('snowluma-linux-x64.so', 0),
]);
installDlopen(
fakeAddon({
loadModuleManualAsync: () => Promise.reject(new Error('map failed')),
}),
);
const { injectHookProcess } = await loadInjector();
await expect(injectHookProcess(7)).rejects.toThrow('map failed');
});
});
describe('unloadHookProcess', () => {
it('propagates native unload rejection', async () => {
setPlatform('linux', 'x64');
allowExisting([nativeCandidate('snowluma-linux-x64.node', 0)]);
installDlopen(
fakeAddon({
unloadModuleManualAsync: async () => {
throw new Error('unload failed');
},
}),
);
const { unloadHookProcess } = await loadInjector();
await expect(unloadHookProcess(4242, HANDLE)).rejects.toThrow('unload failed');
});
it('throws the missing-addon load error', async () => {
setPlatform('linux', 'x64');
const { unloadHookProcess } = await loadInjector();
await expect(unloadHookProcess(4242, HANDLE)).rejects.toThrow(
'No hook native addon found for linux-x64',
);
});
it('forwards pid and handle to unloadModuleManual', async () => {
setPlatform('linux', 'x64');
allowExisting([nativeCandidate('snowluma-linux-x64.node', 0)]);
const addon = fakeAddon();
installDlopen(addon);
const { unloadHookProcess } = await loadInjector();
await expect(unloadHookProcess(4242, HANDLE)).resolves.toBeUndefined();
expect(addon.unloadModuleManualAsync).toHaveBeenCalledTimes(1);
expect(addon.unloadModuleManualAsync).toHaveBeenCalledWith(4242, HANDLE);
});
});