mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 11:22:15 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
308 lines
14 KiB
TypeScript
308 lines
14 KiB
TypeScript
/**
|
|
* Unit tests for bin/aliasResolver.mjs — the ESM resolver hook that fixes
|
|
* `Cannot find package '@/shared'` when OmniRoute is installed globally
|
|
* (issue #7791).
|
|
*
|
|
* The hook is registered via module.register() and runs in a loader worker,
|
|
* which is hard to exercise directly from node:test. Instead we test:
|
|
*
|
|
* 1. The exported `resolveAlias` pure function — the mapping logic without
|
|
* needing the loader machinery.
|
|
* 2. The hook end-to-end by spawning a child process that imports a fixture
|
|
* using `@/...` specifiers after registering the resolver. This proves
|
|
* the integration works in a real Node process, not just in isolation.
|
|
*/
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
import {
|
|
resolveAlias,
|
|
registerAliasResolver,
|
|
ALIAS_PREFIX,
|
|
ALIAS_MAP,
|
|
} from "../../../bin/aliasResolver.mjs";
|
|
|
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
const REPO_ROOT = join(__dirname, "..", "..", "..");
|
|
|
|
// The child scripts below `import()` these paths, and `import()` resolves its
|
|
// specifier as a URL. Replacing backslashes with forward slashes is not enough
|
|
// on Windows: the leading drive letter is then parsed as the URL scheme `e:`,
|
|
// which the ESM loader rejects with ERR_UNSUPPORTED_ESM_URL_SCHEME. Emit a
|
|
// real file:// URL instead.
|
|
const repoFileUrl = (relPath) => pathToFileURL(join(REPO_ROOT, relPath)).href;
|
|
|
|
describe("aliasResolver.resolveAlias (pure)", () => {
|
|
it("returns null for non-@/ specifiers (lets Node/tsx handle them)", () => {
|
|
assert.equal(resolveAlias("node:fs", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("./foo", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("../foo", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("react", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("@scope/pkg", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("", REPO_ROOT), null);
|
|
});
|
|
|
|
it("maps @/X to <root>/src/X as a file URL", () => {
|
|
const got = resolveAlias("@/shared/utils/featureFlags", REPO_ROOT);
|
|
assert.ok(got, "expected a non-null URL");
|
|
assert.ok(got.startsWith("file://"), "must be a file URL");
|
|
const fsPath = fileURLToPath(got);
|
|
// The source file on disk is featureFlags.ts; the resolver probes source
|
|
// extensions and returns the first existing match. Assert against the
|
|
// known extension rather than the specifier's bare stem, so the test does
|
|
// not regress when the resolver's extension probe order changes.
|
|
assert.ok(
|
|
fsPath.endsWith(join("src", "shared", "utils", "featureFlags.ts")),
|
|
`unexpected path: ${fsPath}`
|
|
);
|
|
});
|
|
|
|
it("exposes ALIAS_PREFIX as @/ for symmetry with the hook source", () => {
|
|
assert.equal(ALIAS_PREFIX, "@/");
|
|
});
|
|
|
|
it("rejects non-string / empty input without throwing", () => {
|
|
assert.equal(resolveAlias(undefined, REPO_ROOT), null);
|
|
assert.equal(resolveAlias(null, REPO_ROOT), null);
|
|
assert.equal(resolveAlias(123, REPO_ROOT), null);
|
|
assert.equal(resolveAlias({}, REPO_ROOT), null);
|
|
});
|
|
|
|
it("rejects empty root", () => {
|
|
assert.equal(resolveAlias("@/shared", ""), null);
|
|
});
|
|
|
|
it("rejects @// escape attempts (absolute path injection)", () => {
|
|
// `@//etc/passwd` must NOT become `<root>/src//etc/passwd` — after the
|
|
// path-join normalisation it would still be `src/etc/passwd` inside the
|
|
// root, which is fine, but we explicitly bail out so the hook never
|
|
// silently rewrites a malformed specifier.
|
|
assert.equal(resolveAlias("@//etc/passwd", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("@/\\etc/passwd", REPO_ROOT), null);
|
|
});
|
|
|
|
it("rejects @/../ path-traversal escapes outside <root>/src", () => {
|
|
// `@/../../../etc/hostname` must NOT escape <root>/src onto the real
|
|
// filesystem — even though it does not start with `/` or `\`, `join()`
|
|
// would otherwise normalize it to a path outside the intended root.
|
|
assert.equal(resolveAlias("@/../../../etc/hostname", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("@/..", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("@/foo/../../bar", REPO_ROOT), null);
|
|
// A `..` segment that stays inside <root>/src after normalization is
|
|
// still rejected — the guard is a literal segment check, not just a
|
|
// containment check, so it fails closed even in ambiguous cases.
|
|
assert.equal(resolveAlias("@/shared/../shared/utils/featureFlags", REPO_ROOT), null);
|
|
});
|
|
|
|
it("preserves an explicit .ts extension if the file exists", () => {
|
|
// `@/shared/utils/featureFlags.ts` exists on disk → resolves to it
|
|
const got = resolveAlias("@/shared/utils/featureFlags.ts", REPO_ROOT);
|
|
assert.ok(got);
|
|
assert.ok(fileURLToPath(got).endsWith("featureFlags.ts"));
|
|
});
|
|
|
|
it("probes source extensions when the specifier has none (#7791 core)", () => {
|
|
// `@/lib/db/core` → `<root>/src/lib/db/core.ts` (file on disk has .ts)
|
|
const got = resolveAlias("@/lib/db/core", REPO_ROOT);
|
|
assert.ok(got, "expected non-null URL for @/lib/db/core");
|
|
const fsPath = fileURLToPath(got);
|
|
assert.ok(
|
|
fsPath.endsWith(join("src", "lib", "db", "core.ts")),
|
|
`expected <root>/src/lib/db/core.ts, got ${fsPath}`
|
|
);
|
|
});
|
|
|
|
it("resolves directory imports to <dir>/index.*", () => {
|
|
// Pick a real directory that has an index file in src/
|
|
// `@/shared` → src/shared/index.ts if it exists, else null.
|
|
const got = resolveAlias("@/shared", REPO_ROOT);
|
|
// We don't assert non-null (depends on repo layout) but the call must not
|
|
// throw and must return either null or a file URL.
|
|
if (got !== null) {
|
|
assert.ok(got.startsWith("file://"));
|
|
}
|
|
});
|
|
|
|
it("returns null when no candidate file exists on disk", () => {
|
|
// Real root, but specifier points at a non-existent path
|
|
assert.equal(resolveAlias("@/does/not/exist", REPO_ROOT), null);
|
|
});
|
|
});
|
|
|
|
describe("aliasResolver.resolveAlias — @omniroute/open-sse aliases", () => {
|
|
it("exposes ALIAS_MAP with three entries matching tsconfig paths", () => {
|
|
assert.equal(ALIAS_MAP.length, 3, "must have 3 alias entries");
|
|
// @/
|
|
assert.equal(ALIAS_MAP[0].prefix, "@/");
|
|
assert.equal(ALIAS_MAP[0].target, "src");
|
|
assert.equal(ALIAS_MAP[0].exact, false);
|
|
// @omniroute/open-sse/ (subpath)
|
|
assert.equal(ALIAS_MAP[1].prefix, "@omniroute/open-sse/");
|
|
assert.equal(ALIAS_MAP[1].target, "open-sse");
|
|
assert.equal(ALIAS_MAP[1].exact, false);
|
|
// @omniroute/open-sse (exact package name)
|
|
assert.equal(ALIAS_MAP[2].prefix, "@omniroute/open-sse");
|
|
assert.equal(ALIAS_MAP[2].target, "open-sse");
|
|
assert.equal(ALIAS_MAP[2].exact, true);
|
|
});
|
|
|
|
it("resolves @omniroute/open-sse (bare) to open-sse/index.ts", () => {
|
|
const got = resolveAlias("@omniroute/open-sse", REPO_ROOT);
|
|
assert.ok(got, "expected non-null URL for @omniroute/open-sse");
|
|
assert.ok(got.startsWith("file://"), "must be a file URL");
|
|
const fsPath = fileURLToPath(got);
|
|
assert.ok(
|
|
fsPath.endsWith(join("open-sse", "index.ts")),
|
|
`expected <root>/open-sse/index.ts, got ${fsPath}`
|
|
);
|
|
});
|
|
|
|
it("resolves @omniroute/open-sse/services/usage to open-sse/services/usage.ts", () => {
|
|
const got = resolveAlias("@omniroute/open-sse/services/usage", REPO_ROOT);
|
|
assert.ok(got, "expected non-null URL");
|
|
const fsPath = fileURLToPath(got);
|
|
assert.ok(
|
|
fsPath.endsWith(join("open-sse", "services", "usage.ts")),
|
|
`expected <root>/open-sse/services/usage.ts, got ${fsPath}`
|
|
);
|
|
});
|
|
|
|
it("resolves @omniroute/open-sse/utils/proxyFetch to open-sse/utils/proxyFetch.ts", () => {
|
|
const got = resolveAlias("@omniroute/open-sse/utils/proxyFetch", REPO_ROOT);
|
|
assert.ok(got, "expected non-null URL");
|
|
const fsPath = fileURLToPath(got);
|
|
assert.ok(
|
|
fsPath.endsWith(join("open-sse", "utils", "proxyFetch.ts")),
|
|
`expected <root>/open-sse/utils/proxyFetch.ts, got ${fsPath}`
|
|
);
|
|
});
|
|
|
|
it("returns null for non-existent @omniroute/open-sse/* paths", () => {
|
|
assert.equal(resolveAlias("@omniroute/open-sse/does/not/exist", REPO_ROOT), null);
|
|
});
|
|
|
|
it("returns null for @omniroute/other (unmatched scope)", () => {
|
|
assert.equal(resolveAlias("@omniroute/other", REPO_ROOT), null);
|
|
assert.equal(resolveAlias("@omniroute/other/pkg", REPO_ROOT), null);
|
|
});
|
|
|
|
it("rejects path-traversal via @omniroute/open-sse/../../etc/passwd", () => {
|
|
assert.equal(resolveAlias("@omniroute/open-sse/../../etc/passwd", REPO_ROOT), null);
|
|
});
|
|
});
|
|
|
|
describe("aliasResolver.registerAliasResolver", () => {
|
|
it("returns false when <root>/src does not exist (no-op, does not throw)", async () => {
|
|
const tmp = mkdtempSync(join(tmpdir(), "alias-resolver-no-src-"));
|
|
try {
|
|
const ok = await registerAliasResolver(tmp);
|
|
assert.equal(ok, false, "must return false when there is no src/ dir");
|
|
} finally {
|
|
rmSync(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
});
|
|
|
|
it("returns true and registers when <root>/src exists", async () => {
|
|
const tmp = mkdtempSync(join(tmpdir(), "alias-resolver-with-src-"));
|
|
try {
|
|
mkdirSync(join(tmp, "src"), { recursive: true });
|
|
const ok = await registerAliasResolver(tmp);
|
|
assert.equal(ok, true, "must register when src/ exists");
|
|
} finally {
|
|
rmSync(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
});
|
|
|
|
it("throws TypeError when root is empty or non-string", async () => {
|
|
// Empty string triggers the guard; null/undefined are coerced by the
|
|
// function signature (no default), so they also reach the guard and
|
|
// reject with TypeError. (null is falsy → `!root` true; undefined same.)
|
|
await assert.rejects(() => registerAliasResolver(""), TypeError);
|
|
await assert.rejects(() => registerAliasResolver(null), TypeError);
|
|
await assert.rejects(() => registerAliasResolver(undefined), TypeError);
|
|
// Numbers/objects are explicitly rejected by the typeof check too.
|
|
await assert.rejects(() => registerAliasResolver(123), TypeError);
|
|
await assert.rejects(() => registerAliasResolver({}), TypeError);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* End-to-end regression for issue #7791: spawning a child Node process that
|
|
* mimics a global install (no tsconfig.json in CWD, no parent package.json)
|
|
* and confirms `@/...` specifiers resolve via the hook.
|
|
*
|
|
* This is a separate process because module.register() installs a process-
|
|
* wide hook and we do not want to pollute the test runner's loader.
|
|
*/
|
|
describe("aliasResolver end-to-end (#7791 regression)", () => {
|
|
function runChild(script) {
|
|
const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
|
|
cwd: REPO_ROOT,
|
|
env: {
|
|
...process.env,
|
|
// Force a clean DATA_DIR so loadEnvFile() does not pick up dev .env
|
|
DATA_DIR: mkdtempSync(join(tmpdir(), "alias-resolver-e2e-")),
|
|
OMNIROUTE_CLI_SKIP_REPO_ENV: "1",
|
|
},
|
|
encoding: "utf8",
|
|
});
|
|
return { stdout: result.stdout, stderr: result.stderr, status: result.status };
|
|
}
|
|
|
|
it("resolves @/shared/network/outboundUrlGuard from a child process", () => {
|
|
// Reproduces the exact import chain that setup-opencode triggers.
|
|
// Before #7791 fix: `Cannot find package '@/shared'`.
|
|
// After fix: imports succeed and the module exports are reachable.
|
|
//
|
|
// The child process registers tsx/esm *before* the alias resolver: the
|
|
// loaded module's transitive relative imports (e.g. `./core` without an
|
|
// extension) need tsx to resolve, and tsx must be installed in the loader
|
|
// before any dynamic import() of a .ts file. This mirrors what
|
|
// bin/omniroute.mjs does (await import("tsx/esm") first, then register).
|
|
const script = `
|
|
await import("tsx/esm");
|
|
import { join } from "node:path";
|
|
import { registerAliasResolver } from ${JSON.stringify(repoFileUrl("bin/aliasResolver.mjs"))};
|
|
const ok = await registerAliasResolver(${JSON.stringify(REPO_ROOT)});
|
|
if (!ok) { console.error("FAIL: registerAliasResolver returned false"); process.exit(2); }
|
|
try {
|
|
const m = await import(${JSON.stringify(repoFileUrl("src/shared/network/outboundUrlGuard.ts"))});
|
|
const keys = Object.keys(m).sort().join(",");
|
|
console.log("OK:" + keys);
|
|
} catch (err) {
|
|
console.error("FAIL:" + (err && err.message || err));
|
|
process.exit(3);
|
|
}
|
|
`;
|
|
const { stdout, stderr, status } = runChild(script);
|
|
assert.equal(status, 0, `expected exit 0, got ${status}. stderr=${stderr.slice(0, 500)}`);
|
|
const trimmed = stdout.trim();
|
|
assert.match(trimmed, /^OK:/, `expected OK:<exports>, got: ${trimmed}`);
|
|
// Sanity: the module must actually export the documented symbols
|
|
assert.match(
|
|
trimmed,
|
|
/OutboundUrlGuardError|PROVIDER_URL_BLOCKED_MESSAGE/,
|
|
`unexpected exports: ${trimmed}`
|
|
);
|
|
});
|
|
|
|
it("does not interfere with bare/relative specifiers (regression guard)", () => {
|
|
const script = `
|
|
import { registerAliasResolver } from ${JSON.stringify(repoFileUrl("bin/aliasResolver.mjs"))};
|
|
await registerAliasResolver(${JSON.stringify(REPO_ROOT)});
|
|
// node:fs must still resolve via the default resolver
|
|
const fs = await import("node:fs");
|
|
console.log("OK:" + typeof fs.readFile);
|
|
`;
|
|
const { stdout, status, stderr } = runChild(script);
|
|
assert.equal(status, 0, `node:fs broke: stderr=${stderr.slice(0, 500)}`);
|
|
// Node's console.log appends a newline; trim before matching the anchored regex.
|
|
assert.match(stdout.trim(), /^OK:function$/);
|
|
});
|
|
});
|