Files
gbrain/test/cli-args.test.ts
Garry TanandClaude Fable 5 fb5e7476f7 fix(put): reject empty content and stacked frontmatter instead of writing corrupt pages
Two verified backlog items on the put_page ingest path:

- #2822: `gbrain put` with empty non-TTY stdin silently wrote an empty
  page (0 chunks — invisible to search AND to embed --stale). parseOpArgs
  now leaves the stdin-fed param unset when stdin is empty/whitespace so
  the required-param check rejects with a message naming the empty pipe;
  a flag that overwrites a positional value (and vice versa) now warns to
  stderr instead of silently discarding it; and importFromContent throws
  on empty/whitespace content as the shared backstop, covering MCP
  put_page, capture, and every other caller.

- #2743: put_page silently accepted stacked frontmatter (the double-put
  corruption class: already-serialized markdown re-wrapped in fresh
  frontmatter). parseMarkdown(validate) grows a MULTI_FRONTMATTER finding
  (second ---…--- block with YAML-shaped lines right after the close
  fence; lone horizontal rules and the timeline sentinel stay untouched),
  lint maps it to frontmatter-multi, and importFromContent rejects it
  before any DB write.

Fixes #2822
Fixes #2743

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:58:03 -07:00

94 lines
3.1 KiB
TypeScript

import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import { join, resolve } from 'path';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { parseOpArgs } from '../src/cli.ts';
import { operationsByName } from '../src/core/operations.ts';
describe('parseOpArgs', () => {
test('--no-<boolean> maps to false without consuming the next flag', () => {
const params = parseOpArgs(operationsByName.query, [
'freshEmbedSourceScope code source',
'--limit',
'8',
'--no-expand',
'--source-id',
'gstack-code-repo-0e4763c9',
]);
expect(params).toEqual({
query: 'freshEmbedSourceScope code source',
limit: 8,
expand: false,
source_id: 'gstack-code-repo-0e4763c9',
});
});
describe('positional/flag overwrite warning (#2822)', () => {
const errors: string[] = [];
const origError = console.error;
const captureErrors = () => {
console.error = (...args: unknown[]) => errors.push(args.join(' '));
};
afterEach(() => {
console.error = origError;
errors.length = 0;
});
test('a flag that overwrites a positional value warns to stderr', () => {
captureErrors();
const params = parseOpArgs(operationsByName.query, ['positional text', '--query', 'flag text']);
expect(params.query).toBe('flag text');
expect(errors.some(e => e.includes('Warning') && e.includes('--query'))).toBe(true);
});
test('a positional that overwrites an earlier flag value warns to stderr', () => {
captureErrors();
const params = parseOpArgs(operationsByName.query, ['--query', 'flag text', 'positional text']);
expect(params.query).toBe('positional text');
expect(errors.some(e => e.includes('Warning') && e.includes('<query>'))).toBe(true);
});
test('no warning when flag and positional agree', () => {
captureErrors();
parseOpArgs(operationsByName.query, ['same', '--query', 'same']);
expect(errors).toEqual([]);
});
});
});
describe('gbrain put — empty non-TTY stdin rejects (#2822)', () => {
const REPO = resolve(import.meta.dir, '..');
const CLI = join(REPO, 'src', 'cli.ts');
const runPut = (input: string) => {
// Isolated HOME so a regression can never write into a real brain.
const home = mkdtempSync(join(tmpdir(), 'gbrain-put-empty-'));
try {
return spawnSync('bun', [CLI, 'put', 'inbox/empty-stdin-test'], {
stdio: ['pipe', 'pipe', 'pipe'],
input,
encoding: 'utf-8',
timeout: 60_000,
env: { ...process.env, HOME: home, GBRAIN_SKIP_STARTUP_HOOKS: '1' },
});
} finally {
rmSync(home, { recursive: true, force: true });
}
};
test('empty stdin exits 1 and names the missing content param', () => {
const res = runPut('');
expect(res.status).toBe(1);
expect(res.stderr).toContain('content');
expect(res.stderr).toContain('stdin');
}, 90_000);
test('whitespace-only stdin also exits 1', () => {
const res = runPut(' \n\t\n');
expect(res.status).toBe(1);
expect(res.stderr).toContain('stdin');
}, 90_000);
});