Files
gbrain/test/sql-ranking.test.ts
T
4fc1246606 v0.24.0: production-hardening pass on the skillify loop (#387)
* feat: v0.19.0 — skillify loop + AGENTS.md compat + brain-first convention

This is the v0.19.0 release. The branch ships four new CLI commands, a
refactor to check-resolvable, and an expansion of the brain-first
convention for sub-agent tool discovery. The original commit message
described only the convention expansion, undercounting the scope by ~5x;
this amend captures the full release.

NEW COMMANDS

- gbrain skillify scaffold <name>     — 4 stub files + idempotent resolver row
- gbrain skillify check [path]        — 10-item post-task audit (promoted)
- gbrain skillpack list / install     — curated 25-skill bundle, atomic install
- gbrain skillpack diff <name>        — per-file diff preview
- gbrain routing-eval                 — dedicated CI verb for Check 5 fixtures

CHECK-RESOLVABLE REFACTOR

- Accepts AGENTS.md as a resolver file alongside RESOLVER.md, at either
  the skills directory or one level up (workspace root layout).
- Auto-derives the skill manifest by walking skills/*/SKILL.md when
  manifest.json is missing.
- Splits ResolvableReport into errors[] + warnings[] so advisory checks
  (filing audit, routing gaps, DRY violations) don't break CI by default.
- New --strict opt-in flag promotes warnings to exit 1.

BRAIN-FIRST CONVENTION

- skills/conventions/brain-first.md expanded from 5-step lookup guide to
  full sub-agent reference: tool inventory, lookup chain, score thresholds,
  authority hierarchy, sync rules, entity page conventions, sub-agent
  propagation rule.

PRODUCTION-READINESS HARDENING (this branch's review pass)

- routing-eval --llm: emits stderr placeholder notice + runs structural
  layer only. README, CHANGELOG, CLI help all rewritten consistently.
  Was a silent no-op against documented contract.
- skillpack installer: receipt comment in fence (cumulative-slugs="...")
  preserves single-skill-install accumulation while letting install --all
  prune removed bundle skills cleanly. Unknown rows preserved + stderr
  warning for the operating agent. Pre-v0.19 fences upgrade silently.
- skillify scaffold: resolver-row regex broadened to detect backticked,
  quoted, and bare path forms. No duplicate row on --force after the
  user normalizes formatting.
- scripts/check-privacy.sh: now wired into package.json test chain so
  the wintermute-ban rule is actually enforced. New regression test.
- E2E Tier 2 (LLM skills) promoted from schedule-only to required per-PR
  CI. Local Tier 1 + Tier 2 verified clean.
- Stale v0.17/v0.18 version labels rewritten across new files.

TESTS

- test/routing-eval-cli.test.ts: 4 cases covering --llm warn semantics
- test/privacy-script-wired.test.ts: regression guard for CI wiring
- test/skillpack-install.test.ts: 4 new cases for receipt + cumulative
  + unknown-row preserve+warn + pre-v0.19 upgrade path
- test/skillify-scaffold.test.ts: 4 new cases for broadened regex

VERIFICATION

- bun test: 2237 pass / 18 known PGLite-contention flakes (CI green;
  documented as P3 dev-experience in TODOS.md)
- bun run typecheck: clean
- bun run test:e2e: 18/19 files green (1 pre-existing flake on master,
  not caused by this branch — verified via git stash)
- llms.txt + llms-full.txt regenerated to match README + CHANGELOG

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: scrub banned fork name from public artifacts

The privacy guard wired into the test chain in this branch caught 5
pre-existing references to the banned OpenClaw fork name in CHANGELOG.md
(2x), skills/migrations/v0.19.0.md (1x), src/cli.ts (1x), and
src/commands/sync.ts (1x). All originated in master's v0.19.0 release
notes and migration doc when the privacy script existed but wasn't
wired into CI yet.

Replacements per CLAUDE.md privacy mapping:
- Origin-story copy (CHANGELOG layer narratives, code comments naming
  the production deployment that drove the feature) → "Garry's OpenClaw"
- Reader-facing migration step → "your OpenClaw"

No code semantics changed. Comments + headings only.

Verification: scripts/check-privacy.sh exits 0, full CI guard chain
green (privacy + jsonb + progress + wasm + typecheck).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump VERSION to 0.24.0 + new CHANGELOG entry

Bump branch version above master's v0.21.0 per CLAUDE.md
"CHANGELOG + VERSION are branch-scoped" rule. The new v0.24.0 entry at
the top of CHANGELOG covers what THIS branch adds vs master:

- routing-eval --llm honesty pass (4-surface contract drift fix)
- skillpack installer cumulative-receipt + unknown-row preserve+warn
  (the Codex-caught regression that would have shipped in master if
  the original v0.19.0 had landed without this branch's review pass)
- skillify scaffold resolver-row regex broadening (backtick + quoted
  + bare forms; idempotency contract preserved under hand-editing)
- 5 banned-name leaks scrubbed from public artifacts
- check-privacy.sh wired into CI test chain + regression guard test
- 7 stale v0.17/v0.18 version labels rewritten across 5 files
- Tier 2 (LLM-skills E2E) promoted from schedule-only to required per-PR

VERSION 0.21.0 → 0.24.0
package.json version field synced.
llms.txt + llms-full.txt regenerated (no content drift; sizes match).

Test suite: 62/62 green across the 5 test files this branch added or
extended (routing-eval-cli, privacy-script-wired, skillpack-install,
skillify-scaffold, build-llms).

CI guards: privacy + jsonb + progress + wasm + typecheck all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.24.0

Auto-discovered drift via /document-release after the v0.24.0 hardening
pass landed. All factual corrections clearly warranted by the diff.

CLAUDE.md:
- Skillpack installer: documented the cumulative-slugs receipt comment,
  install --all prune semantics, unknown-row preserve+warn behavior,
  and pre-v0.24 silent upgrade. Was previously vague about
  "tracks a skill manifest so install --update diffs cleanly" without
  explaining what the receipt is or why it matters.
- routing-eval: replaced the false claim that --llm "opts into a Haiku
  tie-break layer for CI." Now correctly describes the placeholder
  semantic landed in v0.24.0 (stderr notice + structural-only run).

README.md:
- Skillpack section: added one paragraph on the receipt comment + the
  user-visible stderr message for hand-added rows. Connects the safe
  rerun promise to the v0.24.0 implementation that actually enforces it.

CONTRIBUTING.md:
- Running tests section: now recommends `bun run test` (full CI guard
  chain + typecheck + tests) before pushing. Names each guard so new
  contributors understand what catches what. The privacy guard (newly
  wired in v0.24.0) is one of these — without `bun run test` you'd skip
  it locally and find out from CI.

llms-full.txt: regenerated to reflect CLAUDE.md changes.

Verification: full guard chain green locally (privacy + jsonb + progress
+ wasm + typecheck).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Garry Tan <garry@ycombinator.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:17:54 -07:00

256 lines
9.2 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import {
buildSourceFactorCase,
buildHardExcludeClause,
__test__,
} from '../src/core/search/sql-ranking.ts';
import {
DEFAULT_SOURCE_BOOSTS,
DEFAULT_HARD_EXCLUDES,
parseSourceBoostEnv,
parseHardExcludesEnv,
resolveBoostMap,
resolveHardExcludes,
} from '../src/core/search/source-boost.ts';
const { escapeLikePattern, escapeSqlLiteral, buildLikePrefixLiteral } = __test__;
describe('escapeLikePattern', () => {
test('escapes %', () => {
expect(escapeLikePattern('foo%bar')).toBe('foo\\%bar');
});
test('escapes _', () => {
expect(escapeLikePattern('foo_bar')).toBe('foo\\_bar');
});
test('escapes \\ (Postgres LIKE default escape char)', () => {
expect(escapeLikePattern('foo\\bar')).toBe('foo\\\\bar');
});
test('escapes all three together', () => {
expect(escapeLikePattern('a%b_c\\d')).toBe('a\\%b\\_c\\\\d');
});
test('leaves plain strings untouched', () => {
expect(escapeLikePattern('originals/talks/')).toBe('originals/talks/');
});
});
describe('escapeSqlLiteral', () => {
test('doubles single quotes', () => {
expect(escapeSqlLiteral("O'Brien")).toBe("O''Brien");
});
test('handles SQL injection attempts as literal data', () => {
// Classic injection pattern is rendered harmless because the doubled
// quote keeps it inside a string literal in the emitted SQL.
expect(escapeSqlLiteral("'; DROP TABLE pages; --")).toBe("''; DROP TABLE pages; --");
});
});
describe('buildLikePrefixLiteral', () => {
test('produces a quoted LIKE pattern with trailing %', () => {
expect(buildLikePrefixLiteral('originals/')).toBe("'originals/%'");
});
test('escapes meta-chars before adding the trailing %', () => {
// Input contains a literal % that should be escaped, and the trailing
// % we add is the LIKE wildcard.
expect(buildLikePrefixLiteral('weird%path/')).toBe("'weird\\%path/%'");
});
test('escapes single-quote in prefix as SQL literal', () => {
expect(buildLikePrefixLiteral("o'brien/")).toBe("'o''brien/%'");
});
});
describe('buildSourceFactorCase', () => {
test('returns plain 1.0 when detail is "high" (temporal bypass)', () => {
const result = buildSourceFactorCase('p.slug', { 'originals/': 1.5 }, 'high');
expect(result).toBe('1.0');
});
test('temporal bypass tolerates uppercase / whitespace from MCP boundary', () => {
// Agents passing JSON over MCP can send "HIGH" or "high " (trailing
// space). The bypass must catch these — otherwise loose-string callers
// silently get boosted ranking instead of temporal bypass.
const map = { 'originals/': 1.5 };
expect(buildSourceFactorCase('p.slug', map, 'HIGH' as 'high')).toBe('1.0');
expect(buildSourceFactorCase('p.slug', map, 'high ' as 'high')).toBe('1.0');
expect(buildSourceFactorCase('p.slug', map, ' High ' as 'high')).toBe('1.0');
});
test('returns plain 1.0 when boost map is empty', () => {
expect(buildSourceFactorCase('p.slug', {}, 'medium')).toBe('1.0');
});
test('emits a CASE expression for non-high detail', () => {
const result = buildSourceFactorCase('p.slug', { 'originals/': 1.5 }, 'medium');
expect(result).toBe("(CASE WHEN p.slug LIKE 'originals/%' THEN 1.5 ELSE 1.0 END)");
});
test('sorts prefixes by length descending so longest-match wins', () => {
const result = buildSourceFactorCase(
'p.slug',
{ 'media/': 0.9, 'media/articles/': 1.1, 'media/x/': 0.7 },
'medium',
);
// Longest first: media/articles/ (15), media/x/ (8), media/ (6)
const m = result.match(/LIKE '([^']+)%'/g);
expect(m).toEqual([
"LIKE 'media/articles/%'",
"LIKE 'media/x/%'",
"LIKE 'media/%'",
]);
});
test('detail=low and detail=undefined both emit the boost CASE', () => {
const map = { 'originals/': 1.5 };
expect(buildSourceFactorCase('p.slug', map, 'low')).toContain('CASE WHEN');
expect(buildSourceFactorCase('p.slug', map, undefined)).toContain('CASE WHEN');
});
test('rejects non-finite or negative factors', () => {
const result = buildSourceFactorCase(
'p.slug',
{ 'good/': 1.5, 'nan/': NaN, 'neg/': -1, 'inf/': Infinity },
'medium',
);
expect(result).toBe("(CASE WHEN p.slug LIKE 'good/%' THEN 1.5 ELSE 1.0 END)");
});
test('uses the supplied slug column reference', () => {
expect(buildSourceFactorCase('slug', { 'originals/': 1.5 }, 'medium'))
.toContain('WHEN slug LIKE');
});
});
describe('buildHardExcludeClause', () => {
test('returns empty string when prefixes is empty', () => {
expect(buildHardExcludeClause('p.slug', [])).toBe('');
});
test('emits NOT (col LIKE ... OR col LIKE ...)', () => {
const result = buildHardExcludeClause('p.slug', ['test/', 'archive/']);
expect(result).toBe(`AND NOT (p.slug LIKE 'test/%' OR p.slug LIKE 'archive/%')`);
});
test('escapes %, _, and \\ as LIKE meta-characters', () => {
// CEO pass 4 + codex finding: backslash is Postgres LIKE's default escape char.
// A literal backslash in a user-supplied prefix must be escaped to \\ so
// it's treated as data, not as "escape the next char".
const result = buildHardExcludeClause('p.slug', ['weird\\path/']);
expect(result).toBe(`AND NOT (p.slug LIKE 'weird\\\\path/%')`);
});
test('treats SQL-injection-style input as literal', () => {
const result = buildHardExcludeClause('p.slug', ["'; DROP TABLE pages; --"]);
// Single quotes get doubled — the injection becomes inert text inside
// the string literal.
expect(result).toContain("''; DROP TABLE pages; --");
// Sanity: the structure of the clause is intact.
expect(result).toMatch(/^AND NOT \(p\.slug LIKE '.*%'\)$/);
});
test('skips empty-string prefixes', () => {
const result = buildHardExcludeClause('p.slug', ['test/', '', 'archive/']);
// Two LIKE clauses, one OR.
expect((result.match(/LIKE/g) || []).length).toBe(2);
});
});
describe('parseSourceBoostEnv', () => {
test('parses comma-separated prefix:factor pairs', () => {
expect(parseSourceBoostEnv('originals/:1.8,openclaw/chat/:0.3'))
.toEqual({ 'originals/': 1.8, 'openclaw/chat/': 0.3 });
});
test('returns empty object for undefined or empty', () => {
expect(parseSourceBoostEnv(undefined)).toEqual({});
expect(parseSourceBoostEnv('')).toEqual({});
});
test('skips malformed entries', () => {
expect(parseSourceBoostEnv('bogus,no-colon,originals/:abc,valid/:1.5'))
.toEqual({ 'valid/': 1.5 });
});
test('rejects negative factors', () => {
expect(parseSourceBoostEnv('foo/:-1.0,bar/:0.5')).toEqual({ 'bar/': 0.5 });
});
test('accepts factor=0 (legal but performance-inferior to hard-exclude)', () => {
expect(parseSourceBoostEnv('foo/:0')).toEqual({ 'foo/': 0 });
});
test('uses last colon to separate prefix from factor', () => {
// Edge case: someone puts a colon inside the prefix. Last colon wins.
expect(parseSourceBoostEnv('foo:bar/:1.5')).toEqual({ 'foo:bar/': 1.5 });
});
});
describe('parseHardExcludesEnv', () => {
test('parses comma-separated prefixes', () => {
expect(parseHardExcludesEnv('test/,scratch/,private/'))
.toEqual(['test/', 'scratch/', 'private/']);
});
test('returns empty array for undefined', () => {
expect(parseHardExcludesEnv(undefined)).toEqual([]);
});
test('trims whitespace and drops empty entries', () => {
expect(parseHardExcludesEnv(' test/ , , scratch/ ')).toEqual(['test/', 'scratch/']);
});
});
describe('resolveBoostMap', () => {
test('returns defaults when env is unset', () => {
expect(resolveBoostMap(undefined)).toEqual(DEFAULT_SOURCE_BOOSTS);
});
test('env override takes precedence over defaults', () => {
const merged = resolveBoostMap('originals/:99');
expect(merged['originals/']).toBe(99);
// Other defaults still present.
expect(merged['concepts/']).toBe(DEFAULT_SOURCE_BOOSTS['concepts/']);
});
test('env-only entries are added on top of defaults', () => {
const merged = resolveBoostMap('newprefix/:2.5');
expect(merged['newprefix/']).toBe(2.5);
expect(merged['originals/']).toBe(DEFAULT_SOURCE_BOOSTS['originals/']);
});
});
describe('resolveHardExcludes', () => {
test('returns defaults when nothing is overridden', () => {
const r = resolveHardExcludes(undefined, undefined, undefined);
for (const p of DEFAULT_HARD_EXCLUDES) expect(r).toContain(p);
});
test('caller exclude_slug_prefixes adds to the union', () => {
const r = resolveHardExcludes(['scratch/'], undefined, undefined);
expect(r).toContain('scratch/');
expect(r).toContain('test/'); // default still present
});
test('include_slug_prefixes opts back in', () => {
const r = resolveHardExcludes(undefined, ['test/'], undefined);
expect(r).not.toContain('test/');
// Other defaults still present.
expect(r).toContain('archive/');
});
test('env GBRAIN_SEARCH_EXCLUDE adds to the union', () => {
const r = resolveHardExcludes(undefined, undefined, 'envdir/');
expect(r).toContain('envdir/');
});
test('include subtracts from env-supplied excludes too', () => {
const r = resolveHardExcludes(undefined, ['envdir/'], 'envdir/');
expect(r).not.toContain('envdir/');
});
});