mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
* feat(analyze): add explicit FTS opt-out * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(analyze): treat flag and env FTS opt-out as one mode Avoid a same-commit rebuild when only the skipReason discriminator changes, and advertise disablement on dirty meta before leftover indexes are wiped. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
6.7 KiB
TypeScript
162 lines
6.7 KiB
TypeScript
/**
|
|
* Unit Tests: /api/grep query parsing (gitnexus/src/server/grep-params.ts)
|
|
*
|
|
* Patch 12 contract fix — the grep tool schema promised regex +
|
|
* fileFilter + caseSensitive, but the handler escaped every pattern into
|
|
* a literal substring. These tests pin the restored semantics:
|
|
* - regex is real regex (alternation, classes, quantifiers work)
|
|
* - literal=1 keeps the old escaped-substring behaviour opt-in
|
|
* - fileFilter normalizes to a lowercase path substring
|
|
* - caseSensitive=1 drops the 'i' flag
|
|
* - malformed input throws BadRequestError (→ 400 via statusFromError)
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
parseGrepQuery,
|
|
GREP_PATTERN_MAX_LENGTH,
|
|
GREP_DEFAULT_LIMIT,
|
|
GREP_MAX_LIMIT,
|
|
} from '../../src/server/grep-params.js';
|
|
import { BadRequestError } from '../../src/server/validation.js';
|
|
|
|
describe('parseGrepQuery — regex semantics (restored contract)', () => {
|
|
it('honours alternation, the case the agent was burned by', () => {
|
|
const { regex } = parseGrepQuery({ pattern: 'sign|Sign' });
|
|
expect(regex.test('signOrder(order)')).toBe(true);
|
|
expect(regex.test('SignOffOrderAfterDecorator')).toBe(true);
|
|
expect(regex.test('assign a value')).toBe(true); // substring semantics, like grep
|
|
expect(regex.test('unrelated line')).toBe(false);
|
|
});
|
|
|
|
it('supports the schema example "console\\.log" against real code lines', () => {
|
|
const { regex } = parseGrepQuery({ pattern: 'console\\.log' });
|
|
expect(regex.test('console.log("hi")')).toBe(true);
|
|
expect(regex.test('consolexlog("hi")')).toBe(false);
|
|
});
|
|
|
|
it('is case-insensitive by default', () => {
|
|
const { regex } = parseGrepQuery({ pattern: 'todo' });
|
|
expect(regex.test('TODO: fix me')).toBe(true);
|
|
});
|
|
|
|
it('caseSensitive=1 drops the i flag', () => {
|
|
const { regex } = parseGrepQuery({ pattern: 'todo', caseSensitive: '1' });
|
|
expect(regex.test('TODO: fix me')).toBe(false);
|
|
expect(regex.test('todo: fix me')).toBe(true);
|
|
});
|
|
|
|
it('caseSensitive=true is accepted alongside the bare flag form', () => {
|
|
const { regex } = parseGrepQuery({ pattern: 'todo', caseSensitive: 'true' });
|
|
expect(regex.test('TODO')).toBe(false);
|
|
});
|
|
|
|
it('literal=1 restores the old escaped-substring behaviour', () => {
|
|
const literal = parseGrepQuery({ pattern: 'a.b', literal: '1' });
|
|
expect(literal.regex.test('a.b')).toBe(true);
|
|
expect(literal.regex.test('axb')).toBe(false);
|
|
|
|
const regexMode = parseGrepQuery({ pattern: 'a.b' });
|
|
expect(regexMode.regex.test('axb')).toBe(true);
|
|
});
|
|
|
|
it('matches CJK identifiers/comments (the primary consumer is a Chinese-codebase team)', () => {
|
|
const { regex } = parseGrepQuery({ pattern: '签署|signOrder' });
|
|
expect(regex.test('public void signOrder() { // 医嘱签署')).toBe(true);
|
|
expect(regex.test('// 签署接口')).toBe(true);
|
|
expect(regex.test('// 撤销接口')).toBe(false);
|
|
});
|
|
|
|
it('anchors ^/$ at line boundaries — the handler tests one line at a time, so "m" semantics are irrelevant', () => {
|
|
const { regex } = parseGrepQuery({ pattern: '^import .*$' });
|
|
expect(regex.test('import path from "path";')).toBe(true);
|
|
expect(regex.test(' import path from "path";')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('parseGrepQuery — fileFilter', () => {
|
|
it('normalizes to a lowercase path substring', () => {
|
|
const { fileFilter } = parseGrepQuery({ pattern: 'x', fileFilter: 'Controller.JAVA' });
|
|
expect(fileFilter).toBe('controller.java');
|
|
});
|
|
|
|
it('empty / missing fileFilter disables path filtering', () => {
|
|
expect(parseGrepQuery({ pattern: 'x' }).fileFilter).toBe('');
|
|
expect(parseGrepQuery({ pattern: 'x', fileFilter: '' }).fileFilter).toBe('');
|
|
});
|
|
|
|
it('array-form fileFilter is type-confusion-rejected, not partially read', () => {
|
|
expect(() => parseGrepQuery({ pattern: 'x', fileFilter: ['a', 'b'] })).toThrow(BadRequestError);
|
|
});
|
|
});
|
|
|
|
describe('parseGrepQuery — limit clamping', () => {
|
|
it('clamps above the max', () => {
|
|
expect(parseGrepQuery({ pattern: 'x', limit: '5000' }).limit).toBe(GREP_MAX_LIMIT);
|
|
});
|
|
|
|
it('clamps below 1', () => {
|
|
expect(parseGrepQuery({ pattern: 'x', limit: '-5' }).limit).toBe(1);
|
|
});
|
|
|
|
it('falls back to the default on non-numeric input', () => {
|
|
expect(parseGrepQuery({ pattern: 'x', limit: 'abc' }).limit).toBe(GREP_DEFAULT_LIMIT);
|
|
});
|
|
|
|
it('defaults when absent', () => {
|
|
expect(parseGrepQuery({ pattern: 'x' }).limit).toBe(GREP_DEFAULT_LIMIT);
|
|
});
|
|
});
|
|
|
|
describe('parseGrepQuery — error paths (→ 400 via statusFromError)', () => {
|
|
it('rejects a missing pattern', () => {
|
|
expect(() => parseGrepQuery({})).toThrow(/Missing "pattern" query parameter/);
|
|
expect(() => parseGrepQuery({ pattern: '' })).toThrow(/Missing "pattern" query parameter/);
|
|
});
|
|
|
|
it('rejects array-form patterns (type-confusion guard unchanged)', () => {
|
|
try {
|
|
parseGrepQuery({ pattern: ['a', 'b'] });
|
|
expect.unreachable();
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(BadRequestError);
|
|
expect((err as BadRequestError).status).toBe(400);
|
|
expect((err as Error).message).toContain('pattern');
|
|
}
|
|
});
|
|
|
|
it('rejects over-long patterns at the same 200-char cap', () => {
|
|
const long = 'a'.repeat(GREP_PATTERN_MAX_LENGTH + 1);
|
|
expect(() => parseGrepQuery({ pattern: long })).toThrow(/max 200 characters/);
|
|
expect(() => parseGrepQuery({ pattern: 'a'.repeat(GREP_PATTERN_MAX_LENGTH) })).not.toThrow();
|
|
});
|
|
|
|
it('rejects syntactically invalid regex', () => {
|
|
expect(() => parseGrepQuery({ pattern: '((' })).toThrow(/Invalid regex/);
|
|
});
|
|
});
|
|
|
|
describe('/api/grep handler wiring (source-level, api-readonly-wiring.test.ts style)', () => {
|
|
const readSource = async () => fs.readFile(SRC_PATH, 'utf-8');
|
|
const SRC_PATH = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../../src/server/api.ts',
|
|
);
|
|
|
|
it('threaded through: parseGrepQuery call, worker scan, fileFilter filter, timedOut flag', async () => {
|
|
const source = await readSource();
|
|
const grepSection = source.match(/app\.get\('\/api\/grep'[\s\S]*?\n \}\);/);
|
|
expect(grepSection).not.toBeNull();
|
|
const section = grepSection![0];
|
|
expect(section).toContain('parseGrepQuery(');
|
|
expect(section).toContain('GREP_TIME_BUDGET_MS');
|
|
expect(section).toContain('runGrepScanInWorker(');
|
|
expect(section).toMatch(/filePath\.toLowerCase\(\)\.includes\(fileFilter\)/);
|
|
expect(section).toContain('timedOut: true');
|
|
// Unchanged read-only DB open — now expressed via readOnlyFtsOptions(skipFts),
|
|
// whose read-only guarantee is pinned in api-readonly-wiring.test.ts.
|
|
expect(section).toMatch(/readOnly: true|readOnlyFtsOptions\(/);
|
|
});
|
|
});
|