ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { getPool } from '@/tests/pg/setup'
|
|
|
|
/**
|
|
* Covers 20230101000000_extension_function_wrappers:
|
|
*
|
|
* - Wrapper existence: public.uuid_generate_v4() and
|
|
* public.gen_random_bytes() are present and resolve correctly.
|
|
*
|
|
* - Delegation: public.uuid_generate_v4() → extensions.uuid_generate_v4()
|
|
* and public.gen_random_bytes() → extensions.gen_random_bytes().
|
|
*
|
|
* - Correctness: uuid_generate_v4() produces valid non-null UUIDs with
|
|
* distinct values across calls. gen_random_bytes(n) returns a bytea of
|
|
* exactly n bytes (n >= 1; OpenSSL-backed pgcrypto rejects size 0).
|
|
*
|
|
* - Function properties: VOLATILE, PARALLEL SAFE, SET search_path = '',
|
|
* LANGUAGE sql: consistent with the rest of the codebase.
|
|
*/
|
|
|
|
describe('extension function wrappers', () => {
|
|
it('uuid_generate_v4() exists in public schema and returns a valid UUID', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{ uuid: string }>(
|
|
`SELECT public.uuid_generate_v4() AS uuid`,
|
|
)
|
|
const uuid = r.rows[0]!.uuid
|
|
expect(uuid).toBeTruthy()
|
|
// UUID v4 format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
|
|
expect(uuid).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
)
|
|
})
|
|
|
|
it('uuid_generate_v4() returns distinct values across calls (no caching)', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{ uuid: string }>(
|
|
`SELECT public.uuid_generate_v4() AS uuid
|
|
UNION ALL
|
|
SELECT public.uuid_generate_v4()`,
|
|
)
|
|
expect(r.rows).toHaveLength(2)
|
|
expect(r.rows[0]!.uuid).not.toBe(r.rows[1]!.uuid)
|
|
})
|
|
|
|
it('uuid_generate_v4() can be used as a column DEFAULT expression', async () => {
|
|
const pool = getPool()
|
|
// Create a temp table using the wrapper as default, insert a row, check
|
|
// the UUID was generated and is valid.
|
|
await pool.query(`
|
|
CREATE TEMP TABLE _test_uuid_default (
|
|
id uuid PRIMARY KEY DEFAULT public.uuid_generate_v4(),
|
|
label text
|
|
)
|
|
`)
|
|
await pool.query(`INSERT INTO _test_uuid_default (label) VALUES ('test')`)
|
|
const r = await pool.query<{ id: string }>(`SELECT id FROM _test_uuid_default`)
|
|
expect(r.rows[0]!.id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
)
|
|
await pool.query(`DROP TABLE _test_uuid_default`)
|
|
})
|
|
|
|
it('gen_random_bytes() exists in public schema', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{ ok: boolean }>(
|
|
`SELECT pg_get_functiondef('public.gen_random_bytes(integer)'::regprocedure) IS NOT NULL AS ok`,
|
|
)
|
|
expect(r.rows[0]!.ok).toBe(true)
|
|
})
|
|
|
|
it('gen_random_bytes(n) returns bytea of exactly n bytes', async () => {
|
|
const pool = getPool()
|
|
for (const n of [1, 16, 32, 64]) {
|
|
const r = await pool.query<{ len: number }>(
|
|
`SELECT octet_length(public.gen_random_bytes($1)) AS len`,
|
|
[n],
|
|
)
|
|
expect(r.rows[0]!.len).toBe(n)
|
|
}
|
|
})
|
|
|
|
it('gen_random_bytes(n) returns exactly n bytes', async () => {
|
|
// Modern (OpenSSL-backed) pgcrypto rejects size 0 with "Length not in
|
|
// range", so we assert the real contract on a positive length rather than
|
|
// the version-dependent 0-byte edge case.
|
|
const pool = getPool()
|
|
const r = await pool.query<{ len: number }>(
|
|
`SELECT octet_length(public.gen_random_bytes(16)) AS len`,
|
|
)
|
|
expect(r.rows[0]!.len).toBe(16)
|
|
})
|
|
|
|
it('gen_random_bytes returns distinct values across calls', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{ a: Buffer; b: Buffer }>(
|
|
`SELECT public.gen_random_bytes(16) AS a, public.gen_random_bytes(16) AS b`,
|
|
)
|
|
expect(r.rows[0]!.a).not.toEqual(r.rows[0]!.b)
|
|
})
|
|
|
|
it('uuid_generate_v4() is VOLATILE and PARALLEL SAFE', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{
|
|
provolatile: string
|
|
proparallel: string
|
|
}>(
|
|
`SELECT provolatile, proparallel
|
|
FROM pg_proc
|
|
WHERE oid = 'public.uuid_generate_v4()'::regprocedure`,
|
|
)
|
|
expect(r.rows[0]!.provolatile).toBe('v')
|
|
expect(r.rows[0]!.proparallel).toBe('s')
|
|
})
|
|
|
|
it('gen_random_bytes(integer) is VOLATILE and PARALLEL SAFE', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{
|
|
provolatile: string
|
|
proparallel: string
|
|
}>(
|
|
`SELECT provolatile, proparallel
|
|
FROM pg_proc
|
|
WHERE oid = 'public.gen_random_bytes(integer)'::regprocedure`,
|
|
)
|
|
expect(r.rows[0]!.provolatile).toBe('v')
|
|
expect(r.rows[0]!.proparallel).toBe('s')
|
|
})
|
|
|
|
it('both wrappers delegate to extensions schema functions', async () => {
|
|
const pool = getPool()
|
|
// Verify the extensions schema exists and has the source functions.
|
|
const extFns = await pool.query<{ name: string; schema: string }>(
|
|
`SELECT p.proname AS name, n.nspname AS schema
|
|
FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname = 'extensions'
|
|
AND p.proname IN ('uuid_generate_v4', 'gen_random_bytes')`,
|
|
)
|
|
expect(extFns.rows).toHaveLength(2)
|
|
|
|
// Verify the public wrappers exist.
|
|
const pubFns = await pool.query<{ name: string; schema: string }>(
|
|
`SELECT p.proname AS name, n.nspname AS schema
|
|
FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname = 'public'
|
|
AND p.proname IN ('uuid_generate_v4', 'gen_random_bytes')`,
|
|
)
|
|
expect(pubFns.rows).toHaveLength(2)
|
|
})
|
|
|
|
it('wrappers are defined in SQL (not plpgsql or C)', async () => {
|
|
const pool = getPool()
|
|
const r = await pool.query<{ proname: string; prolang: string }>(
|
|
`SELECT proname, l.lanname AS prolang
|
|
FROM pg_proc p
|
|
JOIN pg_language l ON l.oid = p.prolang
|
|
WHERE p.proname IN ('uuid_generate_v4', 'gen_random_bytes')
|
|
AND p.pronamespace = 'public'::regnamespace`,
|
|
)
|
|
for (const row of r.rows) {
|
|
expect(row.prolang).toBe('sql')
|
|
}
|
|
})
|
|
|
|
it('wrappers are idempotent: CREATE OR REPLACE succeeds on re-run', async () => {
|
|
const pool = getPool()
|
|
// Simulate re-applying the migration: re-running the same CREATE OR REPLACE
|
|
// should succeed without errors and the functions should still work.
|
|
await pool.query(`
|
|
CREATE OR REPLACE FUNCTION public.uuid_generate_v4()
|
|
RETURNS uuid
|
|
LANGUAGE sql
|
|
VOLATILE
|
|
PARALLEL SAFE
|
|
SET search_path = ''
|
|
AS $$ SELECT extensions.uuid_generate_v4(); $$
|
|
`)
|
|
await pool.query(`
|
|
CREATE OR REPLACE FUNCTION public.gen_random_bytes(size integer)
|
|
RETURNS bytea
|
|
LANGUAGE sql
|
|
VOLATILE
|
|
PARALLEL SAFE
|
|
SET search_path = ''
|
|
AS $$ SELECT extensions.gen_random_bytes(size); $$
|
|
`)
|
|
// Functions must still work after re-creation.
|
|
const r = await pool.query<{ uuid: string; bytes_len: number }>(
|
|
`SELECT public.uuid_generate_v4() AS uuid,
|
|
octet_length(public.gen_random_bytes(32)) AS bytes_len`,
|
|
)
|
|
expect(r.rows[0]!.uuid).toBeTruthy()
|
|
expect(r.rows[0]!.bytes_len).toBe(32)
|
|
})
|
|
})
|