|
| 1 | +const assert = require("node:assert/strict"); |
| 2 | +const { mkdtempSync, readFileSync, rmSync, writeFileSync } = require("node:fs"); |
| 3 | +const { tmpdir } = require("node:os"); |
| 4 | +const { join } = require("node:path"); |
| 5 | +const test = require("node:test"); |
| 6 | +const vm = require("node:vm"); |
| 7 | +const ts = require("typescript"); |
| 8 | + |
| 9 | +// Exercise runRbw with simulated GUI environment and Vicinae preferences. |
| 10 | +// The fixture rbw launches a sibling rbw-agent by name, like the real executable. |
| 11 | +const source = ts.transpileModule( |
| 12 | + readFileSync(join(__dirname, "../src/rbw.ts"), "utf8"), |
| 13 | + { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }, |
| 14 | +).outputText; |
| 15 | + |
| 16 | +function loadRbw(rbwPath, env) { |
| 17 | + const exports = {}; |
| 18 | + vm.runInNewContext(source, { |
| 19 | + exports, |
| 20 | + process: { env }, |
| 21 | + require(name) { |
| 22 | + if (name === "@vicinae/api") { |
| 23 | + return { getPreferenceValues: () => ({ rbwPath }) }; |
| 24 | + } |
| 25 | + if (name === "node:child_process") { |
| 26 | + // A real child inherits the caller's environment; use the simulated |
| 27 | + // GUI environment unless runRbw explicitly supplies one. |
| 28 | + function execFile(file, args, options, callback) { |
| 29 | + return require(name).execFile(file, args, { env, ...options }, callback); |
| 30 | + } |
| 31 | + const { promisify } = require("node:util"); |
| 32 | + execFile[promisify.custom] = (file, args, options) => |
| 33 | + promisify(require(name).execFile)(file, args, { env, ...options }); |
| 34 | + return { execFile }; |
| 35 | + } |
| 36 | + return require(name); |
| 37 | + }, |
| 38 | + }); |
| 39 | + return exports; |
| 40 | +} |
| 41 | + |
| 42 | +function fixture(t) { |
| 43 | + const dir = mkdtempSync(join(tmpdir(), "rbw path ")); |
| 44 | + t.after(() => rmSync(dir, { recursive: true, force: true })); |
| 45 | + writeFileSync(join(dir, "rbw"), '#!/bin/sh\nexec rbw-agent "$@"\n', { mode: 0o755 }); |
| 46 | + writeFileSync( |
| 47 | + join(dir, "rbw-agent"), |
| 48 | + '#!/bin/sh\nprintf "%s\\n" "$1" "$TEST_MARKER"\n', |
| 49 | + { mode: 0o755 }, |
| 50 | + ); |
| 51 | + return dir; |
| 52 | +} |
| 53 | + |
| 54 | +const guiPath = "/usr/bin:/bin:/usr/sbin:/sbin"; |
| 55 | + |
| 56 | +test("absolute rbw path finds its sibling agent with a macOS GUI PATH", async (t) => { |
| 57 | + const dir = fixture(t); |
| 58 | + const env = { PATH: guiPath, TEST_MARKER: "preserved" }; |
| 59 | + const { runRbw } = loadRbw(join(dir, "rbw"), env); |
| 60 | + assert.equal(await runRbw(["unlock"]), "unlock\npreserved\n"); |
| 61 | + assert.equal(env.PATH, guiPath, "do not mutate the extension runtime environment"); |
| 62 | +}); |
| 63 | + |
| 64 | +test("bare rbw command retains the existing PATH", async (t) => { |
| 65 | + const dir = fixture(t); |
| 66 | + const { runRbw } = loadRbw("rbw", { PATH: `${dir}:${guiPath}`, TEST_MARKER: "bare" }); |
| 67 | + assert.equal(await runRbw(["unlock"]), "unlock\nbare\n"); |
| 68 | +}); |
| 69 | + |
| 70 | +test("absolute rbw path finds its agent even when PATH is unset", async (t) => { |
| 71 | + const dir = fixture(t); |
| 72 | + const { runRbw } = loadRbw(join(dir, "rbw"), { TEST_MARKER: "no-path" }); |
| 73 | + assert.equal(await runRbw(["unlock"]), "unlock\nno-path\n"); |
| 74 | +}); |
| 75 | + |
| 76 | +test("missing rbw still reports the installation error", async (t) => { |
| 77 | + const dir = fixture(t); |
| 78 | + const { runRbw, RbwNotInstalledError } = loadRbw(join(dir, "missing"), { PATH: guiPath }); |
| 79 | + await assert.rejects(runRbw(["unlock"]), RbwNotInstalledError); |
| 80 | +}); |
0 commit comments