Files
fusion/scripts/__tests__/check-no-getdatabase.test.mjs
gsxdsm 06d03d4e1f FN-8103: enforce PostgreSQL-only production data access
Require production paths to use PostgreSQL-aware stores and prevent new unrestricted database access.

- Add a checked allowlist that bans production getDatabase() calls by default.
- Route Quality plugin persistence through an async PostgreSQL-aware store and add Drizzle ORM.
- Document backend-safe plugin storage patterns and cover guarded access behavior.

Files changed:
 docs/PLUGIN_AUTHORING.md                           |  20 +++
 package.json                                       |   6 +-
 .../src/__tests__/agent-logs-backend-mode.test.ts  |   7 +
 packages/core/src/store.ts                         |   7 +-
 packages/core/src/task-store/remaining-ops-5.ts    |   8 +-
 plugins/fusion-plugin-quality/package.json         |   1 +
 .../src/__tests__/async-quality-store.pg.test.ts   |  36 +++++
 .../src/__tests__/cancel-and-plans.test.ts         |   8 +-
 .../src/__tests__/experimental-gate.test.ts        |   1 +
 .../src/routes/create-routes.ts                    |  50 +++----
 .../src/runner/command-runner.ts                   |  17 ++-
 .../src/store/async-quality-store.ts               |  34 +++++
 pnpm-lock.yaml                                     |   3 +
 scripts/__tests__/check-no-getdatabase.test.mjs    |  90 ++++++++++++
 scripts/check-no-getdatabase.mjs                   | 159 +++++++++++++++++++++
 scripts/lib/getdatabase-allowlist.json             |  18 +++
 16 files changed, 422 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-8103
Fusion-Task-Lineage: ff17bcb2-5341-4c6c-a5c4-993580539676
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 10:13:49 -07:00

91 lines
4.3 KiB
JavaScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
scanFileContent,
scanTrackedFiles,
validateAllowlistEntries,
} from "../check-no-getdatabase.mjs";
const file = "plugins/example/src/route.ts";
const line = " return ctx.taskStore.getDatabase();";
const pinned = { file, line: 2, snippet: line.trim(), reason: "FN-9999: temporary backend-guarded shim; remove during migration.", allowlistedAt: "2026-07-16" };
function invocations(content, entries = []) {
return scanFileContent(content, file, { allowlistEntries: entries }).filter((match) => match.type === "invocation");
}
describe("check-no-getdatabase", () => {
it("flags executable invocations and exempts only the exact pinned occurrence", () => {
const content = `const ignored = 1;\n${line}`;
assert.equal(invocations(content).length, 1);
assert.equal(scanFileContent(content, file, { allowlistEntries: [pinned] }).length, 0);
});
it("does not turn an invocation pin into a file-level exemption", () => {
const content = `const ignored = 1;\n${line}\n return ctx.taskStore.getDatabase();`;
assert.equal(invocations(content, [pinned]).length, 1);
});
it("does not exempt identical source text on a different line", () => {
const content = `const ignored = 1;\n${line}\n${line}`;
assert.equal(invocations(content, [pinned]).length, 1);
});
it("flags a moved call and reports the former pin as stale", () => {
const content = `\n\n${line}`;
const matches = scanFileContent(content, file, { allowlistEntries: [pinned] });
assert.equal(matches.filter((match) => match.type === "invocation").length, 1);
assert.equal(matches.filter((match) => match.type === "stale-allowlist").length, 1);
});
it("fails stale allowlist fingerprints", () => {
const content = `const ignored = 1;\n return ctx.taskStore.getDatabase ( );`;
const matches = scanFileContent(content, file, { allowlistEntries: [pinned] });
assert.equal(matches.some((match) => match.type === "stale-allowlist"), true);
assert.equal(matches.some((match) => match.type === "invocation"), true);
});
it("ignores comments, strings, literal templates, declarations, and near-misses", () => {
const content = [
"// getDatabase()",
"/** getDatabase( */",
"const quoted = 'getDatabase('; const double = \"call getDatabase()\";",
"const literal = `call getDatabase() only`;",
"getDatabase(): Database { return database; }",
"public getDatabase(): Database { return database; }",
"async getDatabase ( ) { return database; }",
"interface Store { getDatabase(): Database; }",
"const testDouble = { getDatabase() { return database; } };",
"getDatabaseHealth(); getDatabasePath(); refreshDatabaseHealth();",
].join("\n");
assert.deepEqual(invocations(content), []);
});
it("scans template interpolation expressions but not template literal text", () => {
assert.equal(invocations("const text = `literal getDatabase()`;").length, 0);
assert.equal(invocations("const text = `${ctx.taskStore.getDatabase()}`;").length, 1);
assert.equal(invocations("const text = `${`nested ${ctx.taskStore.getDatabase()}`}`;").length, 1);
});
it("allows explicit backend-guarded legacy pins", () => {
const core = "packages/core/src/store.ts";
const legacyLine = " await this.getDatabase().runPluginSchemaInits(";
const entry = { file: core, line: 1, snippet: legacyLine.trim(), reason: "FN-8104: remove legacy SQLite fallback.", allowlistedAt: "2026-07-16" };
assert.deepEqual(scanFileContent(legacyLine, core, { allowlistEntries: [entry] }), []);
});
it("skips deleted tracked files and rethrows non-ENOENT read failures", () => {
const matches = scanTrackedFiles(["deleted.ts"], { allowlistEntries: [], readFile: () => { const error = new Error("gone"); error.code = "ENOENT"; throw error; } });
assert.deepEqual(matches, []);
assert.throws(() => scanTrackedFiles(["denied.ts"], { allowlistEntries: [], readFile: () => { throw new Error("denied"); } }), /denied/);
});
it("validates every required allowlist field", () => {
for (const key of ["file", "line", "snippet", "reason", "allowlistedAt"]) {
const candidate = { ...pinned };
delete candidate[key];
assert.throws(() => validateAllowlistEntries([candidate]));
}
});
});