createSynastryFullReport counted today's reports with .gte(), which the self-hosted client did not implement; every create threw before any credit was reserved and surfaced as a generic 500 (BUG-1399). The synastry create path now uses the shared DataQueryBuilder type, and a scan test requires every builder method used in src to exist on the self-hosted client. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
68 lines
3.2 KiB
TypeScript
68 lines
3.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
// BUG-1399: a feature called `.gte()` on the self-hosted database client, which
|
|
// did not implement it. Unit tests passed because they used a hand-written query
|
|
// stub that had the method. This test reads every PostgREST-style filter or
|
|
// modifier call in frontend/src and requires the self-hosted builder to declare it.
|
|
const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const srcDir = path.join(frontendDir, "src");
|
|
|
|
const POSTGREST_METHODS = [
|
|
"eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "is", "in", "not", "or",
|
|
"contains", "containedBy", "overlaps", "range", "rangeGt", "rangeGte", "rangeLt",
|
|
"rangeLte", "rangeAdjacent", "textSearch", "match", "filter", "order", "limit",
|
|
"single", "maybeSingle", "csv", "abortSignal", "returns", "throwOnError",
|
|
] as const;
|
|
|
|
function sourceFiles(dir: string): string[] {
|
|
return readdirSync(dir).flatMap((name) => {
|
|
const full = path.join(dir, name);
|
|
if (statSync(full).isDirectory()) return sourceFiles(full);
|
|
return /\.(ts|tsx)$/.test(name) ? [full] : [];
|
|
});
|
|
}
|
|
|
|
function builderMethods(): Set<string> {
|
|
const source = readFileSync(path.join(srcDir, "lib/db/local-postgres-client-core.ts"), "utf8");
|
|
const start = source.indexOf("class LocalPostgresQueryBuilder");
|
|
const end = source.indexOf("\nexport class LocalPostgresDataClient", start);
|
|
assert.ok(start >= 0 && end > start, "LocalPostgresQueryBuilder class not found");
|
|
return new Set([...source.slice(start, end).matchAll(/^ {2}(?:async )?([a-zA-Z]+)\(/gm)].map((match) => match[1]));
|
|
}
|
|
|
|
// Lines that are clearly not database query chains: zod schemas and plain arrays.
|
|
// `.contains(` is left out of the scan: DOM `element.contains()` uses the same name.
|
|
function isDatabaseChain(line: string): boolean {
|
|
if (/\bz\.|\.number\(\)|\.string\(\)|\.array\(|\.date\(\)/.test(line)) return false;
|
|
return true;
|
|
}
|
|
|
|
test("every PostgREST method used in frontend/src exists on the self-hosted query builder", () => {
|
|
const implemented = builderMethods();
|
|
const missing: string[] = [];
|
|
const builderOnly = new Set<string>(["gte", "lt", "lte", "gt", "neq", "ilike", "maybeSingle", "containedBy", "overlaps", "rangeGt", "rangeGte", "rangeLt", "rangeLte", "rangeAdjacent", "textSearch", "csv", "throwOnError"]);
|
|
for (const file of sourceFiles(srcDir)) {
|
|
const lines = readFileSync(file, "utf8").split("\n");
|
|
lines.forEach((line, index) => {
|
|
if (!isDatabaseChain(line)) return;
|
|
for (const method of POSTGREST_METHODS) {
|
|
if (!builderOnly.has(method)) continue;
|
|
if (new RegExp(`\\.${method}\\(`).test(line) && !implemented.has(method)) {
|
|
missing.push(`${path.relative(frontendDir, file)}:${index + 1} .${method}()`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
assert.deepEqual(missing, [], `self-hosted builder lacks methods used in src:\n${missing.join("\n")}`);
|
|
});
|
|
|
|
test("the self-hosted builder declares gte and lt (BUG-1399)", () => {
|
|
const implemented = builderMethods();
|
|
assert.ok(implemented.has("gte"));
|
|
assert.ok(implemented.has("lt"));
|
|
});
|