mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
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.
504 lines
16 KiB
TypeScript
504 lines
16 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
// ── Temp dirs ──
|
|
// IMPORTANT: DATA_DIR must be set BEFORE importing any module that imports core.ts,
|
|
// because core.ts evaluates DATA_DIR at module load time. All imports of DB-touching
|
|
// modules must be dynamic (after this line) to ensure the temp DB is used.
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-plugins-edge-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const dbPlugins = await import("../../src/lib/db/plugins.ts");
|
|
const { scanPluginDir } = await import("../../src/lib/plugins/scanner.ts");
|
|
const { pluginManager } = await import("../../src/lib/plugins/manager.ts");
|
|
const { registerHook, unregisterHooks, emitHook, emitHookBlocking, resetHooks, getHooks } =
|
|
await import("../../src/lib/plugins/hooks.ts");
|
|
|
|
const activeSourceDirs: string[] = [];
|
|
|
|
function cleanupSourceDirs() {
|
|
for (const dir of activeSourceDirs) {
|
|
try {
|
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
} catch {}
|
|
}
|
|
activeSourceDirs.length = 0;
|
|
}
|
|
|
|
function writeTestPlugin(opts: {
|
|
name: string;
|
|
onRequest?: boolean;
|
|
onResponse?: boolean;
|
|
onError?: boolean;
|
|
configSchema?: Record<string, unknown>;
|
|
indexJs?: string;
|
|
}) {
|
|
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-edge-src-"));
|
|
const pluginDir = path.join(sourceDir, opts.name);
|
|
fs.mkdirSync(pluginDir, { recursive: true });
|
|
|
|
const manifest: Record<string, unknown> = {
|
|
name: opts.name,
|
|
version: "1.0.0",
|
|
description: "Edge test plugin",
|
|
main: "index.js",
|
|
hooks: {
|
|
onRequest: opts.onRequest ?? false,
|
|
onResponse: opts.onResponse ?? false,
|
|
onError: opts.onError ?? false,
|
|
},
|
|
enabledByDefault: false,
|
|
requires: { permissions: [] },
|
|
};
|
|
if (opts.configSchema) manifest.configSchema = opts.configSchema;
|
|
|
|
fs.writeFileSync(path.join(pluginDir, "plugin.json"), JSON.stringify(manifest, null, 2));
|
|
|
|
let indexJs = opts.indexJs;
|
|
if (!indexJs) {
|
|
const handlers: string[] = [];
|
|
if (opts.onRequest)
|
|
handlers.push(
|
|
`onRequest: function(ctx) { ctx.metadata = ctx.metadata || {}; ctx.metadata.hookCalled = true; }`
|
|
);
|
|
if (opts.onResponse) handlers.push(`onResponse: function(ctx, resp) { return resp; }`);
|
|
if (opts.onError) handlers.push(`onError: function(ctx, err) {}`);
|
|
indexJs =
|
|
handlers.length > 0 ? `module.exports = { ${handlers.join(", ")} };` : `module.exports = {};`;
|
|
}
|
|
fs.writeFileSync(path.join(pluginDir, "index.js"), indexJs);
|
|
|
|
activeSourceDirs.push(sourceDir);
|
|
return { sourceDir, pluginDir, name: opts.name };
|
|
}
|
|
|
|
// ── Lifecycle ──
|
|
|
|
test.beforeEach(() => {
|
|
core.resetDbInstance();
|
|
// Clean all existing plugins to prevent UNIQUE constraint failures.
|
|
// Wrapped in try-catch: when running alongside other test files that load core
|
|
// before DATA_DIR is set, the SQLITE_FILE may point to the production DB which
|
|
// may not have the plugins table yet (migration 076 renumbered). The cleanup
|
|
// is redundant anyway — resetDbInstance() already invalidates the instance,
|
|
// and rmSync/mkdirSync below gives us a fresh DATA_DIR for next getDbInstance().
|
|
try {
|
|
for (const p of dbPlugins.listPlugins()) {
|
|
dbPlugins.deletePlugin(p.name);
|
|
}
|
|
} catch {
|
|
// Production DB may not have the plugins table — ignore; fresh DB created below.
|
|
}
|
|
resetHooks();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
cleanupSourceDirs();
|
|
});
|
|
|
|
test.after(() => {
|
|
core.resetDbInstance();
|
|
cleanupSourceDirs();
|
|
try {
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
} catch {}
|
|
});
|
|
|
|
// ══════════════════════════════════════════
|
|
// Scanner edge cases
|
|
// ══════════════════════════════════════════
|
|
|
|
test("scanner: empty directory returns empty results", async () => {
|
|
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-scan-empty-"));
|
|
activeSourceDirs.push(emptyDir);
|
|
const result = await scanPluginDir(emptyDir);
|
|
assert.deepEqual(result.plugins, []);
|
|
assert.deepEqual(result.errors, []);
|
|
});
|
|
|
|
test("scanner: directory with no manifest reports error", async () => {
|
|
const noManifestDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-scan-nomanifest-"));
|
|
const pluginDir = path.join(noManifestDir, "some-plugin");
|
|
fs.mkdirSync(pluginDir);
|
|
fs.writeFileSync(path.join(pluginDir, "index.js"), "module.exports = {};");
|
|
activeSourceDirs.push(noManifestDir);
|
|
|
|
const result = await scanPluginDir(noManifestDir);
|
|
assert.equal(result.plugins.length, 0);
|
|
assert.equal(result.errors.length, 1);
|
|
assert.ok(result.errors[0].error.includes("no plugin.json"));
|
|
});
|
|
|
|
test("scanner: invalid JSON manifest reports error", async () => {
|
|
const badDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-scan-badjson-"));
|
|
const pluginDir = path.join(badDir, "bad-json");
|
|
fs.mkdirSync(pluginDir);
|
|
fs.writeFileSync(path.join(pluginDir, "plugin.json"), "{invalid json!!!}}}");
|
|
fs.writeFileSync(path.join(pluginDir, "index.js"), "module.exports = {};");
|
|
activeSourceDirs.push(badDir);
|
|
|
|
const result = await scanPluginDir(badDir);
|
|
assert.equal(result.plugins.length, 0);
|
|
assert.equal(result.errors.length, 1);
|
|
assert.ok(
|
|
result.errors[0].error.includes("invalid manifest") || result.errors[0].error.includes("JSON")
|
|
);
|
|
});
|
|
|
|
test("scanner: missing required fields reports error", async () => {
|
|
const missingDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-scan-missing-"));
|
|
const pluginDir = path.join(missingDir, "missing-fields");
|
|
fs.mkdirSync(pluginDir);
|
|
// Missing version
|
|
fs.writeFileSync(path.join(pluginDir, "plugin.json"), JSON.stringify({ name: "missing-fields" }));
|
|
fs.writeFileSync(path.join(pluginDir, "index.js"), "module.exports = {};");
|
|
activeSourceDirs.push(missingDir);
|
|
|
|
const result = await scanPluginDir(missingDir);
|
|
assert.equal(result.plugins.length, 0);
|
|
assert.equal(result.errors.length, 1);
|
|
assert.ok(result.errors[0].error.includes("invalid manifest"));
|
|
});
|
|
|
|
test("scanner: non-directory entries are skipped", async () => {
|
|
const mixedDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-scan-mixed-"));
|
|
// Create a file (not a directory) that looks like a plugin
|
|
fs.writeFileSync(path.join(mixedDir, "not-a-dir.json"), "{}");
|
|
activeSourceDirs.push(mixedDir);
|
|
|
|
const result = await scanPluginDir(mixedDir);
|
|
assert.equal(result.plugins.length, 0);
|
|
assert.equal(result.errors.length, 0);
|
|
});
|
|
|
|
// ══════════════════════════════════════════
|
|
// Manager edge cases
|
|
// ══════════════════════════════════════════
|
|
|
|
test("manager: install with path traversal throws", async () => {
|
|
await assert.rejects(
|
|
() => pluginManager.install("/tmp/../../../etc"),
|
|
(err: Error) => {
|
|
assert.ok(
|
|
err.message.includes("path traversal") || err.message.includes("No valid plugin found"),
|
|
`Unexpected error: ${err.message}`
|
|
);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
test("manager: install with null bytes in path throws", async () => {
|
|
await assert.rejects(
|
|
() => pluginManager.install("/tmp/test\0malicious"),
|
|
(err: Error) => {
|
|
assert.ok(
|
|
err.message.includes("Invalid") ||
|
|
err.message.includes("null") ||
|
|
err.message.includes("No valid plugin found"),
|
|
`Unexpected error: ${err.message}`
|
|
);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
test("manager: double install same plugin throws", async () => {
|
|
const { sourceDir, name } = writeTestPlugin({ name: "double-install" });
|
|
|
|
await pluginManager.install(sourceDir);
|
|
await assert.rejects(() => pluginManager.install(sourceDir), /already installed/);
|
|
|
|
await pluginManager.uninstall(name);
|
|
});
|
|
|
|
test("manager: deactivate when already inactive is idempotent", async () => {
|
|
const { sourceDir, name } = writeTestPlugin({ name: "deactivate-idempotent" });
|
|
|
|
await pluginManager.install(sourceDir);
|
|
// Plugin starts as "installed", not "active"
|
|
// Deactivating should either succeed silently or throw with a clear message
|
|
try {
|
|
await pluginManager.deactivate(name);
|
|
// If it succeeds, verify status
|
|
const row = dbPlugins.getPluginByName(name);
|
|
assert.ok(row);
|
|
} catch (err: unknown) {
|
|
// If it throws, the message should be clear
|
|
assert.ok(err instanceof Error);
|
|
}
|
|
|
|
await pluginManager.uninstall(name);
|
|
});
|
|
|
|
test("manager: install then uninstall then reinstall works", async () => {
|
|
const { sourceDir, name } = writeTestPlugin({ name: "reinstall-test" });
|
|
|
|
await pluginManager.install(sourceDir);
|
|
await pluginManager.uninstall(name);
|
|
|
|
// Reinstall should work
|
|
const row = await pluginManager.install(sourceDir);
|
|
assert.equal(row.name, name);
|
|
|
|
await pluginManager.uninstall(name);
|
|
});
|
|
|
|
test("manager: activate registers hooks from manifest", async () => {
|
|
const { sourceDir, name } = writeTestPlugin({
|
|
name: "hook-register",
|
|
onRequest: true,
|
|
onResponse: true,
|
|
});
|
|
|
|
await pluginManager.install(sourceDir);
|
|
await pluginManager.activate(name);
|
|
|
|
assert.ok(getHooks("onRequest").find((r) => r.pluginName === name));
|
|
assert.ok(getHooks("onResponse").find((r) => r.pluginName === name));
|
|
assert.equal(
|
|
getHooks("onError").find((r) => r.pluginName === name),
|
|
undefined
|
|
);
|
|
|
|
await pluginManager.uninstall(name);
|
|
});
|
|
|
|
test("manager: deactivate unregisters all hooks", async () => {
|
|
const { sourceDir, name } = writeTestPlugin({
|
|
name: "hook-unregister",
|
|
onRequest: true,
|
|
onResponse: true,
|
|
onError: true,
|
|
});
|
|
|
|
await pluginManager.install(sourceDir);
|
|
await pluginManager.activate(name);
|
|
|
|
assert.ok(getHooks("onRequest").find((r) => r.pluginName === name));
|
|
assert.ok(getHooks("onResponse").find((r) => r.pluginName === name));
|
|
assert.ok(getHooks("onError").find((r) => r.pluginName === name));
|
|
|
|
await pluginManager.deactivate(name);
|
|
|
|
assert.equal(
|
|
getHooks("onRequest").find((r) => r.pluginName === name),
|
|
undefined
|
|
);
|
|
assert.equal(
|
|
getHooks("onResponse").find((r) => r.pluginName === name),
|
|
undefined
|
|
);
|
|
assert.equal(
|
|
getHooks("onError").find((r) => r.pluginName === name),
|
|
undefined
|
|
);
|
|
|
|
await pluginManager.uninstall(name);
|
|
});
|
|
|
|
// ══════════════════════════════════════════
|
|
// Hooks edge cases
|
|
// ══════════════════════════════════════════
|
|
|
|
test("hooks: emitHookBlocking with no handlers returns empty body", async () => {
|
|
const result = await emitHookBlocking("onRequest", { body: {}, metadata: {} });
|
|
assert.deepEqual(result.metadata, {});
|
|
assert.ok(result.blocked === undefined || result.blocked === false);
|
|
});
|
|
|
|
test("hooks: multiple plugins on same event fire in priority order", async () => {
|
|
const order: string[] = [];
|
|
registerHook(
|
|
"onRequest",
|
|
"low",
|
|
() => {
|
|
order.push("low");
|
|
},
|
|
200
|
|
);
|
|
registerHook(
|
|
"onRequest",
|
|
"high",
|
|
() => {
|
|
order.push("high");
|
|
},
|
|
10
|
|
);
|
|
registerHook(
|
|
"onRequest",
|
|
"mid",
|
|
() => {
|
|
order.push("mid");
|
|
},
|
|
100
|
|
);
|
|
|
|
await emitHookBlocking("onRequest", { body: {}, metadata: {} });
|
|
assert.deepEqual(order, ["high", "mid", "low"]);
|
|
});
|
|
|
|
test("hooks: handler that returns undefined does not modify payload", async () => {
|
|
registerHook("onRequest", "noop", () => undefined);
|
|
const result = await emitHookBlocking("onRequest", { body: { original: true }, metadata: {} });
|
|
assert.deepEqual(result.body, { original: true });
|
|
});
|
|
|
|
test("hooks: handler error in emitHookBlocking stops chain", async () => {
|
|
registerHook("onRequest", "bad", () => {
|
|
throw new Error("handler error");
|
|
});
|
|
registerHook("onRequest", "good", () => ({ metadata: { from: "good" } }));
|
|
|
|
// emitHookBlocking should handle the error gracefully
|
|
try {
|
|
const result = await emitHookBlocking("onRequest", { body: {}, metadata: {} });
|
|
// If it returns, good handler may or may not have run
|
|
assert.ok(result !== undefined);
|
|
} catch (err) {
|
|
// If it throws, that's also acceptable behavior
|
|
assert.ok(err instanceof Error);
|
|
}
|
|
});
|
|
|
|
test("hooks: unregister one plugin does not affect others", () => {
|
|
registerHook("onRequest", "keep", () => {});
|
|
registerHook("onRequest", "remove", () => {});
|
|
registerHook("onError", "remove", () => {});
|
|
|
|
assert.equal(getHooks("onRequest").length, 2);
|
|
|
|
unregisterHooks("remove");
|
|
|
|
assert.equal(getHooks("onRequest").length, 1);
|
|
assert.equal(getHooks("onRequest")[0].pluginName, "keep");
|
|
assert.equal(getHooks("onError").length, 0);
|
|
});
|
|
|
|
test("hooks: registerHook with same handler ref is idempotent", () => {
|
|
const handler = () => {};
|
|
registerHook("onRequest", "idempotent", handler);
|
|
registerHook("onRequest", "idempotent", handler);
|
|
|
|
assert.equal(getHooks("onRequest").length, 1);
|
|
assert.equal(getHooks("onRequest")[0].handler, handler);
|
|
});
|
|
|
|
test("hooks: registerHook with different handler refs registers both", () => {
|
|
registerHook("onRequest", "multi", () => {});
|
|
registerHook("onRequest", "multi", () => {});
|
|
|
|
// Different function refs = both registered (hooks module uses reference equality)
|
|
assert.equal(getHooks("onRequest").length, 2);
|
|
});
|
|
|
|
// ══════════════════════════════════════════
|
|
// DB edge cases
|
|
// ══════════════════════════════════════════
|
|
|
|
test("db: updatePluginConfig replaces existing config", () => {
|
|
dbPlugins.insertPlugin({
|
|
id: "merge-test",
|
|
name: "merge-test",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/test",
|
|
manifest: {},
|
|
config: { existing: "value", override: "old" },
|
|
});
|
|
|
|
dbPlugins.updatePluginConfig("merge-test", { override: "new", added: "extra" });
|
|
|
|
const plugin = dbPlugins.getPluginByName("merge-test");
|
|
const config = JSON.parse(plugin!.config);
|
|
// updatePluginConfig replaces, does not merge
|
|
assert.equal(config.existing, undefined);
|
|
assert.equal(config.override, "new");
|
|
assert.equal(config.added, "extra");
|
|
});
|
|
|
|
test("db: listPlugins with no status returns all", () => {
|
|
dbPlugins.insertPlugin({
|
|
id: "p1",
|
|
name: "alpha",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/a",
|
|
manifest: {},
|
|
});
|
|
dbPlugins.insertPlugin({
|
|
id: "p2",
|
|
name: "beta",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/b",
|
|
manifest: {},
|
|
});
|
|
|
|
const all = dbPlugins.listPlugins();
|
|
assert.equal(all.length, 2);
|
|
// Should be sorted by name
|
|
assert.equal(all[0].name, "alpha");
|
|
assert.equal(all[1].name, "beta");
|
|
});
|
|
|
|
test("db: listPlugins with status filters correctly", () => {
|
|
dbPlugins.insertPlugin({
|
|
id: "f1",
|
|
name: "installed-filter",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/f1",
|
|
manifest: {},
|
|
});
|
|
dbPlugins.insertPlugin({
|
|
id: "f2",
|
|
name: "active-filter",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/f2",
|
|
manifest: {},
|
|
});
|
|
dbPlugins.updatePluginStatus("active-filter", "active");
|
|
|
|
const installed = dbPlugins.listPlugins("installed");
|
|
assert.equal(installed.length, 1);
|
|
assert.equal(installed[0].name, "installed-filter");
|
|
|
|
const active = dbPlugins.listPlugins("active");
|
|
assert.equal(active.length, 1);
|
|
assert.equal(active[0].name, "active-filter");
|
|
});
|
|
|
|
test("db: pluginExists returns true/false correctly", () => {
|
|
dbPlugins.insertPlugin({
|
|
id: "exists-test",
|
|
name: "exists-test",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/e",
|
|
manifest: {},
|
|
});
|
|
|
|
assert.equal(dbPlugins.pluginExists("exists-test"), true);
|
|
assert.equal(dbPlugins.pluginExists("nope"), false);
|
|
});
|
|
|
|
test("db: deletePlugin returns true when plugin exists, false when not", () => {
|
|
dbPlugins.insertPlugin({
|
|
id: "del-test",
|
|
name: "del-test",
|
|
version: "1.0.0",
|
|
main: "index.js",
|
|
pluginDir: "/tmp/d",
|
|
manifest: {},
|
|
});
|
|
|
|
assert.equal(dbPlugins.deletePlugin("del-test"), true);
|
|
assert.equal(dbPlugins.deletePlugin("del-test"), false);
|
|
assert.equal(dbPlugins.getPluginByName("del-test"), null);
|
|
});
|