style: format server and shared with prettier

A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
This commit is contained in:
Maurice
2026-10-10 21:23:37 +02:00
parent f4e518b2df
commit 724c88c201
1978 changed files with 78448 additions and 39347 deletions
@@ -13,9 +13,9 @@
* which `run()` below reads back out instead of treating the throw as a test
* failure.
*/
import { describe, it, expect } from 'vitest';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { describe, it, expect } from 'vitest';
const SERVER_ROOT = path.join(__dirname, '../../..');
const SCRIPT = path.join(SERVER_ROOT, 'scripts/db-call-graph.mjs');
@@ -110,10 +110,15 @@ describe('db-call-graph.mjs', () => {
// Non-empty lists exit 1 — that is what makes the CI step fail on a regression.
expect(status).toBe(1);
expect(syncDbMethodCount).toBe(2);
expect(syncDbMethods.map((m) => `${m.class}.${m.method}`).sort()).toEqual(['SyncSample.readSync', 'SyncSample.writeInTx']);
expect(syncDbMethods.map((m) => `${m.class}.${m.method}`).sort()).toEqual([
'SyncSample.readSync',
'SyncSample.writeInTx',
]);
expect(syncDbMethods.every((m) => m.file.endsWith('sync-db-sample.ts') && m.direct)).toBe(true);
expect(transactionSiteCount).toBe(1);
expect(transactionSites).toEqual([expect.objectContaining({ class: 'SyncSample', method: 'writeInTx', receiver: 'this.db' })]);
expect(transactionSites).toEqual([
expect.objectContaining({ class: 'SyncSample', method: 'writeInTx', receiver: 'this.db' }),
]);
// The unawaited fixture beside it stays clean under both flags.
expect(syncDbMethods.some((m) => m.class === 'Sample')).toBe(false);
});
+11 -8
View File
@@ -71,7 +71,7 @@ describe('dialect-lint.mjs', () => {
it.each([
['insert-or', "void 'INSERT OR IGNORE INTO t (a) VALUES (?)';"],
['insert-or', 'void `INSERT OR REPLACE INTO t (a) VALUES (?)`;'],
['datetime', "void \"UPDATE t SET at = datetime('now')\";"],
['datetime', 'void "UPDATE t SET at = datetime(\'now\')";'],
['datetime', "declare function fn(name: string, args: string[]): void;\nfn('julianday', ['now']);"],
['strftime', "void `SELECT strftime('%s', 'now')`;"],
['json', "void 'SELECT json_extract(settings, ?) FROM users';"],
@@ -80,8 +80,8 @@ describe('dialect-lint.mjs', () => {
['last-insert-rowid', "void 'SELECT last_insert_rowid()';"],
['autoincrement', "void 'id INTEGER PRIMARY KEY AUTOINCREMENT';"],
['pragma', "void 'PRAGMA foreign_keys = OFF';"],
['concat', "declare const sql: (s: TemplateStringsArray, ...v: unknown[]) => unknown;\nvoid sql`a || b`;"],
['concat', "void \"SELECT first || ' ' || last FROM users\";"],
['concat', 'declare const sql: (s: TemplateStringsArray, ...v: unknown[]) => unknown;\nvoid sql`a || b`;'],
['concat', 'void "SELECT first || \' \' || last FROM users";'],
['insert-id', 'declare const r: { insertId: bigint };\nvoid Number(r.insertId);'],
['insert-id', 'declare const r: { insertId: bigint };\nconst { insertId } = r;\nvoid insertId;'],
])('DIALECT-002: fails a new %s hit and names the rule', (rule, code) => {
@@ -92,11 +92,11 @@ describe('dialect-lint.mjs', () => {
it('DIALECT-003: leaves comments, ordinary strings and the HTTP Pragma header alone', () => {
const code = [
'// INSERT OR IGNORE INTO t, datetime(\'now\'), PRAGMA foreign_keys',
"// INSERT OR IGNORE INTO t, datetime('now'), PRAGMA foreign_keys",
'/** strftime and json_extract and GLOB and AUTOINCREMENT */',
"declare const res: { set(k: string, v: string): void };",
'declare const res: { set(k: string, v: string): void };',
"res.set('Pragma', 'no-cache');",
"export const fallback = (a: string | null, b: string) => a || b;",
'export const fallback = (a: string | null, b: string) => a || b;',
"export const label = 'one || two';",
'',
].join('\n');
@@ -115,7 +115,8 @@ describe('dialect-lint.mjs', () => {
it('DIALECT-005: a newer migration is held to the DML rules but may spell its own DDL and PRAGMA', () => {
const migration = 'src/db/migrations/Migration20990101000000_probe.ts';
const ddl = "export const t = 'CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT)';\nexport const p = 'PRAGMA foreign_keys = OFF';\n";
const ddl =
"export const t = 'CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT)';\nexport const p = 'PRAGMA foreign_keys = OFF';\n";
expect(run(serverRoot({ [migration]: ddl })).status).toBe(0);
const dml = "export const s = 'INSERT OR IGNORE INTO t (a) VALUES (1)';\n";
@@ -139,7 +140,9 @@ describe('dialect-lint.mjs', () => {
const stale = run(serverRoot({ [REPO]: one }, { [REPO]: { 'insert-or': 2 } }));
expect(stale.status).toBe(1);
expect(stale.out).toContain(`FAIL ${REPO} is held at 2 insert-or hit(s) in scripts/dialect-baseline.json, but has 1 now.`);
expect(stale.out).toContain(
`FAIL ${REPO} is held at 2 insert-or hit(s) in scripts/dialect-baseline.json, but has 1 now.`,
);
expect(stale.out).toContain('npm run lint:dialect -- --update');
});
@@ -66,9 +66,11 @@ describe('the counters', () => {
});
it('ENV-RATCHET-001b: counts a z. call Prettier broke across lines', () => {
expect(countInlineZod(`const s = z
expect(
countInlineZod(`const s = z
.object({ a: idSchema })
.strict();`)).toBe(1);
.strict();`),
).toBe(1);
});
it('ENV-RATCHET-002: counts the result envelope and HTTP in a service, not a 2xx status', () => {
@@ -6,12 +6,18 @@
* settings, so each shape it meets in the real repositories has a pinned
* sample (or a pinned reason it has none).
*/
import {
planRepositories,
readCompilerOptions,
sampleString,
type MethodPlan,
} from '../../../../scripts/pg-probe/arg-samples';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import ts from 'typescript';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { planRepositories, readCompilerOptions, sampleString, type MethodPlan } from '../../../../scripts/pg-probe/arg-samples';
const FIXTURE = `
export interface NewThing {
@@ -71,7 +77,11 @@ beforeAll(() => {
// The server's own settings (non-strict: `string | null` is `string`), so
// the fixture is sampled the way the real repositories are.
const options = readCompilerOptions(path.join(__dirname, '../../../../tsconfig.json'));
plans = planRepositories([path.join(root, 'src/Things.repository.ts')], { ...options, rootDir: root, paths: undefined, types: [] }, root);
plans = planRepositories(
[path.join(root, 'src/Things.repository.ts')],
{ ...options, rootDir: root, paths: undefined, types: [] },
root,
);
});
afterAll(() => {
@@ -8,10 +8,17 @@
* compare (the run fails on it, see report.test.ts), and --update never
* raises or adds.
*/
import {
compareWithBaseline,
countsTowardRatchet,
formatBaseline,
lowerBaseline,
parseBaseline,
} from '../../../../scripts/pg-probe/baseline';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { compareWithBaseline, countsTowardRatchet, formatBaseline, lowerBaseline, parseBaseline } from '../../../../scripts/pg-probe/baseline';
const now = (entries: Record<string, number>): Map<string, number> => new Map(Object.entries(entries));
const none = new Set<string>();
@@ -35,21 +42,31 @@ describe('pg-probe baseline', () => {
expect(() => parseBaseline('{"failing": {"listByUser": 1}}', 'b.json')).toThrow(/not a Class.method key/);
expect(() => parseBaseline('{"failing": {"A.b": 0}}', 'b.json')).toThrow(/expected a positive integer/);
expect(() => parseBaseline('{"failing": {"A.b": 1.5}}', 'b.json')).toThrow(/expected a positive integer/);
expect(() => parseBaseline('{"failing": {}, "uncovered": {}}', 'b.json')).toThrow(/"uncovered" must be null or an array/);
expect(() => parseBaseline('{"failing": {}, "uncovered": ["nodot"]}', 'b.json')).toThrow(/expected a Class.method name/);
expect(() => parseBaseline('{"failing": {}, "uncovered": {}}', 'b.json')).toThrow(
/"uncovered" must be null or an array/,
);
expect(() => parseBaseline('{"failing": {}, "uncovered": ["nodot"]}', 'b.json')).toThrow(
/expected a Class.method name/,
);
expect(() => parseBaseline('{"failing": {}, "uncovered": [1]}', 'b.json')).toThrow(/expected a Class.method name/);
expect(() => parseBaseline('{"failing": {}, "uncovered": ["A.b", "A.b"]}', 'b.json')).toThrow(/lists A.b twice/);
});
it('PGPROBE-003: an unmeasured baseline (either part null) is flagged instead of compared', () => {
const flagged = { unseeded: true, grown: [], stale: [], newlyUncovered: [], nowCovered: [] };
expect(compareWithBaseline({ failing: null, uncovered: null }, now({ 'A.b': 3 }), new Set(['C.d']))).toEqual(flagged);
expect(compareWithBaseline({ failing: null, uncovered: null }, now({ 'A.b': 3 }), new Set(['C.d']))).toEqual(
flagged,
);
expect(compareWithBaseline({ failing: { 'A.b': 1 }, uncovered: null }, now({ 'A.b': 3 }), none)).toEqual(flagged);
expect(compareWithBaseline({ failing: null, uncovered: [] }, now({ 'A.b': 3 }), none)).toEqual(flagged);
});
it('PGPROBE-004: a method failing more statements than its entry (or any without one) fails', () => {
const verdict = compareWithBaseline({ failing: { 'A.b': 1 }, uncovered: [] }, now({ 'A.b': 2, 'C.d': 1, 'E.f': 0 }), none);
const verdict = compareWithBaseline(
{ failing: { 'A.b': 1 }, uncovered: [] },
now({ 'A.b': 2, 'C.d': 1, 'E.f': 0 }),
none,
);
expect(verdict.grown).toEqual([
{ method: 'A.b', allowed: 1, now: 2 },
{ method: 'C.d', allowed: 0, now: 1 },
@@ -58,7 +75,11 @@ describe('pg-probe baseline', () => {
});
it('PGPROBE-005: an entry above what its method fails now is stale, also when the method is gone', () => {
const verdict = compareWithBaseline({ failing: { 'A.b': 2, 'Gone.away': 1 }, uncovered: [] }, now({ 'A.b': 1 }), none);
const verdict = compareWithBaseline(
{ failing: { 'A.b': 2, 'Gone.away': 1 }, uncovered: [] },
now({ 'A.b': 1 }),
none,
);
expect(verdict.grown).toEqual([]);
expect(verdict.stale).toEqual([
{ method: 'A.b', allowed: 2, now: 1 },
@@ -79,7 +100,10 @@ describe('pg-probe baseline', () => {
newlyUncovered: ['New.callback', 'Old.throws'],
nowCovered: ['Gone.away', 'Now.measured'],
});
expect(compareWithBaseline({ failing: {}, uncovered: ['A.b'] }, now({}), new Set(['A.b']))).toMatchObject({ newlyUncovered: [], nowCovered: [] });
expect(compareWithBaseline({ failing: {}, uncovered: ['A.b'] }, now({}), new Set(['A.b']))).toMatchObject({
newlyUncovered: [],
nowCovered: [],
});
});
it('PGPROBE-006: --update lowers and drops entries but never raises or adds one, in either part', () => {
@@ -92,19 +116,25 @@ describe('pg-probe baseline', () => {
});
it('PGPROBE-007: the first --update seeds an unmeasured baseline with what fails and what went unmeasured, sorted', () => {
expect(lowerBaseline({ failing: null, uncovered: null }, now({ 'Z.z': 1, 'A.a': 2, 'M.m': 0 }), new Set(['Y.y', 'B.b']))).toEqual({
expect(
lowerBaseline({ failing: null, uncovered: null }, now({ 'Z.z': 1, 'A.a': 2, 'M.m': 0 }), new Set(['Y.y', 'B.b'])),
).toEqual({
failing: { 'A.a': 2, 'Z.z': 1 },
uncovered: ['B.b', 'Y.y'],
});
expect(lowerBaseline({ failing: { 'A.a': 3 }, uncovered: null }, now({ 'A.a': 2, 'N.n': 1 }), new Set(['B.b']))).toEqual({
expect(
lowerBaseline({ failing: { 'A.a': 3 }, uncovered: null }, now({ 'A.a': 2, 'N.n': 1 }), new Set(['B.b'])),
).toEqual({
failing: { 'A.a': 2 },
uncovered: ['B.b'],
});
});
it('PGPROBE-010: only syntax and feature errors count toward the ratchet, not data errors from the sample values', () => {
for (const code of ['42601', '42883', '42804', '42P01', '42703', '0A000']) expect(countsTowardRatchet(code)).toBe(true);
for (const code of ['22P02', '22007', '22008', '23505', '57014', 'NOSQLSTATE']) expect(countsTowardRatchet(code)).toBe(false);
for (const code of ['42601', '42883', '42804', '42P01', '42703', '0A000'])
expect(countsTowardRatchet(code)).toBe(true);
for (const code of ['22P02', '22007', '22008', '23505', '57014', 'NOSQLSTATE'])
expect(countsTowardRatchet(code)).toBe(false);
});
it('PGPROBE-008: the checked-in baseline parses and formats back to itself', () => {
@@ -6,9 +6,16 @@
* at the Postgres branch and not at the case. The value comparison itself is
* pinned too, since it is what turns a Postgres answer into pass or fail.
*/
import { describe, expect, it } from 'vitest';
import { sqliteHelperEngine } from '../../../../scripts/pg-probe/engines';
import { checkValue, HELPER_CASES, isoSeconds, runHelperCases, type HelperEngine } from '../../../../scripts/pg-probe/helper-cases';
import {
checkValue,
HELPER_CASES,
isoSeconds,
runHelperCases,
type HelperEngine,
} from '../../../../scripts/pg-probe/helper-cases';
import { describe, expect, it } from 'vitest';
const NOW = Date.parse('2026-10-08T12:00:00Z');
@@ -28,7 +35,8 @@ describe('pg-probe helper cases', () => {
it('PGPROBE-051: case names are unique and every case selects through exactly one API', () => {
const names = HELPER_CASES.map((c) => c.name);
expect(new Set(names).size).toBe(names.length);
for (const helperCase of HELPER_CASES) expect(Number(Boolean(helperCase.raw)) + Number(Boolean(helperCase.kysely))).toBe(1);
for (const helperCase of HELPER_CASES)
expect(Number(Boolean(helperCase.raw)) + Number(Boolean(helperCase.kysely))).toBe(1);
});
it('PGPROBE-052: compares values the way both drivers hand them back', () => {
@@ -46,12 +54,16 @@ describe('pg-probe helper cases', () => {
expect(checkValue({ equals: null }, 'x', 'sqlite', NOW)).toBe('expected null, got "x"');
});
it('PGPROBE-053: holds clock values to SQLite\'s text and to the moment of the query', () => {
it("PGPROBE-053: holds clock values to SQLite's text and to the moment of the query", () => {
expect(checkValue({ timestamp: 0 }, '2026-10-08 12:00:30', 'postgres', NOW)).toBeNull();
expect(checkValue({ timestamp: 90_000 }, '2026-10-08 12:01:30', 'postgres', NOW)).toBeNull();
expect(checkValue({ timestamp: 0 }, '2026-10-08 12:10:00', 'postgres', NOW)).toBe('2026-10-08 12:10:00 is 600s off');
expect(checkValue({ timestamp: 0 }, '2026-10-08 12:10:00', 'postgres', NOW)).toBe(
'2026-10-08 12:10:00 is 600s off',
);
expect(checkValue({ timestamp: 0 }, '2026-10-08T12:00:00Z', 'postgres', NOW)).toMatch(/expected timestamp text/);
expect(checkValue({ timestamp: 0 }, '2026-10-08 12:00:00.123456+00', 'postgres', NOW)).toMatch(/expected timestamp text/);
expect(checkValue({ timestamp: 0 }, '2026-10-08 12:00:00.123456+00', 'postgres', NOW)).toMatch(
/expected timestamp text/,
);
expect(checkValue({ date: -400 }, '2025-09-03', 'postgres', NOW)).toBeNull();
expect(checkValue({ date: 1 }, '2026-10-09', 'sqlite', NOW)).toBeNull();
expect(checkValue({ date: 1 }, '2026-10-12', 'sqlite', NOW)).toBe('2026-10-12 is not 1 day(s) from today');
@@ -7,8 +7,6 @@
* own transaction calls do nothing, and statements land on the method that
* sent them.
*/
import { Kysely, PostgresDialect, type PostgresPool } from 'kysely';
import { describe, expect, it } from 'vitest';
import {
errorLine,
isTransactionControl,
@@ -17,6 +15,9 @@ import {
StatementRecorder,
} from '../../../../scripts/pg-probe/recorder';
import { Kysely, PostgresDialect, type PostgresPool } from 'kysely';
import { describe, expect, it } from 'vitest';
interface ProbeDB {
t: { v: number };
}
@@ -62,7 +63,10 @@ describe('pg-probe recorder', () => {
it('PGPROBE-022: a refused statement is recorded with its SQLSTATE and answered as an empty table', async () => {
const { db, log, recorder } = fakeDatabase();
const rows = await recorder.run('A.b', async () => {
const first = await db.selectFrom('t').select('boom' as 'v').execute();
const first = await db
.selectFrom('t')
.select('boom' as 'v')
.execute();
await db.selectFrom('t').select('v').execute();
return first;
});
@@ -83,7 +87,7 @@ describe('pg-probe recorder', () => {
]);
});
it('PGPROBE-023: the caller\'s own transaction and savepoints never reach the database', async () => {
it("PGPROBE-023: the caller's own transaction and savepoints never reach the database", async () => {
const { db, log, recorder } = fakeDatabase();
await recorder.run('A.b', () =>
db.transaction().execute(async (trx) => {
@@ -113,12 +117,23 @@ describe('pg-probe recorder', () => {
}),
]);
expect(recorder.statementsOf('A.one')).toHaveLength(1);
expect(recorder.statementsOf('B.two').map((s) => s.sql)).toEqual(['select "v" from "t"', 'select "v" from "t" where "v" = $1']);
expect(recorder.statementsOf('B.two').map((s) => s.sql)).toEqual([
'select "v" from "t"',
'select "v" from "t" where "v" = $1',
]);
expect(recorder.current()).toBeNull();
});
it('PGPROBE-025: classifies transaction control and reads driver errors', () => {
for (const sql of ['begin', 'COMMIT', 'rollback', 'start transaction isolation level serializable', 'savepoint "trx1"', 'release savepoint "trx1"', 'rollback to savepoint "trx1"']) {
for (const sql of [
'begin',
'COMMIT',
'rollback',
'start transaction isolation level serializable',
'savepoint "trx1"',
'release savepoint "trx1"',
'rollback to savepoint "trx1"',
]) {
expect(isTransactionControl(sql)).toBe(true);
}
for (const sql of ['select 1', 'insert into "begin_log" values (1)', 'update "t" set "v" = 1']) {
@@ -4,10 +4,6 @@
* The CI job's verdict is exactly `exitCode()`, so it is pinned here rather
* than discovered in a red run.
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { exitCode, parseArgs, repositoryFiles } from '../../../../scripts/pg-probe';
import type { BaselineVerdict } from '../../../../scripts/pg-probe/baseline';
import type { HelperResult } from '../../../../scripts/pg-probe/helper-cases';
@@ -23,6 +19,11 @@ import {
type ReportInput,
} from '../../../../scripts/pg-probe/report';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
const ok = (sql: string) => ({ sql, outcome: { ok: true as const } });
const refused = (sql: string, code = '42883', message = 'function datetime(unknown) does not exist') => ({
sql,
@@ -31,12 +32,23 @@ const refused = (sql: string, code = '42883', message = 'function datetime(unkno
const RESULTS: MethodResult[] = [
{ key: 'A.clean', statements: [ok('select 1'), ok('select 1'), ok('select 2')] },
{ key: 'B.broken', statements: [refused('select datetime(1)'), refused('select datetime(1)'), ok('select 3'), refused('insert or ignore', '42601', 'syntax error')] },
{
key: 'B.broken',
statements: [
refused('select datetime(1)'),
refused('select datetime(1)'),
ok('select 3'),
refused('insert or ignore', '42601', 'syntax error'),
],
},
{ key: 'C.silent', statements: [] },
{ key: 'D.hung', statements: [ok('select 4')], timedOut: true },
{ key: 'E.skipped', statements: [], unprobeable: 'fn: a function parameter' },
{ key: 'F.thrower', statements: [], threw: 'no trip 1' },
{ key: 'G.sampled', statements: [refused('select $1::date', '22007', 'invalid input syntax for type date: "probe"'), ok('select 5')] },
{
key: 'G.sampled',
statements: [refused('select $1::date', '22007', 'invalid input syntax for type date: "probe"'), ok('select 5')],
},
];
const CLEAN: BaselineVerdict = { unseeded: false, grown: [], stale: [], newlyUncovered: [], nowCovered: [] };
@@ -44,7 +56,14 @@ const UNSEEDED: BaselineVerdict = { ...CLEAN, unseeded: true };
const helper = (failure: string | null): HelperResult => ({ engine: 'postgres', name: 'dateOf', failure });
function input(overrides: Partial<ReportInput> = {}): ReportInput {
return { summary: summarize(RESULTS), verdict: CLEAN, helpers: [helper(null)], schemaFailures: [], results: RESULTS, ...overrides };
return {
summary: summarize(RESULTS),
verdict: CLEAN,
helpers: [helper(null)],
schemaFailures: [],
results: RESULTS,
...overrides,
};
}
describe('pg-probe report', () => {
@@ -52,9 +71,22 @@ describe('pg-probe report', () => {
const summary = summarize(RESULTS);
expect(summary).toMatchObject({ methods: 7, called: 6, unprobeable: 1, withoutSql: 2, timedOut: 1, statements: 8 });
expect(summary.failingByMethod).toEqual(
new Map([['A.clean', 0], ['B.broken', 2], ['C.silent', 0], ['D.hung', 0], ['F.thrower', 0], ['G.sampled', 0]]),
new Map([
['A.clean', 0],
['B.broken', 2],
['C.silent', 0],
['D.hung', 0],
['F.thrower', 0],
['G.sampled', 0],
]),
);
expect(summary.failuresByCode).toEqual(
new Map([
['42883', 1],
['42601', 1],
['22007', 1],
]),
);
expect(summary.failuresByCode).toEqual(new Map([['42883', 1], ['42601', 1], ['22007', 1]]));
expect(summary.failedStatements.map((f) => `${f.method} ${f.code} ${f.counted}`)).toEqual([
'B.broken 42883 true',
'B.broken 42601 true',
@@ -63,7 +95,12 @@ describe('pg-probe report', () => {
});
it('PGPROBE-068: a data error from a sample value is reported but not held to the baseline', () => {
const summary = summarize([{ key: 'G.sampled', statements: [refused('select $1::date', '22P02', 'bad'), refused('select x', '0A000', 'nope')] }]);
const summary = summarize([
{
key: 'G.sampled',
statements: [refused('select $1::date', '22P02', 'bad'), refused('select x', '0A000', 'nope')],
},
]);
expect(summary.failingByMethod.get('G.sampled')).toBe(1);
expect(splitRefusals(summary).counted.map((f) => f.code)).toEqual(['0A000']);
expect(splitRefusals(summary).reported.map((f) => f.code)).toEqual(['22P02']);
@@ -115,7 +152,9 @@ describe('pg-probe report', () => {
const stale = input({ verdict: { ...CLEAN, stale: [{ method: 'B.broken', allowed: 3, now: 2 }] } });
expect(exitCode(stale, false)).toBe(1);
expect(exitCode(stale, true)).toBe(0);
expect(exitCode(input({ verdict: { ...CLEAN, grown: [{ method: 'B.broken', allowed: 1, now: 2 }] } }), true)).toBe(1);
expect(exitCode(input({ verdict: { ...CLEAN, grown: [{ method: 'B.broken', allowed: 1, now: 2 }] } }), true)).toBe(
1,
);
expect(exitCode(input({ helpers: [helper('threw: boom')] }), true)).toBe(1);
expect(exitCode(input(), false)).toBe(0);
});
@@ -140,16 +179,24 @@ describe('pg-probe report', () => {
newlyUncovered: ['F.thrower'],
nowCovered: ['Y.measured'],
},
schemaFailures: [{ statement: 'create table "x" (a text collate "NOCASE")', message: 'collation "NOCASE" does not exist' }],
schemaFailures: [
{ statement: 'create table "x" (a text collate "NOCASE")', message: 'collation "NOCASE" does not exist' },
],
}),
);
expect(text).toContain('FAIL [postgres] dateOf: expected 1, got 2');
expect(text).toContain('collation "NOCASE" does not exist');
expect(text).toContain('Statements: 8 distinct, 3 refused by Postgres (2 as SQL, held to the baseline; 1 over a sample value');
expect(text).toContain(
'Statements: 8 distinct, 3 refused by Postgres (2 as SQL, held to the baseline; 1 over a sample value',
);
expect(text).toContain(' 22007: 1 (not held)');
expect(text).toContain(' F.thrower: threw before any statement: no trip 1');
expect(text).toContain('FAIL F.thrower threw before any statement: no trip 1, and the baseline does not list it as uncovered.');
expect(text).toContain('FAIL Y.measured is listed as uncovered but is measured now (or gone); drop it from the baseline.');
expect(text).toContain(
'FAIL F.thrower threw before any statement: no trip 1, and the baseline does not list it as uncovered.',
);
expect(text).toContain(
'FAIL Y.measured is listed as uncovered but is measured now (or gone); drop it from the baseline.',
);
expect(text).toContain('B.broken 42883 function datetime(unknown) does not exist');
expect(text).toContain('FAIL B.broken sends 2 statement(s) Postgres refuses, 1 allowed.');
expect(text).toContain('FAIL Z.gone is held at 1 failing statement(s) but fails 0 now; lower the baseline.');
@@ -158,20 +205,24 @@ describe('pg-probe report', () => {
expect(unseeded).toContain('FAIL Baseline: not measured yet');
expect(unseeded).toContain('as scripts/pg-probe-baseline.json');
expect(unseeded.trim().endsWith('Postgres probe failed.')).toBe(true);
expect(formatConsole(input({ summary: summarize([]), results: [] }))).toContain('FAIL No repository method was called (0 planned)');
expect(formatConsole(input({ summary: summarize([]), results: [] }))).toContain(
'FAIL No repository method was called (0 planned)',
);
});
it('PGPROBE-064: the step summary is a Markdown table with escaped cells', () => {
const md = formatMarkdown(input({ helpers: [{ engine: 'postgres', name: 'a|b', failure: 'got `x`' }] }));
expect(md).toContain('## Postgres probe');
expect(md).toContain('Result: **failed**');
expect(md).toContain('| postgres | a\\|b | got \'x\' |');
expect(md).toContain("| postgres | a\\|b | got 'x' |");
expect(md).toContain('| 42883 | 1 |');
expect(md).toContain('| E.skipped | could not be called: fn: a function parameter |');
expect(md).toContain('| Refused by Postgres as SQL (held) | 2 |');
expect(md).toContain('| Refused over a sample value or the empty tables (reported only) | 1 |');
expect(md).toContain('| G.sampled | 22007 | no | invalid input syntax for type date: "probe" |');
const ratchet = formatMarkdown(input({ verdict: { ...CLEAN, newlyUncovered: ['C.silent'], nowCovered: ['Z.gone'] } }));
const ratchet = formatMarkdown(
input({ verdict: { ...CLEAN, newlyUncovered: ['C.silent'], nowCovered: ['Z.gone'] } }),
);
expect(ratchet).toContain('### Coverage ratchet');
expect(ratchet).toContain('| C.silent | measured | sent no SQL |');
expect(ratchet).toContain('| Z.gone | uncovered | measured or gone |');
@@ -196,12 +247,25 @@ describe('pg-probe entry point', () => {
report: null,
summary: null,
});
const args = parseArgs(['--url=postgresql://h/d', '--update', '--next-baseline=n.json', '--report=r.json', '--summary=s.md'], {});
expect(args).toEqual({ url: 'postgresql://h/d', update: true, nextBaseline: 'n.json', report: 'r.json', summary: 's.md' });
expect(parseArgs([], { TREK_PG_PROBE_URL: 'postgres://h/d', GITHUB_STEP_SUMMARY: '/tmp/summary' }).summary).toBe('/tmp/summary');
const args = parseArgs(
['--url=postgresql://h/d', '--update', '--next-baseline=n.json', '--report=r.json', '--summary=s.md'],
{},
);
expect(args).toEqual({
url: 'postgresql://h/d',
update: true,
nextBaseline: 'n.json',
report: 'r.json',
summary: 's.md',
});
expect(parseArgs([], { TREK_PG_PROBE_URL: 'postgres://h/d', GITHUB_STEP_SUMMARY: '/tmp/summary' }).summary).toBe(
'/tmp/summary',
);
expect(() => parseArgs([], {})).toThrow(/no Postgres URL/);
expect(() => parseArgs(['--url=mysql://h/d'], {})).toThrow(/no Postgres URL/);
expect(() => parseArgs(['--verbose'], { TREK_PG_PROBE_URL: 'postgres://h/d' })).toThrow(/unknown argument --verbose/);
expect(() => parseArgs(['--verbose'], { TREK_PG_PROBE_URL: 'postgres://h/d' })).toThrow(
/unknown argument --verbose/,
);
expect(() => parseArgs(['--update=yes'], { TREK_PG_PROBE_URL: 'postgres://h/d' })).toThrow(/unknown argument/);
});
@@ -4,8 +4,16 @@
* NOCASE collation, and splitting the generated DDL so one refused statement
* does not take the rest with it.
*/
import {
adjustColumns,
NOCASE_COLLATION_SQL,
PG_NOW_TEXT_DEFAULT,
portableDefault,
splitStatements,
type ColumnMeta,
} from '../../../../scripts/pg-probe/schema';
import { describe, expect, it } from 'vitest';
import { adjustColumns, NOCASE_COLLATION_SQL, PG_NOW_TEXT_DEFAULT, portableDefault, splitStatements, type ColumnMeta } from '../../../../scripts/pg-probe/schema';
class DbTimestampType {}
class TextType {}
@@ -22,7 +30,12 @@ describe('pg-probe schema', () => {
it('PGPROBE-041: makes timestamp columns text, rewrites defaults and the NOCASE collation, and names what it changed', () => {
const columns: ColumnMeta[] = [
{ name: 'created_at', customType: new DbTimestampType(), columnTypes: ['timestamptz'], defaultRaw: 'CURRENT_TIMESTAMP' },
{
name: 'created_at',
customType: new DbTimestampType(),
columnTypes: ['timestamptz'],
defaultRaw: 'CURRENT_TIMESTAMP',
},
{ name: 'title', customType: new TextType(), columnTypes: ['text'], defaultRaw: "'x'" },
{ name: 'stamp', columnTypes: ['int'], defaultRaw: "(strftime('%s','now'))" },
{ name: 'region', columnTypes: ['text'], collation: 'NOCASE' },
@@ -31,7 +44,10 @@ describe('pg-probe schema', () => {
expect(adjustColumns(columns)).toEqual(['created_at', 'stamp', 'region']);
expect(columns[0]).toMatchObject({ columnTypes: ['text'], defaultRaw: PG_NOW_TEXT_DEFAULT });
expect(columns[1]).toMatchObject({ columnTypes: ['text'], defaultRaw: "'x'" });
expect(columns[2]).toMatchObject({ columnTypes: ['int'], defaultRaw: 'CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS bigint)' });
expect(columns[2]).toMatchObject({
columnTypes: ['int'],
defaultRaw: 'CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS bigint)',
});
expect(columns[3]).toMatchObject({ collation: 'nocase' });
expect(NOCASE_COLLATION_SQL).toContain('CREATE COLLATION IF NOT EXISTS nocase');
});
@@ -129,7 +129,9 @@ describe('test-sql-ratchet.mjs', () => {
it('TSQL-005c: the cover ends with the statement, after the first call, and REACH lines down', () => {
const marker = '// test-sql-allow: reason\n';
// The statement below the marker ends on its first line; the next one is not covered.
const ended = run(serverRoot({ 'tests/a.test.ts': `${marker}const a = 1;\nconst b = testDb\n ${CALL}'x').get();\n` }));
const ended = run(
serverRoot({ 'tests/a.test.ts': `${marker}const a = 1;\nconst b = testDb\n ${CALL}'x').get();\n` }),
);
expect(ended.out).toContain('tests/a.test.ts: 1 raw statement(s)');
// Only the first call of the statement is covered.
const two = `${marker}const rows = [\n testDb${CALL}'a').get(),\n testDb${CALL}'b').get(),\n];\n`;
@@ -154,13 +156,17 @@ describe('test-sql-ratchet.mjs', () => {
const gone = run(serverRoot({ 'tests/a.test.ts': '' }, { exempt, counts: {} }));
expect(gone.status).toBe(1);
expect(gone.out).toContain('FAIL tests/unit/db/dialect.test.ts is exempt in scripts/test-sql-baseline.json, but the file is gone');
expect(gone.out).toContain(
'FAIL tests/unit/db/dialect.test.ts is exempt in scripts/test-sql-baseline.json, but the file is gone',
);
});
it('TSQL-008: fails an entry above its file, or for a file with none left, until --update lowers it', () => {
const shrunk = run(serverRoot({ 'tests/a.test.ts': stmt(1) }, { exempt: {}, counts: { 'tests/a.test.ts': 3 } }));
expect(shrunk.status).toBe(1);
expect(shrunk.out).toContain('FAIL tests/a.test.ts is held at 3 in scripts/test-sql-baseline.json, but it has 1 now.');
expect(shrunk.out).toContain(
'FAIL tests/a.test.ts is held at 3 in scripts/test-sql-baseline.json, but it has 1 now.',
);
expect(shrunk.out).toContain('npm run lint:test-sql -- --update');
const converted = run(serverRoot({ 'tests/a.test.ts': '' }, { exempt: {}, counts: { 'tests/a.test.ts': 3 } }));
@@ -180,7 +186,12 @@ describe('test-sql-ratchet.mjs', () => {
},
{
exempt,
counts: { 'tests/shrunk.test.ts': 5, 'tests/converted.test.ts': 2, 'tests/grown.test.ts': 6, 'tests/gone.test.ts': 1 },
counts: {
'tests/shrunk.test.ts': 5,
'tests/converted.test.ts': 2,
'tests/grown.test.ts': 6,
'tests/gone.test.ts': 1,
},
},
);
const { status } = run(dir, '--update');
+14 -4
View File
@@ -200,7 +200,9 @@ export class BarRepository {
describe('tx-writes.mjs: the baseline', () => {
const TWO = service(' async twoWrites() { await this.foos.insertFoo(); await this.foos.deleteFoo(); }');
const LOOP = service(' async twoWrites(ids: number[]) { for (const id of ids) { await this.foos.insertFoo(); await this.foos.deleteFoo(); } }');
const LOOP = service(
' async twoWrites(ids: number[]) { for (const id of ids) { await this.foos.insertFoo(); await this.foos.deleteFoo(); } }',
);
it('TXW-010: a method not in the baseline fails, naming the fix', () => {
const { status, out } = run(serverRoot({ [SERVICE_FILE]: TWO }));
@@ -218,7 +220,11 @@ describe('tx-writes.mjs: the baseline', () => {
it('TXW-012: an entry for a fixed method, or one above its count, fails until --update lowers it', () => {
const fixed = serverRoot(
{ [SERVICE_FILE]: service(' async twoWrites() { await this.uow.transactional(async () => { await this.foos.insertFoo(); await this.foos.deleteFoo(); }); }') },
{
[SERVICE_FILE]: service(
' async twoWrites() { await this.uow.transactional(async () => { await this.foos.insertFoo(); await this.foos.deleteFoo(); }); }',
),
},
{ [KEY('twoWrites')]: 2 },
);
const stale = run(fixed);
@@ -231,7 +237,9 @@ describe('tx-writes.mjs: the baseline', () => {
expect(run(fixed, '--update').status).toBe(0);
expect(JSON.parse(readFileSync(path.join(fixed, 'scripts/tx-writes-baseline.json'), 'utf8'))).toEqual({});
expect(run(lower, '--update').status).toBe(0);
expect(JSON.parse(readFileSync(path.join(lower, 'scripts/tx-writes-baseline.json'), 'utf8'))).toEqual({ [KEY('twoWrites')]: 2 });
expect(JSON.parse(readFileSync(path.join(lower, 'scripts/tx-writes-baseline.json'), 'utf8'))).toEqual({
[KEY('twoWrites')]: 2,
});
});
it('TXW-013: --update never adds an entry', () => {
@@ -241,7 +249,9 @@ describe('tx-writes.mjs: the baseline', () => {
});
it('TXW-014: a missing or malformed baseline, or a tree without src/, refuses to run', () => {
expect(run(serverRoot({ [SERVICE_FILE]: TWO }, null)).out).toContain('scripts/tx-writes-baseline.json cannot be read');
expect(run(serverRoot({ [SERVICE_FILE]: TWO }, null)).out).toContain(
'scripts/tx-writes-baseline.json cannot be read',
);
expect(run(serverRoot({}, '[]')).out).toContain('must be an object of methods to write counts');
expect(run(serverRoot({}, { [KEY('x')]: 1 })).out).toContain('expected an integer of at least 2');
const empty = mkdtempSync(path.join(tmpdir(), 'trek-tx-writes-'));