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
405 lines
16 KiB
JavaScript
405 lines
16 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const {
|
|
HIDDEN_LAUNCH_ARG,
|
|
isAutoLaunchSupported,
|
|
resolveEffectiveLoginState,
|
|
buildLoginItemQueryOptions,
|
|
getAutoLaunchEnabled,
|
|
setAutoLaunchEnabled,
|
|
wasLaunchedHidden,
|
|
registerHandlers,
|
|
} = require("./autoLaunch.cjs");
|
|
|
|
const EXEC_PATH = "C:\\Netcatty\\Netcatty.exe";
|
|
|
|
function hiddenLaunchItem(overrides = {}) {
|
|
return { name: "Netcatty", path: EXEC_PATH, args: [HIDDEN_LAUNCH_ARG], scope: "user", enabled: true, ...overrides };
|
|
}
|
|
|
|
test("isAutoLaunchSupported is false when running unpackaged (electron .)", () => {
|
|
assert.equal(isAutoLaunchSupported({ defaultApp: true, platform: "win32" }), false);
|
|
assert.equal(isAutoLaunchSupported({ defaultApp: true, platform: "darwin" }), false);
|
|
});
|
|
|
|
test("isAutoLaunchSupported is true on macOS and Windows when packaged", () => {
|
|
assert.equal(isAutoLaunchSupported({ defaultApp: false, platform: "darwin" }), true);
|
|
assert.equal(isAutoLaunchSupported({ defaultApp: false, platform: "win32" }), true);
|
|
});
|
|
|
|
test("isAutoLaunchSupported is false on Linux — Electron's login-item API is a no-op there", () => {
|
|
assert.equal(isAutoLaunchSupported({ defaultApp: false, platform: "linux" }), false);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState uses the matching launchItems entry's enabled flag on Windows", () => {
|
|
const settings = { openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: false })] };
|
|
|
|
assert.equal(
|
|
resolveEffectiveLoginState(settings, "win32", EXEC_PATH),
|
|
false,
|
|
"Task Manager can disable the specific --hidden entry while openAtLogin stays true",
|
|
);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState reports true when the matching launchItems entry is enabled", () => {
|
|
const settings = { openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: true })] };
|
|
|
|
assert.equal(resolveEffectiveLoginState(settings, "win32", EXEC_PATH), true);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState ignores unrelated launchItems entries (different path or args)", () => {
|
|
const settings = {
|
|
openAtLogin: false,
|
|
launchItems: [
|
|
hiddenLaunchItem({ path: "C:\\Other\\App.exe", enabled: true }),
|
|
hiddenLaunchItem({ args: [], enabled: true }),
|
|
],
|
|
};
|
|
|
|
assert.equal(
|
|
resolveEffectiveLoginState(settings, "win32", EXEC_PATH),
|
|
false,
|
|
"a differently-scoped entry (wrong path or wrong args) must not count as our own registration",
|
|
);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState falls back to openAtLogin when no matching launchItems entry exists", () => {
|
|
assert.equal(resolveEffectiveLoginState({ openAtLogin: true, launchItems: [] }, "win32", EXEC_PATH), true);
|
|
assert.equal(resolveEffectiveLoginState({ openAtLogin: false, launchItems: [] }, "win32", EXEC_PATH), false);
|
|
assert.equal(resolveEffectiveLoginState({ openAtLogin: true }, "win32", EXEC_PATH), true, "no launchItems array at all");
|
|
});
|
|
|
|
test("resolveEffectiveLoginState does not consult launchItems on non-Windows platforms", () => {
|
|
const settings = { openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: false })] };
|
|
|
|
assert.equal(
|
|
resolveEffectiveLoginState(settings, "darwin", EXEC_PATH),
|
|
true,
|
|
"launchItems is Windows-only; macOS must read openAtLogin directly",
|
|
);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState treats a macOS pending-approval registration as enabled", () => {
|
|
const settings = { openAtLogin: false, status: "requires-approval" };
|
|
|
|
assert.equal(
|
|
resolveEffectiveLoginState(settings, "darwin", EXEC_PATH),
|
|
true,
|
|
"openAtLogin reports false while SMAppService awaits System Settings approval, even though registration succeeded; " +
|
|
"reporting false here would make the renderer's push effect immediately unregister the pending item",
|
|
);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState ignores the requires-approval special-case on non-macOS platforms", () => {
|
|
const settings = { openAtLogin: false, status: "requires-approval" };
|
|
|
|
assert.equal(resolveEffectiveLoginState(settings, "win32", EXEC_PATH), false);
|
|
});
|
|
|
|
test("resolveEffectiveLoginState reads openAtLogin normally for other macOS statuses", () => {
|
|
assert.equal(resolveEffectiveLoginState({ openAtLogin: true, status: "enabled" }, "darwin", EXEC_PATH), true);
|
|
assert.equal(resolveEffectiveLoginState({ openAtLogin: false, status: "not-registered" }, "darwin", EXEC_PATH), false);
|
|
assert.equal(resolveEffectiveLoginState({ openAtLogin: false, status: "not-found" }, "darwin", EXEC_PATH), false);
|
|
});
|
|
|
|
test("buildLoginItemQueryOptions matches the path+args our own writes always register", () => {
|
|
assert.deepEqual(
|
|
buildLoginItemQueryOptions(EXEC_PATH),
|
|
{ path: EXEC_PATH, args: [HIDDEN_LAUNCH_ARG] },
|
|
);
|
|
});
|
|
|
|
test("getAutoLaunchEnabled reports unsupported without touching app in dev", () => {
|
|
let called = false;
|
|
const app = { getLoginItemSettings: () => { called = true; return { openAtLogin: true }; } };
|
|
|
|
const result = getAutoLaunchEnabled({ app, defaultApp: true, platform: "win32" });
|
|
|
|
assert.deepEqual(result, { success: true, enabled: false, supported: false });
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
test("getAutoLaunchEnabled queries the same path+args the login item was registered with", () => {
|
|
let capturedOptions = null;
|
|
const app = {
|
|
getLoginItemSettings: (options) => { capturedOptions = options; return { openAtLogin: true }; },
|
|
};
|
|
|
|
getAutoLaunchEnabled({ app, execPath: EXEC_PATH, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(capturedOptions, { path: EXEC_PATH, args: [HIDDEN_LAUNCH_ARG] });
|
|
});
|
|
|
|
test("getAutoLaunchEnabled reports unsupported on Linux without touching app", () => {
|
|
let called = false;
|
|
const app = { getLoginItemSettings: () => { called = true; return { openAtLogin: true }; } };
|
|
|
|
const result = getAutoLaunchEnabled({ app, defaultApp: false, platform: "linux" });
|
|
|
|
assert.deepEqual(result, { success: true, enabled: false, supported: false });
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
test("getAutoLaunchEnabled reflects the current login item state on macOS", () => {
|
|
const app = { getLoginItemSettings: () => ({ openAtLogin: true }) };
|
|
|
|
const result = getAutoLaunchEnabled({ app, defaultApp: false, platform: "darwin" });
|
|
|
|
assert.deepEqual(result, { success: true, enabled: true, supported: true });
|
|
});
|
|
|
|
test("getAutoLaunchEnabled reports disabled when Windows Startup Apps has disabled the matching entry", () => {
|
|
const app = {
|
|
getLoginItemSettings: () => ({ openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: false })] }),
|
|
};
|
|
|
|
const result = getAutoLaunchEnabled({ app, execPath: EXEC_PATH, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(result, { success: true, enabled: false, supported: true });
|
|
});
|
|
|
|
test("getAutoLaunchEnabled is not fooled by an unrelated no-argument entry for the same executable", () => {
|
|
const app = {
|
|
getLoginItemSettings: () => ({
|
|
openAtLogin: false,
|
|
launchItems: [hiddenLaunchItem({ args: [], enabled: true })],
|
|
}),
|
|
};
|
|
|
|
const result = getAutoLaunchEnabled({ app, execPath: EXEC_PATH, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(
|
|
result,
|
|
{ success: true, enabled: false, supported: true },
|
|
"executableWillLaunchAtLogin-style any-args matching would wrongly report true here",
|
|
);
|
|
});
|
|
|
|
test("getAutoLaunchEnabled reports success:false (not a confirmed disabled state) when the app API throws", () => {
|
|
const app = { getLoginItemSettings: () => { throw new Error("boom"); } };
|
|
|
|
const result = getAutoLaunchEnabled({ app, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(
|
|
result,
|
|
{ success: false, enabled: false, supported: true },
|
|
"callers (renderer hydration) must be able to tell a transient read failure apart from a confirmed enabled:false, or they will overwrite a cached true and cascade into an unwanted disable write",
|
|
);
|
|
});
|
|
|
|
test("setAutoLaunchEnabled(true) registers the hidden launch arg", () => {
|
|
let capturedSettings = null;
|
|
const app = {
|
|
setLoginItemSettings: (settings) => { capturedSettings = settings; },
|
|
getLoginItemSettings: () => ({ openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: true })] }),
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(true, {
|
|
app,
|
|
execPath: EXEC_PATH,
|
|
defaultApp: false,
|
|
platform: "win32",
|
|
});
|
|
|
|
assert.deepEqual(capturedSettings, {
|
|
openAtLogin: true,
|
|
openAsHidden: true,
|
|
path: EXEC_PATH,
|
|
args: [HIDDEN_LAUNCH_ARG],
|
|
});
|
|
assert.deepEqual(result, { success: true, enabled: true, supported: true });
|
|
});
|
|
|
|
test("setAutoLaunchEnabled(true) verifies with the same path+args it just wrote", () => {
|
|
let capturedQueryOptions = null;
|
|
const app = {
|
|
setLoginItemSettings: () => {},
|
|
getLoginItemSettings: (options) => {
|
|
capturedQueryOptions = options;
|
|
// Electron 42 contract: openAtLogin only reflects true for the exact
|
|
// path+args queried — a bare getLoginItemSettings() call (no args)
|
|
// would incorrectly report false right after enabling with --hidden.
|
|
return options?.args?.includes(HIDDEN_LAUNCH_ARG)
|
|
? { openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: true })] }
|
|
: { openAtLogin: false, launchItems: [] };
|
|
},
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(true, {
|
|
app,
|
|
execPath: EXEC_PATH,
|
|
defaultApp: false,
|
|
platform: "win32",
|
|
});
|
|
|
|
assert.deepEqual(capturedQueryOptions, { path: EXEC_PATH, args: [HIDDEN_LAUNCH_ARG] });
|
|
assert.equal(result.enabled, true, "must not report false just because the query omitted matching args");
|
|
});
|
|
|
|
test("setAutoLaunchEnabled(true) reports disabled when Windows Startup Apps blocks the matching entry", () => {
|
|
const app = {
|
|
setLoginItemSettings: () => {},
|
|
getLoginItemSettings: () => ({ openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: false })] }),
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(true, { app, execPath: EXEC_PATH, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(result, { success: true, enabled: false, supported: true });
|
|
});
|
|
|
|
test("setAutoLaunchEnabled(true) on macOS does not report disabled while approval is pending", () => {
|
|
const app = {
|
|
setLoginItemSettings: () => {},
|
|
getLoginItemSettings: () => ({ openAtLogin: false, status: "requires-approval" }),
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(true, { app, defaultApp: false, platform: "darwin" });
|
|
|
|
assert.deepEqual(
|
|
result,
|
|
{ success: true, enabled: true, supported: true },
|
|
"must match what the user requested, or the renderer's push effect fires an unregistering write before approval",
|
|
);
|
|
});
|
|
|
|
test("setAutoLaunchEnabled(false) clears the hidden launch arg", () => {
|
|
let capturedSettings = null;
|
|
const app = {
|
|
setLoginItemSettings: (settings) => { capturedSettings = settings; },
|
|
getLoginItemSettings: () => ({ openAtLogin: false, launchItems: [] }),
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(false, { app, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(capturedSettings.args, []);
|
|
assert.equal(capturedSettings.openAtLogin, false);
|
|
assert.deepEqual(result, { success: true, enabled: false, supported: true });
|
|
});
|
|
|
|
test("setAutoLaunchEnabled is a no-op in dev and does not call the app API", () => {
|
|
let called = false;
|
|
const app = { setLoginItemSettings: () => { called = true; } };
|
|
|
|
const result = setAutoLaunchEnabled(true, { app, defaultApp: true, platform: "win32" });
|
|
|
|
assert.equal(called, false);
|
|
assert.deepEqual(
|
|
result,
|
|
{ success: true, enabled: false, supported: false },
|
|
"enabled:false is a confirmed fact when unsupported, not an unknown state — matches getAutoLaunchEnabled",
|
|
);
|
|
});
|
|
|
|
test("setAutoLaunchEnabled is a no-op on Linux and does not call the app API", () => {
|
|
let called = false;
|
|
const app = { setLoginItemSettings: () => { called = true; } };
|
|
|
|
const result = setAutoLaunchEnabled(true, { app, defaultApp: false, platform: "linux" });
|
|
|
|
assert.equal(called, false);
|
|
assert.deepEqual(result, { success: true, enabled: false, supported: false });
|
|
});
|
|
|
|
test("setAutoLaunchEnabled reports the real (unwritten) state when the write fails but a fallback read succeeds", () => {
|
|
const app = {
|
|
setLoginItemSettings: () => { throw new Error("registry locked"); },
|
|
getLoginItemSettings: () => ({ openAtLogin: false, launchItems: [] }),
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(true, { app, execPath: EXEC_PATH, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(
|
|
result,
|
|
{ success: true, enabled: false, supported: true },
|
|
"success means \"enabled is trustworthy\", not \"the write succeeded\" — the renderer's push effect relies on " +
|
|
"this to roll an optimistic toggle back to the real state instead of leaving it stuck on a failed write",
|
|
);
|
|
});
|
|
|
|
test("setAutoLaunchEnabled reports success:false only when the write AND the fallback read both fail", () => {
|
|
const app = {
|
|
setLoginItemSettings: () => { throw new Error("registry locked"); },
|
|
getLoginItemSettings: () => { throw new Error("registry unreadable"); },
|
|
};
|
|
|
|
const result = setAutoLaunchEnabled(true, { app, defaultApp: false, platform: "win32" });
|
|
|
|
assert.deepEqual(
|
|
result,
|
|
{ success: false, enabled: false, supported: true },
|
|
"a genuine double failure leaves the real state unknown — the renderer must preserve its last-known value",
|
|
);
|
|
});
|
|
|
|
test("wasLaunchedHidden detects the --hidden cold-start flag", () => {
|
|
assert.equal(wasLaunchedHidden({ argv: ["node", "main.js", "--hidden"], platform: "win32" }), true);
|
|
assert.equal(wasLaunchedHidden({ argv: ["node", "main.js"], platform: "win32" }), false);
|
|
assert.equal(wasLaunchedHidden({ argv: undefined, platform: "win32" }), false);
|
|
});
|
|
|
|
test("wasLaunchedHidden detects a macOS login-item launch via wasOpenedAtLogin", () => {
|
|
const app = { getLoginItemSettings: () => ({ wasOpenedAtLogin: true }) };
|
|
|
|
const result = wasLaunchedHidden({ argv: ["node", "main.js"], app, platform: "darwin" });
|
|
|
|
assert.equal(
|
|
result,
|
|
true,
|
|
"openAsHidden/wasOpenedAsHidden are deprecated and stop working on macOS 13+, so wasOpenedAtLogin is the only reliable signal",
|
|
);
|
|
});
|
|
|
|
test("wasLaunchedHidden does not consult macOS login-item state on other platforms", () => {
|
|
let called = false;
|
|
const app = { getLoginItemSettings: () => { called = true; return { wasOpenedAtLogin: true }; } };
|
|
|
|
const result = wasLaunchedHidden({ argv: ["node", "main.js"], app, platform: "win32" });
|
|
|
|
assert.equal(result, false);
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
test("wasLaunchedHidden tolerates a throwing macOS login-item lookup", () => {
|
|
const app = { getLoginItemSettings: () => { throw new Error("boom"); } };
|
|
|
|
const result = wasLaunchedHidden({ argv: [], app, platform: "darwin" });
|
|
|
|
assert.equal(result, false);
|
|
});
|
|
|
|
test("registerHandlers wires get/set IPC channels", async () => {
|
|
const handlers = new Map();
|
|
const ipcMain = { handle: (channel, fn) => handlers.set(channel, fn) };
|
|
const app = {
|
|
getLoginItemSettings: () => ({ openAtLogin: false, launchItems: [] }),
|
|
setLoginItemSettings: () => {},
|
|
};
|
|
|
|
registerHandlers(ipcMain, { app, platform: "win32" });
|
|
|
|
assert.ok(handlers.has("netcatty:autoLaunch:get"));
|
|
assert.ok(handlers.has("netcatty:autoLaunch:set"));
|
|
|
|
const getResult = await handlers.get("netcatty:autoLaunch:get")();
|
|
assert.deepEqual(getResult, { success: true, enabled: false, supported: true });
|
|
|
|
app.getLoginItemSettings = () => ({ openAtLogin: true, launchItems: [hiddenLaunchItem({ enabled: true })] });
|
|
const setResult = await handlers.get("netcatty:autoLaunch:set")(null, { enabled: true });
|
|
assert.deepEqual(setResult, { success: true, enabled: true, supported: true });
|
|
});
|
|
|
|
test("registerHandlers respects the real process.platform when no override is given", async () => {
|
|
const handlers = new Map();
|
|
const ipcMain = { handle: (channel, fn) => handlers.set(channel, fn) };
|
|
const app = { getLoginItemSettings: () => ({ openAtLogin: false }) };
|
|
|
|
registerHandlers(ipcMain, { app });
|
|
const result = await handlers.get("netcatty:autoLaunch:get")();
|
|
|
|
assert.equal(
|
|
result.supported,
|
|
process.platform === "darwin" || process.platform === "win32",
|
|
);
|
|
});
|