GitNexus/gitnexus/test/unit/chunker.test.ts
mengkaka 4154b63131
feat(indexing): add Objective-C semantic indexing support (#3179)
* docs: add Objective-C fork provider notes

* feat(objective-c): add deterministic provider and grammar

* feat(objective-c): finalize provider MVP

* fix(objective-c): harden provider integration

* fix(objective-c): normalize bare macro markers

* docs(objective-c): integrate provider documentation

* fix(objective-c): harden resolution and header classification

* fix(objective-c): complete provider follow-ups

* fix: address Objective-C review follow-ups

* chore: format Objective-C grammar sources

* fix(objective-c): harden review follow-ups

* Address PR review feedback (#3179)

Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address follow-up review on ObjC chunking and language fallback.

Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parse Objective-C headers with the objc grammar in embeddings.

ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(bench): rebaseline Objective-C CSV emit

* feat(objective-c): add workspace resolution and linear emit benches

Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.

Refs #3179

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.

Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:40:21 +00:00

838 lines
30 KiB
TypeScript

/**
* Unit tests for character chunking and AST-aware chunking logic.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { characterChunk } from '../../src/core/embeddings/character-chunk.js';
const { createParserForLanguage, resolveLanguageKey } = vi.hoisted(() => ({
createParserForLanguage: vi.fn(),
resolveLanguageKey: vi.fn((language: string, filePath?: string) =>
language === 'typescript' && filePath?.endsWith('.tsx') ? 'typescript:tsx' : language,
),
}));
const { getLanguageFromFilename } = vi.hoisted(() => ({
getLanguageFromFilename: vi.fn((filePath: string) => {
if (filePath.endsWith('.m') || filePath.endsWith('.mm')) return 'objective-c';
return filePath.endsWith('.rs') ? 'rust' : 'typescript';
}),
}));
vi.mock('../../src/core/tree-sitter/parser-loader.js', () => ({
createParserForLanguage,
isLanguageAvailable: vi.fn().mockReturnValue(true),
resolveLanguageKey,
}));
// Partial mock: `ast-utils` now resolves the LanguageProvider registry to apply
// `preprocessSource`, and that graph needs the real shared exports (#2771).
vi.mock('gitnexus-shared', async (importOriginal) => ({
...(await importOriginal<typeof import('gitnexus-shared')>()),
getLanguageFromFilename,
}));
import { chunkNode } from '../../src/core/embeddings/chunker.js';
type FakeNode = {
type: string;
startIndex: number;
endIndex: number;
namedChildCount: number;
namedChild: (index: number) => FakeNode | null;
childForFieldName?: (name: string) => FakeNode | null;
};
const makeFakeNode = (
type: string,
startIndex: number,
endIndex: number,
children: FakeNode[] = [],
fields: Record<string, FakeNode> = {},
): FakeNode => ({
type,
startIndex,
endIndex,
namedChildCount: children.length,
namedChild: (index: number) => children[index] ?? null,
childForFieldName: (name: string) => fields[name] ?? null,
});
const makeFunctionTree = (content: string, statementTexts: string[]) => {
const statementNodes = statementTexts.map((text) => {
const startIndex = content.indexOf(text);
return makeFakeNode('expression_statement', startIndex, startIndex + text.length);
});
const bodyStart = content.indexOf('{');
const bodyEnd = content.lastIndexOf('}') + 1;
const bodyNode = makeFakeNode('statement_block', bodyStart, bodyEnd, statementNodes);
const fnNode = makeFakeNode('function_declaration', 0, bodyEnd, [], { body: bodyNode });
const root = makeFakeNode('program', 0, content.length, [fnNode]);
return {
rootNode: root,
};
};
const makeTypedFunctionTree = (nodeType: string, content: string, statementTexts: string[]) => {
const statementNodes = statementTexts.map((text) => {
const startIndex = content.indexOf(text);
return makeFakeNode('expression_statement', startIndex, startIndex + text.length);
});
const bodyStart = content.indexOf('{');
const bodyEnd = content.lastIndexOf('}') + 1;
const bodyNode = makeFakeNode('statement_block', bodyStart, bodyEnd, statementNodes);
const fnNode = makeFakeNode(nodeType, 0, bodyEnd, [], { body: bodyNode });
const root = makeFakeNode('program', 0, content.length, [fnNode]);
return {
rootNode: root,
};
};
const makeDeclarationTree = (
nodeType: string,
bodyType: string,
content: string,
memberTexts: string[],
) => {
let searchFrom = 0;
const memberNodes = memberTexts.map((text) => {
const startIndex = content.indexOf(text, searchFrom);
if (startIndex < 0) {
throw new Error(`Unable to locate member text: ${text}`);
}
searchFrom = startIndex + text.length;
const inferredType =
text.includes('()') || text.includes(': void') || text.includes(': boolean')
? 'method_definition'
: 'field_definition';
return makeFakeNode(inferredType, startIndex, startIndex + text.length);
});
const bodyStart = content.indexOf('{');
const bodyEnd = content.lastIndexOf('}') + 1;
const bodyNode = makeFakeNode(bodyType, bodyStart, bodyEnd, memberNodes);
const declNode = makeFakeNode(nodeType, 0, bodyEnd, [bodyNode], { body: bodyNode });
const root = makeFakeNode('program', 0, content.length, [declNode]);
return {
rootNode: root,
};
};
const makeObjectiveCDeclarationTree = (
nodeType: 'protocol_declaration' | 'class_interface',
content: string,
memberTexts: string[],
) => {
const headerName = nodeType === 'protocol_declaration' ? 'Worker' : 'Worker (Tracing)';
const headerNode = makeFakeNode(
'identifier',
content.indexOf(headerName),
content.indexOf(headerName) + headerName.length,
);
let searchFrom = 0;
const memberNodes = memberTexts.map((text) => {
const startIndex = content.indexOf(text, searchFrom);
if (startIndex < 0) throw new Error(`Unable to locate member text: ${text}`);
searchFrom = startIndex + text.length;
return makeFakeNode('method_declaration', startIndex, startIndex + text.length);
});
const declNode = makeFakeNode(nodeType, 0, content.length, [headerNode, ...memberNodes]);
return { rootNode: makeFakeNode('program', 0, content.length, [declNode]) };
};
describe('characterChunk', () => {
it('returns single chunk when content fits', () => {
const result = characterChunk('short content', 1, 5, 1200, 120);
expect(result).toHaveLength(1);
expect(result[0].text).toBe('short content');
expect(result[0].chunkIndex).toBe(0);
expect(result[0].startOffset).toBe(0);
expect(result[0].endOffset).toBe('short content'.length);
expect(result[0].startLine).toBe(1);
expect(result[0].endLine).toBe(5);
});
it('splits long content into multiple chunks', () => {
const longContent = 'a'.repeat(3000);
const result = characterChunk(longContent, 1, 100, 1200, 120);
expect(result.length).toBeGreaterThan(1);
for (const chunk of result) {
expect(chunk.text.length).toBeLessThanOrEqual(1200);
}
});
it('maintains sequential chunkIndex and offsets', () => {
const longContent = 'x'.repeat(3000);
const result = characterChunk(longContent, 1, 100, 1200, 120);
for (let i = 0; i < result.length; i++) {
expect(result[i].chunkIndex).toBe(i);
expect(result[i].text).toBe(longContent.slice(result[i].startOffset, result[i].endOffset));
}
});
it('includes overlap between chunks', () => {
const content = 'abcdefghij'.repeat(200);
const result = characterChunk(content, 1, 50, 500, 50);
if (result.length > 1) {
const endOfFirst = result[0].text.slice(-50);
expect(result[1].text.startsWith(endOfFirst)).toBe(true);
}
});
it('keeps the first chunk on the real starting line', () => {
const content = 'alpha\nbeta\ngamma';
const result = characterChunk(content, 38, 40, 6, 0);
expect(result[0].startLine).toBe(38);
});
it('does not advance endLine when a chunk ends at a newline boundary', () => {
const content = 'aaa\nbbb\nccc';
const result = characterChunk(content, 10, 12, 4, 0);
expect(result[0].text).toBe('aaa\n');
expect(result[0].startLine).toBe(10);
expect(result[0].endLine).toBe(10);
});
});
describe('chunkNode', () => {
beforeEach(() => {
createParserForLanguage.mockReset();
resolveLanguageKey.mockReset();
resolveLanguageKey.mockImplementation(
(language: string, filePath?: string) => `${language}:${filePath ?? ''}`,
);
getLanguageFromFilename.mockImplementation((filePath: string) => {
if (filePath.endsWith('.m') || filePath.endsWith('.mm')) return 'objective-c';
return filePath.endsWith('.rs') ? 'rust' : 'typescript';
});
});
it('returns single chunk for short content', async () => {
const result = await chunkNode('Function', 'short', 'test.ts', 1, 5, 1200, 120);
expect(result).toHaveLength(1);
expect(result[0].chunkIndex).toBe(0);
expect(result[0].text).toBe('short');
expect(result[0].startOffset).toBe(0);
});
it('splits a class by members instead of raw character windows', async () => {
const content = [
'class Parser {',
' options: ParserOptions;',
' cache: Map<string, any>;',
' parseJSON() { return JSON.parse("{}"); }',
' validate() { return true; }',
'}',
].join('\n');
const tree = makeDeclarationTree('class_declaration', 'class_body', content, [
'options: ParserOptions;',
'cache: Map<string, any>;',
'parseJSON() { return JSON.parse("{}"); }',
'validate() { return true; }',
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(tree),
});
const result = await chunkNode('Class', content, 'test.ts', 1, 6, 90, 0);
expect(result).toHaveLength(2);
expect(result[0].text).toContain('class Parser {');
expect(result[0].text).toContain('options: ParserOptions;');
expect(result[0].text).toContain('cache: Map<string, any>;');
expect(result[1].text).toContain('parseJSON()');
expect(result[1].text).toContain('validate()');
expect(result[0].startLine).toBe(1);
expect(result[1].startLine).toBe(4);
});
it('preserves interface signatures via declaration-aware chunking', async () => {
const content = [
'interface Handler {',
' handle(event: Event): void;',
' validate(input: string): boolean;',
' readonly name: string;',
'}',
].join('\n');
const tree = makeDeclarationTree('interface_declaration', 'object_type', content, [
'handle(event: Event): void;',
'validate(input: string): boolean;',
'readonly name: string;',
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(tree),
});
const result = await chunkNode('Interface', content, 'test.ts', 10, 14, 500, 0);
expect(result).toHaveLength(1);
expect(result[0].text).toContain('interface Handler {');
expect(result[0].text).toContain('handle(event: Event): void;');
expect(result[0].text).toContain('validate(input: string): boolean;');
expect(result[0].text).toContain('readonly name: string;');
});
it('uses declaration-aware chunking for Struct labels', async () => {
const content = [
'struct User {',
' name: String,',
' email: String,',
' age: u32,',
' address: String,',
'}',
].join('\n');
const tree = makeDeclarationTree('struct_item', 'declaration_list', content, [
'name: String,',
'email: String,',
'age: u32,',
'address: String,',
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(tree),
});
const result = await chunkNode('Struct', content, 'test.rs', 40, 45, 45, 0);
expect(result).toHaveLength(2);
expect(result[0].text).toContain('struct User {');
expect(result[0].text).toContain('name: String,');
expect(result[0].text).toContain('email: String');
const combinedText = result.map((chunk) => chunk.text).join('\n');
expect(combinedText).toContain('email: String');
expect(combinedText).toContain('age: u32');
expect(combinedText).toContain('address: String');
expect(result[0].startLine).toBe(40);
});
it.each([
{
label: 'Protocol',
nodeType: 'protocol_declaration' as const,
filePath: 'Worker.m',
content: [
'@protocol Worker',
'- (void)startWithConfiguration:(id)configuration;',
'- (void)stopWithCompletion:(id)completion;',
'- (void)reloadWithOptions:(id)options;',
'@end',
].join('\n'),
},
{
label: 'Category',
nodeType: 'class_interface' as const,
filePath: 'Worker.mm',
content: [
'@interface Worker (Tracing)',
'- (void)startWithConfiguration:(id)configuration;',
'- (void)stopWithCompletion:(id)completion;',
'- (void)reloadWithOptions:(id)options;',
'@end',
].join('\n'),
},
])(
'chunks Objective-C $label declarations at member boundaries',
async ({ label, nodeType, filePath, content }) => {
const members = [
'- (void)startWithConfiguration:(id)configuration;',
'- (void)stopWithCompletion:(id)completion;',
'- (void)reloadWithOptions:(id)options;',
];
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(makeObjectiveCDeclarationTree(nodeType, content, members)),
});
const result = await chunkNode(label, content, filePath, 1, 5, 90, 0);
expect(result).toHaveLength(2);
expect(result[0].text).toContain(members[0]);
expect(result.slice(1).every((chunk) => chunk.text.startsWith('- (void)'))).toBe(true);
expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', filePath);
},
);
it('skips Objective-C parameterized interface arguments as declaration members', async () => {
const content = ['@interface Worker <Runnable>', '- (void)run;', '@end'].join('\n');
const nameStart = content.indexOf('Worker');
const argumentsStart = content.indexOf('<Runnable>');
const methodStart = content.indexOf('- (void)run;');
const declaration = makeFakeNode('class_interface', 0, content.length, [
makeFakeNode('identifier', nameStart, nameStart + 'Worker'.length),
makeFakeNode('parameterized_arguments', argumentsStart, argumentsStart + '<Runnable>'.length),
makeFakeNode('method_declaration', methodStart, methodStart + '- (void)run;'.length),
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Class', content, 'ParameterizedWorker.m', 1, 3, 40, 0);
expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', 'ParameterizedWorker.m');
expect(result[0].text).toContain('- (void)run');
});
it('skips Objective-C generic argument lists as declaration members', async () => {
const content = ['@interface Worker(Tracing)', '- (void)run;', '@end'].join('\n');
const nameStart = content.indexOf('Worker');
const argumentsStart = content.indexOf('(Tracing)');
const methodStart = content.indexOf('- (void)run;');
const declaration = makeFakeNode('class_interface', 0, content.length, [
makeFakeNode('identifier', nameStart, nameStart + 'Worker'.length),
makeFakeNode('generic_arguments', argumentsStart, argumentsStart + '(Tracing)'.length),
makeFakeNode('method_declaration', methodStart, methodStart + '- (void)run;'.length),
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Class', content, 'GenericCategoryWorker.m', 1, 3, 40, 0);
expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', 'GenericCategoryWorker.m');
expect(result[0].text).toContain('- (void)run');
expect(result.some((chunk) => chunk.text === '(Tracing)')).toBe(false);
});
it('chunks an Objective-C implementation at method boundaries', async () => {
const content = [
'@implementation Worker',
'- (void)first {}',
'- (void)second {}',
'@end',
].join('\n');
const firstMethod = '- (void)first {}';
const secondMethod = '- (void)second {}';
const firstImplementation = makeFakeNode(
'implementation_definition',
content.indexOf(firstMethod),
content.indexOf(firstMethod) + firstMethod.length,
[
makeFakeNode(
'method_definition',
content.indexOf(firstMethod),
content.indexOf(firstMethod) + firstMethod.length,
),
],
);
const secondImplementation = makeFakeNode(
'implementation_definition',
content.indexOf(secondMethod),
content.indexOf(secondMethod) + secondMethod.length,
[
makeFakeNode(
'method_definition',
content.indexOf(secondMethod),
content.indexOf(secondMethod) + secondMethod.length,
),
],
);
const declaration = makeFakeNode('class_implementation', 0, content.indexOf('@end') + 4, [
makeFakeNode(
'identifier',
content.indexOf('Worker'),
content.indexOf('Worker') + 'Worker'.length,
),
firstImplementation,
secondImplementation,
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Class', content, 'ImplementationWorker.m', 1, 4, 48, 0);
expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', 'ImplementationWorker.m');
expect(result).toHaveLength(2);
expect(result[0].text).toContain(firstMethod);
expect(result[1].text).toContain(secondMethod);
expect(result[1].text).not.toContain(firstMethod);
});
it('parses distinct Objective-C sources correctly through one cached parser', async () => {
const protocolMethods = [
'- (void)startWithConfiguration:(id)configuration;',
'- (void)stopWithCompletion:(id)completion;',
'- (void)reloadWithOptions:(id)options;',
];
const categoryMethods = [
'- (void)traceStartWithConfiguration:(id)configuration;',
'- (void)traceStopWithCompletion:(id)completion;',
'- (void)traceReloadWithOptions:(id)options;',
];
const protocolContent = ['@protocol Worker', ...protocolMethods, '@end'].join('\n');
const categoryContent = ['@interface Worker (Tracing)', ...categoryMethods, '@end'].join('\n');
const parser = {
parse: vi.fn((source: string) => {
if (source === protocolContent) {
return makeObjectiveCDeclarationTree(
'protocol_declaration',
protocolContent,
protocolMethods,
);
}
if (source === categoryContent) {
return makeObjectiveCDeclarationTree('class_interface', categoryContent, categoryMethods);
}
throw new Error(`Unexpected Objective-C source: ${source}`);
}),
};
createParserForLanguage.mockResolvedValue(parser);
const protocol = await chunkNode(
'Protocol',
protocolContent,
'CachedObjectiveC.m',
1,
5,
90,
0,
);
const category = await chunkNode(
'Category',
categoryContent,
'CachedObjectiveC.m',
1,
5,
90,
0,
);
expect(createParserForLanguage).toHaveBeenCalledTimes(1);
expect(protocol.map((chunk) => chunk.text).join('\n')).toContain(protocolMethods[2]);
expect(category.map((chunk) => chunk.text).join('\n')).toContain(categoryMethods[2]);
expect(parser.parse.mock.calls.map(([source]) => source)).toEqual([
protocolContent,
categoryContent,
]);
});
it('expands Objective-C protocol optional and required sections', async () => {
const content = [
'@protocol P',
'@optional',
'- (void)first;',
'- (void)second;',
'@required',
'- (void)third;',
'- (void)fourth;',
'@end',
].join('\n');
const members = ['- (void)first;', '- (void)second;', '- (void)third;', '- (void)fourth;'];
let searchFrom = 0;
const methodNodes = members.map((text) => {
const startIndex = content.indexOf(text, searchFrom);
searchFrom = startIndex + text.length;
return makeFakeNode('method_declaration', startIndex, startIndex + text.length);
});
const optionalStart = content.indexOf('@optional');
const requiredStart = content.indexOf('@required');
const optional = makeFakeNode(
'qualified_protocol_interface_declaration',
optionalStart,
methodNodes[1].endIndex,
methodNodes.slice(0, 2),
);
const required = makeFakeNode(
'qualified_protocol_interface_declaration',
requiredStart,
methodNodes[3].endIndex,
methodNodes.slice(2),
);
const header = makeFakeNode('identifier', content.indexOf('P'), content.indexOf('P') + 1);
const declaration = makeFakeNode('protocol_declaration', 0, content.length, [
header,
optional,
required,
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Protocol', content, 'ProtocolSections.m', 1, 8, 36, 0);
const combined = result.map((chunk) => chunk.text).join('\n');
const requiredChunk = result.find((chunk) => chunk.text.includes(members[2]));
expect(result.length).toBeGreaterThan(1);
for (const member of members) expect(combined).toContain(member);
expect(
result.some((chunk) => chunk.text.includes(members[0]) && chunk.text.includes(members[1])),
).toBe(false);
expect(requiredChunk?.text).toContain('@required');
expect(requiredChunk?.text).not.toContain(members[1]);
expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', 'ProtocolSections.m');
});
it('expands Objective-C instance variables before chunking a class declaration', async () => {
const content = [
'@interface Worker {',
' id _first;',
' id _second;',
'}',
'- (void)run;',
'@end',
].join('\n');
const firstIvar = 'id _first;';
const secondIvar = 'id _second;';
const method = '- (void)run;';
const firstIvarStart = content.indexOf(firstIvar);
const secondIvarStart = content.indexOf(secondIvar);
const methodStart = content.indexOf(method);
const instanceVariables = makeFakeNode(
'instance_variables',
content.indexOf('{'),
content.indexOf('}') + 1,
[
makeFakeNode('instance_variable', firstIvarStart, firstIvarStart + firstIvar.length),
makeFakeNode('instance_variable', secondIvarStart, secondIvarStart + secondIvar.length),
],
);
const declaration = makeFakeNode('class_interface', 0, content.length, [
makeFakeNode(
'identifier',
content.indexOf('Worker'),
content.indexOf('Worker') + 'Worker'.length,
),
instanceVariables,
makeFakeNode('method_declaration', methodStart, methodStart + method.length),
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Class', content, 'Worker.m', 1, 6, 48, 0);
const combined = result.map((chunk) => chunk.text).join('\n');
expect(result.length).toBeGreaterThan(1);
expect(combined).toContain(firstIvar);
expect(combined).toContain(secondIvar);
expect(combined).toContain(method);
expect(combined).toContain('}');
expect(
result.some((chunk) => chunk.text.includes(firstIvar) && chunk.text.includes(secondIvar)),
).toBe(true);
expect(
result.some((chunk) => chunk.text.includes(secondIvar) && chunk.text.includes('}')),
).toBe(true);
});
it('keeps ivar attributes attached to the following instance variable', async () => {
const extraMethods = ['- (void)alpha;', '- (void)bravo;', '- (void)charlie;'];
const content = [
'@interface Worker {',
' __attribute__((unused)) id _first;',
'}',
...extraMethods,
'@end',
].join('\n');
const attribute = '__attribute__((unused))';
const firstIvar = 'id _first;';
const attrStart = content.indexOf(attribute);
const firstIvarStart = content.indexOf(firstIvar);
const instanceVariables = makeFakeNode(
'instance_variables',
content.indexOf('{'),
content.indexOf('}') + 1,
[
makeFakeNode('attribute_specifier', attrStart, attrStart + attribute.length),
makeFakeNode('instance_variable', firstIvarStart, firstIvarStart + firstIvar.length),
],
);
const methodNodes = extraMethods.map((method) => {
const methodStart = content.indexOf(method);
return makeFakeNode('method_declaration', methodStart, methodStart + method.length);
});
const declaration = makeFakeNode('class_interface', 0, content.length, [
makeFakeNode(
'identifier',
content.indexOf('Worker'),
content.indexOf('Worker') + 'Worker'.length,
),
instanceVariables,
...methodNodes,
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
// First-chunk prefix starts at `@interface`, so size must cover that
// prefix plus the ivar unit. Smaller sizes fall into characterChunk and
// can split the attribute token itself.
const chunkSize = content.indexOf('}') + 2;
const result = await chunkNode(
'Class',
content,
'WorkerAttr.m',
1,
extraMethods.length + 4,
chunkSize,
0,
);
expect(content.length).toBeGreaterThan(chunkSize);
expect(
result.some((chunk) => chunk.text.includes(attribute) && !chunk.text.includes(firstIvar)),
).toBe(false);
expect(
result.some((chunk) => chunk.text.includes(attribute) && chunk.text.includes(firstIvar)),
).toBe(true);
});
it('keeps Objective-C declaration modifiers in the class prefix', async () => {
const content = ['NS_ROOT_CLASS @interface Worker', '- (void)run;', '@end'].join('\n');
const modifier = 'NS_ROOT_CLASS';
const method = '- (void)run;';
const methodStart = content.indexOf(method);
const declaration = makeFakeNode('class_interface', 0, content.length, [
makeFakeNode('storage_class_specifier', 0, modifier.length),
makeFakeNode(
'identifier',
content.indexOf('Worker'),
content.indexOf('Worker') + 'Worker'.length,
),
makeFakeNode('method_declaration', methodStart, methodStart + method.length),
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Class', content, 'RootClass.m', 1, 3, 80, 0);
expect(result[0].text).toContain(method);
expect(result.some((chunk) => chunk.text.trim() === modifier)).toBe(false);
});
it('keeps Objective-C protocol inheritance in the declaration prefix', async () => {
const content = ['@protocol Worker <Runnable, Observable>', '- (void)run;', '@end'].join('\n');
const protocolNameStart = content.indexOf('Worker');
const inheritanceStart = content.indexOf('<Runnable, Observable>');
const methodStart = content.indexOf('- (void)run;');
const declaration = makeFakeNode('protocol_declaration', 0, content.length, [
makeFakeNode('identifier', protocolNameStart, protocolNameStart + 'Worker'.length),
makeFakeNode(
'protocol_reference_list',
inheritanceStart,
inheritanceStart + '<Runnable, Observable>'.length,
),
makeFakeNode('method_declaration', methodStart, methodStart + '- (void)run;'.length),
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue({
rootNode: makeFakeNode('program', 0, content.length, [declaration]),
}),
});
const result = await chunkNode('Protocol', content, 'ProtocolInheritance.m', 1, 3, 50, 0);
expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', 'ProtocolInheritance.m');
expect(result[0].text).toContain('- (void)');
expect(result[0].text).not.toBe('@protocol Worker <Runnable, Observable>');
});
it('splits a function into multiple AST-aware chunks using snippet offsets', async () => {
const content = [
'function example() {',
' const first = 1;',
'',
' const second = 2;',
' return first + second;',
'}',
].join('\n');
const tree = makeFunctionTree(content, [
'const first = 1;',
'const second = 2;',
'return first + second;',
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(tree),
});
const result = await chunkNode('Function', content, 'test.ts', 38, 43, 68, 0);
expect(result).toHaveLength(2);
expect(result[0].startOffset).toBe(0);
expect(result[0].endOffset).toBeGreaterThan(content.indexOf('const second = 2;'));
expect(result[0].startLine).toBe(38);
expect(result[0].endLine).toBe(42);
expect(result[0].text).toContain('function example() {');
expect(result[0].text).toContain('\n\n const second = 2;');
expect(result[1].startOffset).toBeGreaterThan(content.indexOf('const second = 2;'));
expect(result[1].startLine).toBeGreaterThanOrEqual(42);
expect(result[1].endLine).toBe(43);
expect(result[1].text.length).toBeGreaterThan(0);
});
it('uses AST-aware chunking for Constructor labels too', async () => {
const content = [
'constructor() {',
' this.ready = true;',
' this.mode = "prod";',
' this.start();',
'}',
].join('\n');
const tree = makeFunctionTree(content, [
'this.ready = true;',
'this.mode = "prod";',
'this.start();',
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(tree),
});
const result = await chunkNode('Constructor', content, 'test.ts', 12, 16, 55, 0);
expect(result).toHaveLength(2);
expect(result[0].text).toContain('constructor() {');
expect(result[0].startLine).toBe(12);
expect(result[1].startLine).toBe(14);
});
it('recognizes Rust function_item nodes for AST-aware chunking', async () => {
const content = [
'fn build_user() {',
' let first = 1;',
' let second = 2;',
' return first + second;',
'}',
].join('\n');
const tree = makeTypedFunctionTree('function_item', content, [
'let first = 1;',
'let second = 2;',
'return first + second;',
]);
createParserForLanguage.mockResolvedValue({
parse: vi.fn().mockReturnValue(tree),
});
const result = await chunkNode('Function', content, 'test.rs', 20, 24, 52, 0);
expect(result).toHaveLength(2);
expect(result[0].text).toContain('fn build_user() {');
expect(result[0].startLine).toBe(20);
expect(result[1].text).toContain('return first + second;');
});
it('falls back to character chunks when AST parsing fails', async () => {
createParserForLanguage.mockRejectedValueOnce(new Error('no parser'));
const content = 'x'.repeat(3000);
const result = await chunkNode('Function', content, 'test.tsx', 1, 100, 1200, 120);
expect(result.length).toBeGreaterThan(1);
expect(result[0].startOffset).toBe(0);
});
});