mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-16 10:28:45 +00:00
test: cover kysely guardrails
This commit is contained in:
@@ -65,6 +65,9 @@ const rawSqliteAllowPathGroups = {
|
||||
const rawSqliteAllowPathReasons = new Map();
|
||||
for (const [reason, paths] of Object.entries(rawSqliteAllowPathGroups)) {
|
||||
for (const allowedPath of paths) {
|
||||
if (rawSqliteAllowPathReasons.has(allowedPath)) {
|
||||
throw new Error(`Duplicate raw SQLite allowlist path: ${allowedPath}`);
|
||||
}
|
||||
rawSqliteAllowPathReasons.set(allowedPath, reason);
|
||||
}
|
||||
}
|
||||
@@ -187,16 +190,30 @@ function isLikelySqliteReceiver(expression) {
|
||||
return ts.isPropertyAccessExpression(unwrapped) && getPropertyNameText(unwrapped.name) === "db";
|
||||
}
|
||||
|
||||
function isPersistedRowExpression(expression) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isPropertyAccessExpression(unwrapped)) {
|
||||
const owner = unwrapExpression(unwrapped.expression);
|
||||
return ts.isIdentifier(owner) && /^(?:row|record|entry)$/u.test(owner.text);
|
||||
}
|
||||
if (ts.isElementAccessExpression(unwrapped)) {
|
||||
const owner = unwrapExpression(unwrapped.expression);
|
||||
return ts.isIdentifier(owner) && /^(?:row|record|entry)$/u.test(owner.text);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPersistedStringCastType(typeText) {
|
||||
return [
|
||||
/\bTaskRecord\["(?:runtime|scopeKind|status|deliveryStatus|notifyPolicy|terminalOutcome)"\]/u,
|
||||
/\bTaskFlowRecord\["(?:status|notifyPolicy)"\]/u,
|
||||
/\bTaskFlowSyncMode\b/u,
|
||||
/\bVirtualAgentFsEntryKind\b/u,
|
||||
/\b[A-Z][A-Za-z0-9]*(?:Status|Kind|Mode|Policy|Runtime|Outcome)\b/u,
|
||||
].some((pattern) => pattern.test(typeText));
|
||||
}
|
||||
|
||||
function collectKyselyGuardrailViolations(content, relativePath) {
|
||||
export function collectKyselyGuardrailViolations(content, relativePath) {
|
||||
const sourceFile = ts.createSourceFile(relativePath, content, ts.ScriptTarget.Latest, true);
|
||||
const imports = collectImports(sourceFile);
|
||||
const violations = [];
|
||||
@@ -206,6 +223,7 @@ function collectKyselyGuardrailViolations(content, relativePath) {
|
||||
isSqliteStorePath(relativePath) &&
|
||||
(ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) &&
|
||||
isPersistedStringCastType(node.type.getText(sourceFile)) &&
|
||||
isPersistedRowExpression(node.expression) &&
|
||||
!hasAllowComment(sourceFile, node, "sqlite-allow-persisted-cast")
|
||||
) {
|
||||
addViolation(
|
||||
|
||||
@@ -324,6 +324,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["scripts/changed-lanes.mjs", ["test/scripts/changed-lanes.test.ts"]],
|
||||
["scripts/check-changed.mjs", ["test/scripts/changed-lanes.test.ts"]],
|
||||
["scripts/check-deadcode-unused-files.mjs", ["test/scripts/check-deadcode-unused-files.test.ts"]],
|
||||
["scripts/check-kysely-guardrails.mjs", ["test/scripts/check-kysely-guardrails.test.ts"]],
|
||||
[
|
||||
"scripts/deadcode-unused-files.allowlist.mjs",
|
||||
["test/scripts/check-deadcode-unused-files.test.ts"],
|
||||
|
||||
94
test/scripts/check-kysely-guardrails.test.ts
Normal file
94
test/scripts/check-kysely-guardrails.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectKyselyGuardrailViolations } from "../../scripts/check-kysely-guardrails.mjs";
|
||||
|
||||
function messagesFor(content: string, relativePath = "src/example/store.sqlite.ts"): string[] {
|
||||
return collectKyselyGuardrailViolations(content, relativePath).map(
|
||||
(violation) => violation.message,
|
||||
);
|
||||
}
|
||||
|
||||
describe("Kysely guardrails", () => {
|
||||
it("rejects explicit sync-helper row generics for builder queries", () => {
|
||||
expect(
|
||||
messagesFor(`
|
||||
import { executeSqliteQuerySync } from "../infra/kysely-sync.js";
|
||||
|
||||
executeSqliteQuerySync<{ id: string }>(db, query);
|
||||
`),
|
||||
).toContain("sync helper row generic at call site; let Kysely infer builder result rows");
|
||||
});
|
||||
|
||||
it("rejects persisted row casts to enum-like types in SQLite stores", () => {
|
||||
expect(
|
||||
messagesFor(`
|
||||
type TaskStatus = "running" | "succeeded";
|
||||
|
||||
function rowToRecord(row: { status: string }) {
|
||||
return {
|
||||
status: row.status as TaskStatus,
|
||||
};
|
||||
}
|
||||
`),
|
||||
).toContain(
|
||||
"persisted SQLite enum-like values must be parsed through closed validators, not cast",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows explicit local escape hatches for reviewed persisted casts", () => {
|
||||
expect(
|
||||
messagesFor(`
|
||||
type TaskStatus = "running" | "succeeded";
|
||||
|
||||
function rowToRecord(row: { status: string }) {
|
||||
return {
|
||||
status: row.status as TaskStatus, // sqlite-allow-persisted-cast
|
||||
};
|
||||
}
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects typed raw SQL outside allowlisted boundaries", () => {
|
||||
expect(
|
||||
messagesFor(
|
||||
`
|
||||
import { sql } from "kysely";
|
||||
|
||||
const count = sql<number>\`COUNT(*)\`;
|
||||
`,
|
||||
"src/example/report.ts",
|
||||
),
|
||||
).toContain("typed raw sql snippet needs a small helper or allowlisted boundary");
|
||||
});
|
||||
|
||||
it("rejects direct raw node:sqlite prepare in new production files", () => {
|
||||
expect(
|
||||
messagesFor(
|
||||
`
|
||||
import { requireNodeSqlite } from "../infra/node-sqlite.js";
|
||||
|
||||
const sqlite = requireNodeSqlite();
|
||||
const db = new sqlite.DatabaseSync(":memory:");
|
||||
db.prepare("select 1").get();
|
||||
`,
|
||||
"src/example/raw-store.ts",
|
||||
),
|
||||
).toContain(
|
||||
"new raw node:sqlite access requires Kysely or an explicit raw SQLite allowlist entry",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps ordinary static Kysely reference strings valid", () => {
|
||||
expect(
|
||||
messagesFor(`
|
||||
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
|
||||
|
||||
const query = getNodeSqliteKysely<{ task_runs: { task_id: string } }>(db)
|
||||
.selectFrom("task_runs")
|
||||
.select(["task_id"])
|
||||
.where("task_id", "=", taskId);
|
||||
executeSqliteQuerySync(db, query);
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user