* fix: security hardening — search DoS, slug hijack, symlink traversal, content bombs, stdin guard 4 security vulnerabilities closed: - Search limit clamped to 100 (MAX_SEARCH_LIMIT) with statement_timeout 8s - Frontmatter slug authority enforced (path-derived, mismatch rejected) - Symlink traversal blocked (lstatSync in walker + importFromFile) - Content size guard on importFromContent (Buffer.byteLength, 5MB) - Stdin size guard in parseOpArgs (5MB cap) Search pagination added (--offset param on search + query operations). Clamp warning emitted when limit is capped. Co-Authored-By: garagon <garagon@users.noreply.github.com> * fix: PGLite concurrent access lock — prevent Aborted() crash File-based advisory lock using atomic mkdir with PID tracking and 5-minute stale detection. Clear error messages show which process holds the lock and how to recover. Co-Authored-By: danbr <danbr@users.noreply.github.com> * fix: 12 data integrity fixes + stale embedding prevention CTE searchKeyword rewrite (SQL-level LIMIT, not JS splice). Write validation on addLink/addTag/addTimelineEntry/putRawData/createVersion. Health metrics now measure real problems (stale_pages, orphan_pages, dead_links). Orphan chunk cleanup on empty pages. Embedding error logging. contentHash now covers all PageInput fields. Stale embedding NULL'd when chunk_text changes (prevents wrong vector on new text). hybridSearch stops double-embedding query. MCP param validation. type/exclude_slugs search filters now work. pgcrypto extension for Postgres <13. Co-Authored-By: win4r <win4r@users.noreply.github.com> * perf: 30x embedAll speedup + O(n²) fix + ask alias Sliding worker pool (concurrency 20, tunable via GBRAIN_EMBED_CONCURRENCY). O(n²) chunk lookup in embedPage replaced with Map. gbrain ask alias for query (CLI-only, not in MCP tools-json). .idea added to .gitignore. Co-Authored-By: stephenhungg <stephenhungg@users.noreply.github.com> Co-Authored-By: sharziki <sharziki@users.noreply.github.com> Co-Authored-By: hnshah <hnshah@users.noreply.github.com> Co-Authored-By: doguabaris <doguabaris@users.noreply.github.com> * chore: bump version and changelog (v0.9.1) Community fix wave: 10 PRs, 7 contributors. 4 security fixes, PGLite crash fix, 12 data integrity fixes, 30x embed speedup, search pagination, ask alias. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: garagon <garagon@users.noreply.github.com> Co-authored-by: danbr <danbr@users.noreply.github.com> Co-authored-by: win4r <win4r@users.noreply.github.com> Co-authored-by: stephenhungg <stephenhungg@users.noreply.github.com> Co-authored-by: sharziki <sharziki@users.noreply.github.com> Co-authored-by: hnshah <hnshah@users.noreply.github.com> Co-authored-by: doguabaris <doguabaris@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { readFileSync } from 'fs';
|
|
|
|
// Read cli.ts source for structural checks
|
|
const cliSource = readFileSync(new URL('../src/cli.ts', import.meta.url), 'utf-8');
|
|
|
|
describe('CLI structure', () => {
|
|
test('imports operations from operations.ts', () => {
|
|
expect(cliSource).toContain("from './core/operations.ts'");
|
|
});
|
|
|
|
test('builds cliOps map from operations', () => {
|
|
expect(cliSource).toContain('cliOps');
|
|
});
|
|
|
|
test('CLI_ONLY set contains expected commands', () => {
|
|
expect(cliSource).toContain("'init'");
|
|
expect(cliSource).toContain("'upgrade'");
|
|
expect(cliSource).toContain("'import'");
|
|
expect(cliSource).toContain("'export'");
|
|
expect(cliSource).toContain("'embed'");
|
|
expect(cliSource).toContain("'files'");
|
|
});
|
|
|
|
test('has formatResult function for CLI output', () => {
|
|
expect(cliSource).toContain('function formatResult');
|
|
});
|
|
});
|
|
|
|
describe('CLI version', () => {
|
|
test('VERSION matches package.json', async () => {
|
|
const { VERSION } = await import('../src/version.ts');
|
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
expect(VERSION).toBe(pkg.version);
|
|
});
|
|
|
|
test('VERSION is a valid semver string', async () => {
|
|
const { VERSION } = await import('../src/version.ts');
|
|
expect(VERSION).toMatch(/^\d+\.\d+\.\d+/);
|
|
});
|
|
});
|
|
|
|
describe('ask alias', () => {
|
|
test('ask alias maps to query in source', () => {
|
|
expect(cliSource).toContain("if (command === 'ask')");
|
|
expect(cliSource).toContain("command = 'query'");
|
|
});
|
|
|
|
test('ask does NOT appear in --tools-json output', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', '--tools-json'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
await proc.exited;
|
|
const tools = JSON.parse(stdout);
|
|
const names = tools.map((t: any) => t.name);
|
|
expect(names).not.toContain('ask');
|
|
});
|
|
});
|
|
|
|
describe('CLI dispatch integration', () => {
|
|
test('--version outputs version', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', '--version'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
await proc.exited;
|
|
expect(stdout.trim()).toMatch(/^gbrain \d+\.\d+\.\d+/);
|
|
});
|
|
|
|
test('unknown command prints error and exits 1', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'notacommand'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stderr = await new Response(proc.stderr).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stderr).toContain('Unknown command: notacommand');
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
test('per-command --help prints usage without DB connection', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'get', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('Usage: gbrain get');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('upgrade --help prints usage without running upgrade', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'upgrade', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('Usage: gbrain upgrade');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('--help prints global help', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('USAGE');
|
|
expect(stdout).toContain('gbrain <command>');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('--tools-json outputs valid JSON with operations', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', '--tools-json'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
await proc.exited;
|
|
const tools = JSON.parse(stdout);
|
|
expect(Array.isArray(tools)).toBe(true);
|
|
expect(tools.length).toBeGreaterThanOrEqual(30);
|
|
expect(tools[0]).toHaveProperty('name');
|
|
expect(tools[0]).toHaveProperty('description');
|
|
expect(tools[0]).toHaveProperty('parameters');
|
|
});
|
|
});
|