mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
* test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) Two shards on release/v3.8.51 went red in one day with the same signature — "ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only .github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass alone and on re-run: the cleanup races something still writing into the directory (SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner the window opens. 1154 test files do their own cleanup with fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries. One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record): every rm / rmSync / rmdirSync option object with `recursive: true` and no `maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292 files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included. Only the option object changes: no call site, assertion or import is touched. Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292 files; a random 20-file sample runs green (quota-redis-store hangs identically on the untouched tree — it needs a Redis on localhost, an environment matter). The four unit shards on this PR are the full run. * fix(quality): let check-forgotten-sibling-tests read a 1,000-file diff The gate shells out to `git diff` through execFileSync with Node's default 1 MB maxBuffer; the 1,292-file codemod in this PR is the first diff large enough to overflow it, and the gate died with `spawnSync git ENOBUFS` before comparing anything. 64 MB is far above any real PR and costs nothing when unused.
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
/**
|
|
* Tests for Electron Remote Server Mode
|
|
*
|
|
* Covers:
|
|
* - resolveRemoteServerUrl precedence (env > persisted prefs > null)
|
|
* - URL validation (only http/https accepted, trailing slash stripped)
|
|
* - Corrupt/partial prefs file handled gracefully (falls back to local server)
|
|
* - remoteServerPreferences read/write round-trip
|
|
* - main.js wiring: startNextServer() short-circuits in remote mode, tray
|
|
* menu exposes the toggle, packaging manifest ships the new files
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createRequire } from "node:module";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const {
|
|
resolveRemoteServerUrl,
|
|
isValidHttpUrl,
|
|
} = require("../../electron/lib/resolveRemoteServerUrl");
|
|
const {
|
|
readPreferences,
|
|
writeRemoteServerUrl,
|
|
writeCloseBehavior,
|
|
} = require("../../electron/lib/remoteServerPreferences");
|
|
|
|
function withTempDir(fn: (dir: string) => void) {
|
|
const dir = mkdtempSync(join(tmpdir(), "omniroute-remote-server-"));
|
|
try {
|
|
fn(dir);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
}
|
|
|
|
describe("resolveRemoteServerUrl precedence", () => {
|
|
it("returns null when neither env var nor prefs file are set", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
const result = resolveRemoteServerUrl({ env: {}, prefsPath });
|
|
assert.equal(result, null);
|
|
});
|
|
});
|
|
|
|
it("prefers OMNIROUTE_REMOTE_URL env var over the persisted prefs file", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://from-prefs:20128");
|
|
|
|
const result = resolveRemoteServerUrl({
|
|
env: { OMNIROUTE_REMOTE_URL: "http://from-env:20128" },
|
|
prefsPath,
|
|
});
|
|
assert.equal(result, "http://from-env:20128");
|
|
});
|
|
});
|
|
|
|
it("falls back to the persisted prefs file when no env var is set", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://localhost:20128");
|
|
|
|
const result = resolveRemoteServerUrl({ env: {}, prefsPath });
|
|
assert.equal(result, "http://localhost:20128");
|
|
});
|
|
});
|
|
|
|
it("strips a trailing slash from the resolved URL", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
const result = resolveRemoteServerUrl({
|
|
env: { OMNIROUTE_REMOTE_URL: "http://localhost:20128/" },
|
|
prefsPath,
|
|
});
|
|
assert.equal(result, "http://localhost:20128");
|
|
});
|
|
});
|
|
|
|
it("rejects a non-http(s) URL (e.g. file:// or javascript:) and returns null", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
for (const bad of ["file:///etc/passwd", "javascript:alert(1)", "not a url", ""]) {
|
|
const result = resolveRemoteServerUrl({ env: { OMNIROUTE_REMOTE_URL: bad }, prefsPath });
|
|
assert.equal(result, null, `expected null for ${JSON.stringify(bad)}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
it("ignores a corrupt prefs file and falls back to null rather than throwing", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
require("node:fs").writeFileSync(prefsPath, "{ not valid json", "utf8");
|
|
|
|
const result = resolveRemoteServerUrl({ env: {}, prefsPath });
|
|
assert.equal(result, null);
|
|
});
|
|
});
|
|
|
|
it("treats a missing prefs file as absent rather than throwing", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "does-not-exist.json");
|
|
assert.doesNotThrow(() => resolveRemoteServerUrl({ env: {}, prefsPath }));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isValidHttpUrl", () => {
|
|
it("accepts http and https", () => {
|
|
assert.equal(isValidHttpUrl("http://localhost:20128"), true);
|
|
assert.equal(isValidHttpUrl("https://omniroute.example.com"), true);
|
|
});
|
|
|
|
it("rejects other protocols and invalid strings", () => {
|
|
assert.equal(isValidHttpUrl("ftp://example.com"), false);
|
|
assert.equal(isValidHttpUrl("file:///etc/passwd"), false);
|
|
assert.equal(isValidHttpUrl("javascript:alert(1)"), false);
|
|
assert.equal(isValidHttpUrl("not a url"), false);
|
|
});
|
|
});
|
|
|
|
describe("remoteServerPreferences read/write", () => {
|
|
it("round-trips a URL through write then read", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://localhost:20128");
|
|
assert.deepEqual(readPreferences(prefsPath), {
|
|
remoteServerUrl: "http://localhost:20128",
|
|
closeBehavior: "keep-loaded",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("clearing with null removes the preference", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "http://localhost:20128");
|
|
writeRemoteServerUrl(prefsPath, null);
|
|
assert.deepEqual(readPreferences(prefsPath), {
|
|
remoteServerUrl: null,
|
|
closeBehavior: "keep-loaded",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("creates the parent directory if it does not exist yet", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "nested", "deep", "electron-preferences.json");
|
|
assert.doesNotThrow(() => writeRemoteServerUrl(prefsPath, "http://localhost:20128"));
|
|
assert.equal(existsSync(prefsPath), true);
|
|
assert.deepEqual(readPreferences(prefsPath), {
|
|
remoteServerUrl: "http://localhost:20128",
|
|
closeBehavior: "keep-loaded",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("reading a nonexistent prefs file returns remoteServerUrl: null", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
assert.deepEqual(readPreferences(prefsPath), {
|
|
remoteServerUrl: null,
|
|
closeBehavior: "keep-loaded",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("persists close behavior without discarding the remote server URL", () => {
|
|
withTempDir((dir) => {
|
|
const prefsPath = join(dir, "electron-preferences.json");
|
|
writeRemoteServerUrl(prefsPath, "https://omniroute.example.com");
|
|
writeCloseBehavior(prefsPath, "unload");
|
|
assert.deepEqual(readPreferences(prefsPath), {
|
|
remoteServerUrl: "https://omniroute.example.com",
|
|
closeBehavior: "unload",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── main.js wiring (static-analysis style, matching the repo's existing
|
|
// convention for asserting structure without importing the Electron binary) ───
|
|
|
|
describe("Electron main.js Remote Server Mode wiring", () => {
|
|
const mainSrc = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8");
|
|
|
|
it("startNextServer() short-circuits before the isDev branch when remoteServerUrl is set", () => {
|
|
const fn = mainSrc.match(/function startNextServer\(\)[\s\S]*?\n}/);
|
|
assert.ok(fn, "startNextServer function should exist in electron/main.js");
|
|
const body = fn![0];
|
|
const remoteIdx = body.indexOf("if (remoteServerUrl)");
|
|
const devIdx = body.indexOf("if (isDev)");
|
|
assert.ok(remoteIdx !== -1, "startNextServer must check remoteServerUrl");
|
|
assert.ok(devIdx !== -1, "startNextServer must still check isDev");
|
|
assert.ok(remoteIdx < devIdx, "the remoteServerUrl check must come before the isDev check");
|
|
});
|
|
|
|
it("getServerUrl() prefers remoteServerUrl over the local port", () => {
|
|
assert.match(
|
|
mainSrc,
|
|
/const getServerUrl = \(\) => remoteServerUrl \|\| `http:\/\/localhost:\$\{serverPort\}`;/
|
|
);
|
|
});
|
|
|
|
it("exposes a tray menu entry to configure or clear the remote server", () => {
|
|
assert.match(mainSrc, /label: "Remote Server"/);
|
|
assert.match(mainSrc, /Connect to Remote Server/);
|
|
assert.match(mainSrc, /Disconnect \(use Local Server\)/);
|
|
});
|
|
|
|
it("the remote-server prompt window uses contextIsolation and disables nodeIntegration", () => {
|
|
const fn = mainSrc.match(/function showRemoteServerPrompt\(\)[\s\S]*?\n}/);
|
|
assert.ok(fn, "showRemoteServerPrompt function should exist");
|
|
const body = fn![0];
|
|
assert.match(body, /contextIsolation:\s*true/);
|
|
assert.match(body, /nodeIntegration:\s*false/);
|
|
});
|
|
|
|
// setRemoteServerUrl() is the runtime, UI-driven path (tray prompt / IPC) for
|
|
// applying an operator-supplied URL — distinct from resolveRemoteServerUrl()'s
|
|
// startup precedence, which is already covered above. A URL typed into the
|
|
// "Connect to Remote Server…" prompt must go through the same isValidHttpUrl
|
|
// guard (only http/https accepted) *before* any server-lifecycle mutation, so
|
|
// an arbitrary/malicious string (file://, javascript:, garbage) can never reach
|
|
// stopNextServer()/startNextServer() or get persisted to prefs. Exercised via
|
|
// static analysis (matching this file's convention) since setRemoteServerUrl
|
|
// requires the full Electron main process to invoke directly.
|
|
it("setRemoteServerUrl() validates via isValidHttpUrl and rejects before mutating server state", () => {
|
|
const fn = mainSrc.match(/async function setRemoteServerUrl\(nextUrl\)[\s\S]*?\n}/);
|
|
assert.ok(fn, "setRemoteServerUrl function should exist in electron/main.js");
|
|
const body = fn![0];
|
|
|
|
const validationIdx = body.indexOf("isValidHttpUrl(normalized)");
|
|
const stopServerIdx = body.indexOf("stopNextServer()");
|
|
assert.ok(validationIdx !== -1, "setRemoteServerUrl must validate via isValidHttpUrl");
|
|
assert.ok(
|
|
stopServerIdx !== -1,
|
|
"setRemoteServerUrl must stop the running server when switching modes"
|
|
);
|
|
assert.ok(
|
|
validationIdx < stopServerIdx,
|
|
"URL validation must run before any server-lifecycle mutation"
|
|
);
|
|
|
|
const rejectBranch = body.slice(validationIdx, stopServerIdx);
|
|
assert.match(
|
|
rejectBranch,
|
|
/return;/,
|
|
"an invalid URL must short-circuit setRemoteServerUrl instead of falling through"
|
|
);
|
|
assert.match(
|
|
rejectBranch,
|
|
/console\.warn/,
|
|
"an invalid URL should be logged so operators can see it was rejected"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Electron packaging manifest includes Remote Server Mode files", () => {
|
|
const pkg = JSON.parse(
|
|
readFileSync(join(import.meta.dirname, "../../electron/package.json"), "utf8")
|
|
);
|
|
const files: string[] = pkg.build?.files ?? [];
|
|
|
|
for (const expected of [
|
|
"lib/resolveRemoteServerUrl.js",
|
|
"lib/remoteServerPreferences.js",
|
|
"lib/windowClosePolicy.js",
|
|
"remoteServerPromptPreload.js",
|
|
"remoteServerPromptRenderer.js",
|
|
"assets/remoteServerPrompt.html",
|
|
]) {
|
|
it(`ships ${expected} in package.json build.files`, () => {
|
|
assert.ok(files.includes(expected), `${expected} is missing from build.files`);
|
|
});
|
|
}
|
|
});
|