Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
1611 lines
63 KiB
JavaScript
1611 lines
63 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const {
|
|
SUPPRESSION_VALUE,
|
|
EXPLORER_CONTEXT_MENU_SCHEMA_VERSION,
|
|
applyInitialExplorerContextMenuPreference,
|
|
buildExplorerContextMenuCommand,
|
|
installExplorerContextMenu,
|
|
isExplorerContextMenuRegistered,
|
|
removeExplorerContextMenu,
|
|
resolveExplorerContextMenuEnabled,
|
|
resolveExplorerContextMenuExecutablePath,
|
|
resolveExplorerContextMenuLaunchSpec,
|
|
updateExplorerContextMenuEnabledPreference,
|
|
} = require("./explorerContextMenu.cjs");
|
|
|
|
test("buildExplorerContextMenuCommand puts path args after -- for Electron", () => {
|
|
assert.equal(
|
|
buildExplorerContextMenuCommand("C:\\Program Files\\Netcatty\\Netcatty.exe", "%1"),
|
|
'"C:\\Program Files\\Netcatty\\Netcatty.exe" -- --open-terminal-path="%1."',
|
|
);
|
|
assert.equal(
|
|
buildExplorerContextMenuCommand("C:\\Netcatty\\Netcatty.exe", "%V"),
|
|
'"C:\\Netcatty\\Netcatty.exe" -- --open-terminal-path="%V."',
|
|
);
|
|
assert.equal(
|
|
buildExplorerContextMenuCommand(
|
|
"C:\\dev\\node_modules\\electron\\dist\\electron.exe",
|
|
"%1",
|
|
{ appArgs: ["C:\\dev\\netcatty"] },
|
|
),
|
|
'"C:\\dev\\node_modules\\electron\\dist\\electron.exe" "C:\\dev\\netcatty" -- --open-terminal-path="%1."',
|
|
);
|
|
});
|
|
|
|
test("isExplorerContextMenuRegistered requires both folder and background verbs", () => {
|
|
const queries = [];
|
|
const commandByKey = {
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command":
|
|
'"C:\\\\Apps\\\\Netcatty.exe" -- --open-terminal-path="%1."',
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command":
|
|
'"C:\\\\Apps\\\\Netcatty.exe" -- --open-terminal-path="%V."',
|
|
};
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
queries.push(args.slice());
|
|
if (args.includes("/v") && !args.includes("/ve")) {
|
|
return { status: 1, stdout: "", stderr: "value not found" };
|
|
}
|
|
if (args.includes("/ve")) {
|
|
const command = commandByKey[args[1]];
|
|
if (command) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` (Default) REG_SZ ${command}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "value not found" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "not found" };
|
|
};
|
|
|
|
assert.equal(
|
|
isExplorerContextMenuRegistered({ platform: "win32", spawnSyncImpl, logWarn: () => {} }),
|
|
true,
|
|
);
|
|
assert.ok(queries.some((args) => args[0] === "query"));
|
|
|
|
// Missing background verb => not fully registered.
|
|
present.delete("HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty");
|
|
present.delete("HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command");
|
|
delete commandByKey["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command"];
|
|
assert.equal(
|
|
isExplorerContextMenuRegistered({ platform: "win32", spawnSyncImpl, logWarn: () => {} }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("isExplorerContextMenuRegistered ignores bare verb keys without command", () => {
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
// No ProgrammaticAccessOnly and no command default value.
|
|
return { status: 1, stdout: "", stderr: "value missing" };
|
|
}
|
|
if (args[1] === "HKCU\\Software\\Classes\\Directory\\shell\\Netcatty") {
|
|
return { status: 0, stdout: "ok", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "not found" };
|
|
};
|
|
|
|
assert.equal(
|
|
isExplorerContextMenuRegistered({ platform: "win32", spawnSyncImpl, logWarn: () => {} }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("isExplorerContextMenuRegistered is false when user suppressed HKLM menu", () => {
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] !== "query") return { status: 1, stdout: "", stderr: "unexpected" };
|
|
const key = args[1];
|
|
if (args.includes("/v")) {
|
|
if (
|
|
args.includes(SUPPRESSION_VALUE)
|
|
&& (
|
|
key === "HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"
|
|
|| key === "HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty"
|
|
)
|
|
) {
|
|
return { status: 0, stdout: "ok", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "value missing" };
|
|
}
|
|
if (
|
|
key === "HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"
|
|
|| key === "HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty"
|
|
|| key === "HKLM\\Software\\Classes\\Directory\\shell\\Netcatty"
|
|
|| key === "HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty"
|
|
|| key.endsWith("\\command")
|
|
) {
|
|
return { status: 0, stdout: "ok", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "missing" };
|
|
};
|
|
|
|
assert.equal(
|
|
isExplorerContextMenuRegistered({ platform: "win32", spawnSyncImpl, logWarn: () => {} }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("isExplorerContextMenuRegistered stays true when only one verb is suppressed", () => {
|
|
const commandByKey = {
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command":
|
|
'"C:\\\\Apps\\\\Netcatty.exe" -- --open-terminal-path="%1."',
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command":
|
|
'"C:\\\\Apps\\\\Netcatty.exe" -- --open-terminal-path="%V."',
|
|
};
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] !== "query") return { status: 1, stdout: "", stderr: "unexpected" };
|
|
if (args.includes("/ve")) {
|
|
const command = commandByKey[args[1]];
|
|
return command
|
|
? { status: 0, stdout: ` (Default) REG_SZ ${command}\n`, stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
if (args.includes("/v")) {
|
|
// Only the folder verb is suppressed; background is still visible via HKLM.
|
|
if (
|
|
args.includes(SUPPRESSION_VALUE)
|
|
&& args[1] === "HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"
|
|
) {
|
|
return { status: 0, stdout: "ok", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "value missing" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
};
|
|
|
|
assert.equal(
|
|
isExplorerContextMenuRegistered({ platform: "win32", spawnSyncImpl, logWarn: () => {} }),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("removeExplorerContextMenu deletes per-user folder and background keys", () => {
|
|
const deleted = [];
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v")) {
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
deleted.push(args[1]);
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = removeExplorerContextMenu({
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, false);
|
|
assert.ok(deleted.some((key) => key.endsWith("Directory\\shell\\Netcatty")));
|
|
assert.ok(deleted.some((key) => key.includes("Directory\\Background\\shell\\Netcatty")));
|
|
assert.ok(deleted.every((key) => key.startsWith("HKCU\\")));
|
|
});
|
|
|
|
test("removeExplorerContextMenu never deletes HKLM; suppresses per-user instead", () => {
|
|
const present = new Set([
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
]);
|
|
const deleted = [];
|
|
const suppressed = new Set();
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v")) {
|
|
if (args.includes(SUPPRESSION_VALUE) && suppressed.has(args[1])) {
|
|
return { status: 0, stdout: "ok", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1]) || suppressed.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
// Even if elevated, the toggle must not wipe machine-wide verbs.
|
|
if (String(args[1]).startsWith("HKLM\\")) {
|
|
throw new Error("must not delete HKLM from per-user toggle");
|
|
}
|
|
deleted.push(args[1]);
|
|
present.delete(args[1]);
|
|
suppressed.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const key = args[1];
|
|
const valueIdx = args.indexOf("/v");
|
|
if (valueIdx >= 0 && args[valueIdx + 1] === SUPPRESSION_VALUE) {
|
|
suppressed.add(key);
|
|
present.add(key);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected add" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = removeExplorerContextMenu({
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, false);
|
|
assert.ok(suppressed.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"));
|
|
assert.ok(suppressed.has("HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty"));
|
|
// Machine keys remain for other users / uninstaller; working HKCU verbs are
|
|
// hidden via suppression rather than deleted first.
|
|
assert.equal(deleted.length, 0);
|
|
assert.ok(present.has("HKLM\\Software\\Classes\\Directory\\shell\\Netcatty"));
|
|
assert.ok(present.has("HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty"));
|
|
assert.ok(deleted.every((key) => key.startsWith("HKCU\\")));
|
|
});
|
|
|
|
test("writeUserSuppression rollback preserves pre-existing portable HKCU commands", () => {
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%1."'],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%V."'],
|
|
]);
|
|
let folderSuppressWrites = 0;
|
|
const deletedKeys = [];
|
|
const deletedValues = [];
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const key = args[1];
|
|
const valueIdx = args.indexOf("/v");
|
|
if (valueIdx >= 0 && args[valueIdx + 1] === SUPPRESSION_VALUE) {
|
|
// First suppression succeeds; second fails so rollback must run.
|
|
if (key.includes("Directory\\shell\\Netcatty") && !key.includes("Background")) {
|
|
folderSuppressWrites += 1;
|
|
values.set(`${key}::${SUPPRESSION_VALUE}`, "");
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected add" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
if (args.includes("/v")) {
|
|
const key = args[1];
|
|
const valueName = args[args.indexOf("/v") + 1];
|
|
deletedValues.push(`${key}::${valueName}`);
|
|
values.delete(`${key}::${valueName}`);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
deletedKeys.push(args[1]);
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = removeExplorerContextMenu({
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.equal(folderSuppressWrites, 1);
|
|
assert.equal(deletedKeys.length, 0);
|
|
assert.ok(deletedValues.some((entry) => entry.includes(SUPPRESSION_VALUE)));
|
|
assert.equal(
|
|
present.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
values.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::"),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("removeExplorerContextMenu keeps portable HKCU when machine suppression fails", () => {
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%1."'],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%V."'],
|
|
]);
|
|
const deleted = [];
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
deleted.push(args[1]);
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "add") {
|
|
// Suppression write fails (e.g. partial ACL / disk error).
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = removeExplorerContextMenu({
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.equal(deleted.length, 0);
|
|
assert.equal(present.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"), true);
|
|
assert.equal(result.enabled, true);
|
|
});
|
|
|
|
test("installExplorerContextMenu writes HKCU shell command entries", () => {
|
|
const writes = [];
|
|
const present = new Set();
|
|
const values = new Map();
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
writes.push(args.slice());
|
|
present.add(args[1]);
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${args[1]}::${valueName}`, args[dataIdx + 1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
present.delete(args[1]);
|
|
for (const key of [...values.keys()]) {
|
|
if (key.startsWith(`${args[1]}::`)) values.delete(key);
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: "C:\\Apps\\Netcatty.exe",
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.ok(writes.some((args) => args.includes("MUIVerb") && args.includes("Open in Netcatty")));
|
|
assert.ok(writes.some((args) =>
|
|
args.some((part) => String(part).includes('--open-terminal-path="%1."'))
|
|
));
|
|
assert.ok(writes.some((args) =>
|
|
args.some((part) => String(part).includes('--open-terminal-path="%V."'))
|
|
));
|
|
// Per-user only when no machine registration exists.
|
|
assert.ok(writes.every((args) => String(args[1]).startsWith("HKCU\\")));
|
|
});
|
|
|
|
test("installExplorerContextMenu does not duplicate HKLM verbs into HKCU", () => {
|
|
const writes = [];
|
|
const present = new Set([
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
]);
|
|
const values = new Map();
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
writes.push(args.slice());
|
|
present.add(args[1]);
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${args[1]}::${valueName}`, args[dataIdx + 1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
present.delete(args[1]);
|
|
for (const key of [...values.keys()]) {
|
|
if (key.startsWith(`${args[1]}::`)) values.delete(key);
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: "C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.ok(writes.length > 0);
|
|
assert.ok(writes.every((args) => String(args[1]).startsWith("HKLM\\")));
|
|
assert.ok(!writes.some((args) => String(args[1]).startsWith("HKCU\\")));
|
|
});
|
|
|
|
test("installExplorerContextMenu falls back to HKCU when unelevated HKLM verbs are stale", () => {
|
|
const exe = "C:\\Program Files\\Netcatty\\Netcatty.exe";
|
|
const present = new Set([
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", "C:\\Old\\Netcatty.exe,0"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Old\\Netcatty.exe" --open-terminal-path "%1"'],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", "C:\\Old\\Netcatty.exe,0"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", '"C:\\Old\\Netcatty.exe" --open-terminal-path "%V"'],
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const key = args[1];
|
|
// Unelevated process cannot rewrite HKLM; HKCU fallback must succeed.
|
|
if (String(key).startsWith("HKLM\\")) {
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
present.add(key);
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${key}::${valueName}`, args[dataIdx + 1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(
|
|
values.get("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::"),
|
|
buildExplorerContextMenuCommand(exe, "%1"),
|
|
);
|
|
});
|
|
|
|
test("installExplorerContextMenu skips reg writes when shell verbs are already current", () => {
|
|
const exe = "C:\\Program Files\\Netcatty\\Netcatty.exe";
|
|
const folderCmd = buildExplorerContextMenuCommand(exe, "%1");
|
|
const backgroundCmd = buildExplorerContextMenuCommand(exe, "%V");
|
|
const present = new Set([
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", folderCmd],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", backgroundCmd],
|
|
]);
|
|
const writes = [];
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
writes.push(args.slice());
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(writes.length, 0);
|
|
});
|
|
|
|
test("resolveExplorerContextMenuEnabled prefers saved preference over registry", () => {
|
|
const fsModule = {
|
|
existsSync: () => true,
|
|
readFileSync: () => JSON.stringify({ enabled: false }),
|
|
};
|
|
const app = { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" };
|
|
const spawnSyncImpl = () => ({ status: 0, stdout: "present", stderr: "" });
|
|
|
|
const resolved = resolveExplorerContextMenuEnabled({
|
|
app,
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.deepEqual(resolved, { enabled: false, supported: true });
|
|
});
|
|
|
|
test("updateExplorerContextMenuEnabledPreference rolls back apply on write failure", () => {
|
|
const applied = [];
|
|
const result = updateExplorerContextMenuEnabledPreference({
|
|
currentEnabled: true,
|
|
enabled: false,
|
|
applyPreference: (next) => {
|
|
applied.push(next);
|
|
return { success: true, enabled: next };
|
|
},
|
|
writePreference: () => false,
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.deepEqual(applied, [false, true]);
|
|
assert.equal(result.enabled, true);
|
|
});
|
|
|
|
test("updateExplorerContextMenuEnabledPreference rolls back registry on apply failure", () => {
|
|
const applied = [];
|
|
const result = updateExplorerContextMenuEnabledPreference({
|
|
currentEnabled: true,
|
|
enabled: false,
|
|
applyPreference: (next) => {
|
|
applied.push(next);
|
|
if (next === false) return { success: false, enabled: true, supported: true };
|
|
return { success: true, enabled: true, supported: true };
|
|
},
|
|
writePreference: () => true,
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.deepEqual(applied, [false, true]);
|
|
assert.equal(result.enabled, true);
|
|
});
|
|
|
|
test("installExplorerContextMenu clears stale portable HKCU verbs when HKLM exists", () => {
|
|
const exe = "C:\\Program Files\\Netcatty\\Netcatty.exe";
|
|
const folderCmd = buildExplorerContextMenuCommand(exe, "%1");
|
|
const backgroundCmd = buildExplorerContextMenuCommand(exe, "%V");
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", "C:\\Portable\\Netcatty.exe,0"],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%1."'],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", "C:\\Portable\\Netcatty.exe,0"],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%V."'],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", folderCmd],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", backgroundCmd],
|
|
]);
|
|
const deleted = [];
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
deleted.push(args[1]);
|
|
present.delete(args[1]);
|
|
for (const key of [...values.keys()]) {
|
|
if (key.startsWith(`${args[1]}::`)) values.delete(key);
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "add") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.ok(deleted.some((key) => key.includes("HKCU\\") && key.includes("Directory\\shell\\Netcatty")));
|
|
assert.ok(deleted.some((key) => key.includes("HKCU\\") && key.includes("Directory\\Background\\shell\\Netcatty")));
|
|
assert.equal(present.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"), false);
|
|
});
|
|
|
|
test("installExplorerContextMenu unsuppresses portable HKCU when HKLM refresh fails", () => {
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
[`HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::${SUPPRESSION_VALUE}`, ""],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%1."'],
|
|
[`HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::${SUPPRESSION_VALUE}`, ""],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%V."'],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Old\\Netcatty.exe" --open-terminal-path "%1"'],
|
|
]);
|
|
const deletedValues = [];
|
|
const deletedKeys = [];
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
if (args.includes("/v")) {
|
|
const key = args[1];
|
|
const valueName = args[args.indexOf("/v") + 1];
|
|
deletedValues.push(`${key}::${valueName}`);
|
|
values.delete(`${key}::${valueName}`);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
deletedKeys.push(args[1]);
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: "C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.equal(deletedKeys.length, 0);
|
|
assert.ok(deletedValues.every((entry) => entry.includes(SUPPRESSION_VALUE)));
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(
|
|
values.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::"),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("installExplorerContextMenu keeps working HKCU when HKLM refresh fails", () => {
|
|
const portableCmd = '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%1."';
|
|
const portableBg = '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%V."';
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", "C:\\Portable\\Netcatty.exe,0"],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", portableCmd],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", "C:\\Portable\\Netcatty.exe,0"],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", portableBg],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", "C:\\Old\\Netcatty.exe,0"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Old\\Netcatty.exe" --open-terminal-path "%1"'],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", "C:\\Old\\Netcatty.exe,0"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", '"C:\\Old\\Netcatty.exe" --open-terminal-path "%V"'],
|
|
]);
|
|
const deleted = [];
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
// Unelevated: cannot rewrite HKLM.
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
deleted.push(args[1]);
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: "C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
// Portable HKCU must remain so Explorer still has a working entry.
|
|
assert.equal(deleted.length, 0);
|
|
assert.equal(present.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"), true);
|
|
assert.equal(result.enabled, true);
|
|
});
|
|
|
|
test("installExplorerContextMenu fails when residual HKCU verbs cannot be cleared under HKLM", () => {
|
|
const exe = "C:\\Program Files\\Netcatty\\Netcatty.exe";
|
|
const folderCmd = buildExplorerContextMenuCommand(exe, "%1");
|
|
const backgroundCmd = buildExplorerContextMenuCommand(exe, "%V");
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Portable\\Netcatty.exe" -- --open-terminal-path="%1."'],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", folderCmd],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", backgroundCmd],
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
// Simulate locked residual portable HKCU verb.
|
|
if (String(args[1]).startsWith("HKCU\\")) {
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "add") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
});
|
|
|
|
test("installExplorerContextMenu fails when suppression cleanup fails on user-scope enable", () => {
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
]);
|
|
const values = new Map([
|
|
[`HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::${SUPPRESSION_VALUE}`, ""],
|
|
[`HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::${SUPPRESSION_VALUE}`, ""],
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
if (args[0] === "add") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: "C:\\Apps\\Netcatty.exe",
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, false);
|
|
});
|
|
|
|
test("installExplorerContextMenu falls back to HKCU when only a partial HKLM verb remains", () => {
|
|
const exe = "C:\\Program Files\\Netcatty\\Netcatty.exe";
|
|
const present = new Set([
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKLM\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", '"C:\\Old\\Netcatty.exe" --open-terminal-path "%1"'],
|
|
]);
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const key = args[1];
|
|
if (String(key).startsWith("HKLM\\")) {
|
|
return { status: 1, stdout: "", stderr: "access denied" };
|
|
}
|
|
present.add(key);
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${key}::${valueName}`, args[dataIdx + 1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = installExplorerContextMenu({
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(
|
|
values.get("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::"),
|
|
buildExplorerContextMenuCommand(exe, "%1"),
|
|
);
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference caches default-off via probe marker only", () => {
|
|
const wrote = [];
|
|
const fsModule = {
|
|
existsSync: () => false,
|
|
mkdirSync: () => {},
|
|
writeFileSync: (filePath, data) => {
|
|
wrote.push({ filePath: String(filePath), data: JSON.parse(String(data)) });
|
|
},
|
|
};
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: "D:\\Tools\\NetcattyPortable.exe",
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl: () => ({ status: 1, stdout: "", stderr: "missing" }),
|
|
logWarn: () => {},
|
|
});
|
|
assert.deepEqual(result, { enabled: false, success: true, supported: true });
|
|
assert.equal(wrote.length, 1);
|
|
assert.match(wrote[0].filePath, /explorer-context-menu-probe\.json$/);
|
|
assert.equal(wrote[0].data.schemaVersion, EXPLORER_CONTEXT_MENU_SCHEMA_VERSION);
|
|
assert.equal(Object.hasOwn(wrote[0].data, "enabled"), false);
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference repairs per-user HKCU verbs after probe", () => {
|
|
const exe = "C:\\Users\\me\\AppData\\Local\\Programs\\Netcatty\\Netcatty.exe";
|
|
const folderCmd = buildExplorerContextMenuCommand(exe, "%1");
|
|
const backgroundCmd = buildExplorerContextMenuCommand(exe, "%V");
|
|
const files = new Map([
|
|
[
|
|
"C:\\Users\\test\\AppData\\Roaming\\Netcatty\\explorer-context-menu-probe.json",
|
|
JSON.stringify({ schemaVersion: EXPLORER_CONTEXT_MENU_SCHEMA_VERSION }),
|
|
],
|
|
]);
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", folderCmd],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::MUIVerb", "Open in Netcatty"],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty::Icon", `${exe},0`],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", backgroundCmd],
|
|
]);
|
|
let wrotePreference = null;
|
|
const fsModule = {
|
|
existsSync: (p) => files.has(String(p)),
|
|
readFileSync: (p) => files.get(String(p)),
|
|
mkdirSync: () => {},
|
|
writeFileSync: (p, data) => {
|
|
files.set(String(p), String(data));
|
|
if (String(p).endsWith("explorer-context-menu-preferences.json")) {
|
|
wrotePreference = JSON.parse(String(data));
|
|
}
|
|
},
|
|
};
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add" || args[0] === "delete") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(wrotePreference?.enabled, true);
|
|
});
|
|
|
|
test("resolveExplorerContextMenuLaunchSpec includes dev app entry for electron.exe", () => {
|
|
const spec = resolveExplorerContextMenuLaunchSpec({
|
|
execPath: "C:\\dev\\node_modules\\electron\\dist\\electron.exe",
|
|
env: {},
|
|
argv: [
|
|
"C:\\dev\\node_modules\\electron\\dist\\electron.exe",
|
|
"C:\\dev\\netcatty",
|
|
],
|
|
defaultApp: true,
|
|
pathModule: {
|
|
resolve: (value) => value,
|
|
},
|
|
});
|
|
assert.equal(spec.executablePath, "C:\\dev\\node_modules\\electron\\dist\\electron.exe");
|
|
assert.deepEqual(spec.appArgs, ["C:\\dev\\netcatty"]);
|
|
});
|
|
|
|
test("non-windows platforms report unsupported explorer context menu", () => {
|
|
assert.equal(
|
|
isExplorerContextMenuRegistered({ platform: "darwin", spawnSyncImpl: () => {
|
|
throw new Error("should not run");
|
|
} }),
|
|
false,
|
|
);
|
|
const removed = removeExplorerContextMenu({ platform: "linux" });
|
|
assert.equal(removed.supported, false);
|
|
assert.equal(removed.enabled, false);
|
|
});
|
|
|
|
test("resolveExplorerContextMenuExecutablePath prefers portable launcher path", () => {
|
|
assert.equal(
|
|
resolveExplorerContextMenuExecutablePath({
|
|
execPath: "C:\\Users\\me\\AppData\\Local\\Temp\\ncaXXXX\\Netcatty.exe",
|
|
env: { PORTABLE_EXECUTABLE_FILE: "D:\\Tools\\NetcattyPortable.exe" },
|
|
}),
|
|
"D:\\Tools\\NetcattyPortable.exe",
|
|
);
|
|
assert.equal(
|
|
resolveExplorerContextMenuExecutablePath({
|
|
execPath: "C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
env: {},
|
|
}),
|
|
"C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
);
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference warm-starts when verbs remain registered", () => {
|
|
const exe = "D:\\Tools\\NetcattyPortable.exe";
|
|
const folderCmd = buildExplorerContextMenuCommand(exe, "%1");
|
|
const backgroundCmd = buildExplorerContextMenuCommand(exe, "%V");
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", folderCmd],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", backgroundCmd],
|
|
]);
|
|
let writes = 0;
|
|
const fsModule = {
|
|
existsSync: () => true,
|
|
readFileSync: () => JSON.stringify({
|
|
enabled: true,
|
|
schemaVersion: EXPLORER_CONTEXT_MENU_SCHEMA_VERSION,
|
|
executablePath: exe,
|
|
}),
|
|
mkdirSync: () => {
|
|
throw new Error("should not write preference on healthy warm start");
|
|
},
|
|
writeFileSync: () => {
|
|
throw new Error("should not write preference on healthy warm start");
|
|
},
|
|
};
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "add" || args[0] === "delete") {
|
|
writes += 1;
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.deepEqual(result, { enabled: true, success: true, supported: true });
|
|
assert.equal(writes, 0);
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference re-applies when enabled preference has missing verbs", () => {
|
|
const exe = "D:\\Tools\\NetcattyPortable.exe";
|
|
let wrote = null;
|
|
const present = new Set([
|
|
// Only one verb remains after an interrupted rewrite.
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", buildExplorerContextMenuCommand(exe, "%1")],
|
|
]);
|
|
const fsModule = {
|
|
existsSync: () => true,
|
|
readFileSync: () => JSON.stringify({
|
|
enabled: true,
|
|
schemaVersion: EXPLORER_CONTEXT_MENU_SCHEMA_VERSION,
|
|
executablePath: exe,
|
|
}),
|
|
mkdirSync: () => {},
|
|
writeFileSync: (_p, data) => {
|
|
wrote = JSON.parse(String(data));
|
|
},
|
|
};
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const key = args[1];
|
|
present.add(key);
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${key}::${valueName}`, args[dataIdx + 1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: exe,
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(wrote?.enabled, true);
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference re-suppresses when preference is disabled", () => {
|
|
let wrote = null;
|
|
const present = new Set([
|
|
"HKLM\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKLM\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
]);
|
|
const suppressed = new Set();
|
|
const fsModule = {
|
|
existsSync: () => true,
|
|
readFileSync: () => JSON.stringify({
|
|
enabled: false,
|
|
schemaVersion: EXPLORER_CONTEXT_MENU_SCHEMA_VERSION,
|
|
executablePath: "C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
}),
|
|
mkdirSync: () => {},
|
|
writeFileSync: (_path, data) => {
|
|
wrote = JSON.parse(String(data));
|
|
},
|
|
};
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v")) {
|
|
if (args.includes(SUPPRESSION_VALUE) && suppressed.has(args[1])) {
|
|
return { status: 0, stdout: "ok", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1]) || suppressed.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
present.delete(args[1]);
|
|
suppressed.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const valueIdx = args.indexOf("/v");
|
|
if (valueIdx >= 0 && args[valueIdx + 1] === SUPPRESSION_VALUE) {
|
|
suppressed.add(args[1]);
|
|
present.add(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected add" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: "C:\\Program Files\\Netcatty\\Netcatty.exe",
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, false);
|
|
assert.equal(wrote?.enabled, false);
|
|
assert.ok(suppressed.has("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty"));
|
|
assert.ok(suppressed.has("HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty"));
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference re-applies when development appArgs change", () => {
|
|
const electronExe = "C:\\dev\\node_modules\\electron\\dist\\electron.exe";
|
|
const oldApp = "C:\\old\\netcatty";
|
|
const newApp = "C:\\new\\netcatty";
|
|
let wrote = null;
|
|
const present = new Set([
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty",
|
|
"HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command",
|
|
]);
|
|
const values = new Map([
|
|
["HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::", buildExplorerContextMenuCommand(electronExe, "%1", { appArgs: [oldApp] })],
|
|
["HKCU\\Software\\Classes\\Directory\\Background\\shell\\Netcatty\\command::", buildExplorerContextMenuCommand(electronExe, "%V", { appArgs: [oldApp] })],
|
|
]);
|
|
const fsModule = {
|
|
existsSync: () => true,
|
|
readFileSync: () => JSON.stringify({
|
|
enabled: true,
|
|
schemaVersion: EXPLORER_CONTEXT_MENU_SCHEMA_VERSION,
|
|
executablePath: electronExe,
|
|
appArgs: [oldApp],
|
|
}),
|
|
mkdirSync: () => {},
|
|
writeFileSync: (_p, data) => {
|
|
wrote = JSON.parse(String(data));
|
|
},
|
|
};
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${key}::${valueName}`, args[dataIdx + 1]);
|
|
present.add(key);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: electronExe,
|
|
appArgs: [newApp],
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.deepEqual(wrote?.appArgs, [newApp]);
|
|
assert.equal(
|
|
values.get("HKCU\\Software\\Classes\\Directory\\shell\\Netcatty\\command::"),
|
|
buildExplorerContextMenuCommand(electronExe, "%1", { appArgs: [newApp] }),
|
|
);
|
|
});
|
|
|
|
test("applyInitialExplorerContextMenuPreference re-applies when portable path changes", () => {
|
|
let wrote = null;
|
|
const present = new Set();
|
|
const values = new Map();
|
|
const fsModule = {
|
|
existsSync: () => true,
|
|
readFileSync: () => JSON.stringify({
|
|
enabled: true,
|
|
schemaVersion: EXPLORER_CONTEXT_MENU_SCHEMA_VERSION,
|
|
executablePath: "D:\\Old\\NetcattyPortable.exe",
|
|
}),
|
|
mkdirSync: () => {},
|
|
writeFileSync: (_path, data) => {
|
|
wrote = JSON.parse(String(data));
|
|
},
|
|
};
|
|
const spawnSyncImpl = (cmd, args) => {
|
|
assert.equal(cmd, "reg.exe");
|
|
if (args[0] === "query") {
|
|
if (args.includes("/v") || args.includes("/ve")) {
|
|
const key = args[1];
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const mapKey = `${key}::${valueName}`;
|
|
if (values.has(mapKey)) {
|
|
return {
|
|
status: 0,
|
|
stdout: ` ${(valueName || "(Default)").padEnd(16)}REG_SZ ${values.get(mapKey)}\n`,
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { status: 1, stdout: "", stderr: "no value" };
|
|
}
|
|
return present.has(args[1])
|
|
? { status: 0, stdout: "ok", stderr: "" }
|
|
: { status: 1, stdout: "", stderr: "missing" };
|
|
}
|
|
if (args[0] === "add") {
|
|
present.add(args[1]);
|
|
const valueName = args.includes("/ve") ? "" : args[args.indexOf("/v") + 1];
|
|
const dataIdx = args.indexOf("/d");
|
|
if (dataIdx >= 0) values.set(`${args[1]}::${valueName}`, args[dataIdx + 1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (args[0] === "delete") {
|
|
present.delete(args[1]);
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
}
|
|
return { status: 1, stdout: "", stderr: "unexpected" };
|
|
};
|
|
|
|
const nextExe = "E:\\Moved\\NetcattyPortable.exe";
|
|
const result = applyInitialExplorerContextMenuPreference({
|
|
app: { getPath: () => "C:\\Users\\test\\AppData\\Roaming\\Netcatty" },
|
|
executablePath: nextExe,
|
|
platform: "win32",
|
|
fsModule,
|
|
spawnSyncImpl,
|
|
logWarn: () => {},
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.enabled, true);
|
|
assert.equal(wrote?.executablePath, nextExe);
|
|
assert.ok(
|
|
[...values.values()].some((value) => String(value).includes(nextExe)),
|
|
"registry command should use the new portable launcher path",
|
|
);
|
|
});
|