Files
gbrain/test/fts-language.serial.test.ts
T
f8d11f67a3 feat(search): configurable FTS language + reindex command (lands #580/#581/#582) (#2941)
Squashed superset takeover of the FTS-language trilogy by @rafaelreis-r
(#580 env-var language for query+write side, #581 migration backfill,
#582 gbrain reindex-search-vector), rebased onto current master with the
security review's required fixes applied:

- Migration renumbered v116 -> v123 (master's v116 was already claimed by
  code_edges_source_backfill; master is at v122).
- Restored the v120/#1647 search_path hardening: all four CREATE OR
  REPLACE trigger-function bodies (migration handler + reindex command)
  now carry SET search_path = pg_catalog, public, since CREATE OR REPLACE
  resets proconfig and would otherwise strip the hardening on upgrade.
- reindex-search-vector: shared progress reporter (stderr phases
  reindex_search_vector.pages/.chunks), id-keyset batched backfill
  (5000 rows/UPDATE) instead of single whole-table statements, and
  --json no longer bypasses the --yes/TTY confirmation gate.
- Allowlist validation regex + injection tests kept exactly as authored.
- Stale v33/v116 comments swept; docs/guides/multi-language-fts.md
  written (README referenced it but no PR added it); llms bundles
  regenerated via bun run build:llms.
- Trilogy tests quarantined as *.serial.test.ts (env mutation, per
  check-test-isolation).

Verified live on PGLite: fresh init with GBRAIN_FTS_LANGUAGE=portuguese
produces portuguese-stemmed vectors with search_path pinned; the reindex
command retokenizes an english brain to portuguese in place.

Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: Rafael Reis <rafael.reis@contabilizei.com.br>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:23:47 -07:00

94 lines
2.6 KiB
TypeScript

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { getFtsLanguage, resetFtsLanguageCache } from '../src/core/fts-language.ts';
const ENV_KEY = 'GBRAIN_FTS_LANGUAGE';
beforeEach(() => {
delete process.env[ENV_KEY];
resetFtsLanguageCache();
});
afterEach(() => {
delete process.env[ENV_KEY];
resetFtsLanguageCache();
});
describe('getFtsLanguage', () => {
test('defaults to english when env is unset', () => {
expect(getFtsLanguage()).toBe('english');
});
test('defaults to english when env is empty string', () => {
process.env[ENV_KEY] = '';
expect(getFtsLanguage()).toBe('english');
});
test('defaults to english when env is whitespace', () => {
process.env[ENV_KEY] = ' ';
expect(getFtsLanguage()).toBe('english');
});
test('reads valid pt_br config', () => {
process.env[ENV_KEY] = 'pt_br';
expect(getFtsLanguage()).toBe('pt_br');
});
test('reads valid simple language name', () => {
process.env[ENV_KEY] = 'spanish';
expect(getFtsLanguage()).toBe('spanish');
});
test('reads name with underscores and digits', () => {
process.env[ENV_KEY] = 'custom_lang_v2';
expect(getFtsLanguage()).toBe('custom_lang_v2');
});
test('rejects names with quotes (SQL injection guard)', () => {
process.env[ENV_KEY] = "english'; DROP TABLE pages; --";
expect(getFtsLanguage()).toBe('english');
});
test('rejects names with spaces', () => {
process.env[ENV_KEY] = 'pt br';
expect(getFtsLanguage()).toBe('english');
});
test('rejects names with hyphens', () => {
process.env[ENV_KEY] = 'pt-br';
expect(getFtsLanguage()).toBe('english');
});
test('rejects names starting with digit', () => {
process.env[ENV_KEY] = '1lang';
expect(getFtsLanguage()).toBe('english');
});
test('rejects uppercase (Postgres config names are lowercase)', () => {
process.env[ENV_KEY] = 'English';
expect(getFtsLanguage()).toBe('english');
});
test('caches after first read', () => {
process.env[ENV_KEY] = 'pt_br';
expect(getFtsLanguage()).toBe('pt_br');
// Mutate env after first read \u2014 cached value wins.
process.env[ENV_KEY] = 'spanish';
expect(getFtsLanguage()).toBe('pt_br');
});
test('resetFtsLanguageCache clears cache', () => {
process.env[ENV_KEY] = 'pt_br';
expect(getFtsLanguage()).toBe('pt_br');
resetFtsLanguageCache();
process.env[ENV_KEY] = 'spanish';
expect(getFtsLanguage()).toBe('spanish');
});
test('trims surrounding whitespace from valid value', () => {
process.env[ENV_KEY] = ' pt_br ';
expect(getFtsLanguage()).toBe('pt_br');
});
});