import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { searchFTSFromLbug, type BM25SearchResult } from '../../src/core/search/bm25-index.js'; import { classifyFtsQueryError } from '../../src/core/lbug/lbug-adapter.js'; import { extensionManager, resetExtensionState } from '../../src/core/lbug/extension-loader.js'; import { FTS_INDEXES } from '../../src/core/search/fts-schema.js'; vi.mock('../../src/core/lbug/lbug-adapter.js', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, queryFTS: vi.fn().mockResolvedValue([]), createFTSIndex: vi.fn().mockResolvedValue(undefined), dropFTSIndex: vi.fn().mockResolvedValue(undefined), }; }); // Pool adapter is dynamically imported by the MCP-pool path of // `searchFTSFromLbug`. We mock it so we can drive the executor without // spinning up a real LadybugDB pool. const mockExecuteParameterized = vi.fn(); vi.mock('../../src/core/lbug/pool-adapter.js', () => ({ executeParameterized: (repoId: string, cypher: string, params: Record) => mockExecuteParameterized(repoId, cypher, params), addPoolCloseListener: vi.fn(), })); describe('BM25 search', () => { describe('createSearchFTSIndexes', () => { beforeEach(() => { vi.clearAllMocks(); }); it('creates every configured index on the writable analysis path', async () => { const { createFTSIndex } = await import('../../src/core/lbug/lbug-adapter.js'); const { createSearchFTSIndexes } = await import('../../src/core/search/fts-indexes.js'); await createSearchFTSIndexes(); expect(vi.mocked(createFTSIndex).mock.calls).toEqual( FTS_INDEXES.map((i) => [i.table, i.indexName, [...i.properties], 'porter']), ); }); it('returns no missing indexes when every configured index covers its columns', async () => { // One SHOW_INDEXES call returns a catalog row per configured index, each // covering exactly its expected properties. const showIndexesRows = FTS_INDEXES.map((i) => ({ index_name: i.indexName, property_names: [...i.properties], })); const executeQuery = vi.fn().mockResolvedValue(showIndexesRows); const { verifySearchFTSIndexes } = await import('../../src/core/search/fts-indexes.js'); const missing = await verifySearchFTSIndexes(executeQuery); expect(missing).toEqual([]); expect(executeQuery).toHaveBeenCalledTimes(1); }); it('reports an index that exists but does not cover its configured columns', async () => { // Model a pre-#2299 stale Function index: present, but name+content only, // missing `description`. Every other index covers its columns. const staleIndex = 'function_fts'; const showIndexesRows = FTS_INDEXES.map((i) => ({ index_name: i.indexName, property_names: i.indexName === staleIndex ? ['name', 'content'] : [...i.properties], })); const executeQuery = vi.fn().mockResolvedValue(showIndexesRows); const { verifySearchFTSIndexes } = await import('../../src/core/search/fts-indexes.js'); const missing = await verifySearchFTSIndexes(executeQuery); expect(missing).toEqual(['Function.function_fts']); }); it('reports an index that is absent from the catalog entirely', async () => { // Every configured index present and covering, except const_fts is missing. const absentIndex = 'const_fts'; const showIndexesRows = FTS_INDEXES.filter((i) => i.indexName !== absentIndex).map((i) => ({ index_name: i.indexName, property_names: [...i.properties], })); const executeQuery = vi.fn().mockResolvedValue(showIndexesRows); const { verifySearchFTSIndexes } = await import('../../src/core/search/fts-indexes.js'); const missing = await verifySearchFTSIndexes(executeQuery); expect(missing).toEqual(['Const.const_fts']); }); }); describe('searchFTSFromLbug', () => { it('returns empty results when LadybugDB is not initialized', async () => { // Simulate an uninitialized DB: queryFTS throws instead of returning rows const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockRejectedValue(new Error('DB not initialized')); const { results, ftsAvailable } = await searchFTSFromLbug('test query'); expect(Array.isArray(results)).toBe(true); expect(results).toHaveLength(0); expect(ftsAvailable).toBe(false); }); it('handles empty query', async () => { const { results } = await searchFTSFromLbug(''); expect(Array.isArray(results)).toBe(true); }); it('accepts custom limit parameter', async () => { const { results } = await searchFTSFromLbug('test', 5); expect(Array.isArray(results)).toBe(true); }); }); describe('BM25SearchResult type', () => { it('has correct shape', () => { const result: BM25SearchResult = { filePath: 'src/index.ts', score: 1.5, rank: 1, }; expect(result.filePath).toBe('src/index.ts'); expect(result.score).toBe(1.5); expect(result.rank).toBe(1); }); it('accepts optional nodeIds field', () => { const result: BM25SearchResult = { filePath: 'src/index.ts', score: 1.5, rank: 1, nodeIds: ['func:id1', 'func:id2'], }; expect(result.nodeIds).toEqual(['func:id1', 'func:id2']); }); }); describe('score aggregation', () => { beforeEach(() => { vi.clearAllMocks(); }); it('sums only top-3 scoring nodes per file when more than 3 match', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); // File table: empty; Function table: 5 hits for the same file; rest: empty vi.mocked(queryFTS) .mockResolvedValueOnce([]) // File .mockResolvedValueOnce([ // Function — 5 hits, scores 10/9/8/7/6 { filePath: 'src/views.py', score: 10, nodeId: 'func:node1', name: 'get_queryset' }, { filePath: 'src/views.py', score: 9, nodeId: 'func:node2', name: 'post' }, { filePath: 'src/views.py', score: 8, nodeId: 'func:node3', name: 'delete' }, { filePath: 'src/views.py', score: 7, nodeId: 'func:node4', name: 'patch' }, { filePath: 'src/views.py', score: 6, nodeId: 'func:node5', name: 'put' }, ]) .mockResolvedValueOnce([]) // Class .mockResolvedValueOnce([]) // Method .mockResolvedValueOnce([]); // Interface const { results } = await searchFTSFromLbug('queryset'); expect(results).toHaveLength(1); expect(results[0].filePath).toBe('src/views.py'); // Only top-3 scores (10+9+8=27), not naive sum of all 5 (10+9+8+7+6=40) expect(results[0].score).toBe(27); expect(results[0].nodeIds).toEqual(['func:node1', 'func:node2', 'func:node3']); }); it('propagates nodeIds for files with fewer than 3 matching nodes', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS) .mockResolvedValueOnce([]) // File .mockResolvedValueOnce([ // Function — 2 hits { filePath: 'src/models.py', score: 5, nodeId: 'func:m1', name: 'save' }, { filePath: 'src/models.py', score: 3, nodeId: 'func:m2', name: 'delete' }, ]) .mockResolvedValueOnce([]) // Class .mockResolvedValueOnce([]) // Method .mockResolvedValueOnce([]); // Interface const { results } = await searchFTSFromLbug('model'); expect(results).toHaveLength(1); expect(results[0].score).toBe(8); // 5+3 expect(results[0].nodeIds).toEqual(['func:m1', 'func:m2']); }); it('filters out empty nodeIds', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS) .mockResolvedValueOnce([]) // File .mockResolvedValueOnce([ // Function — nodes with no id { filePath: 'src/utils.py', score: 5, nodeId: '', name: 'helper' }, { filePath: 'src/utils.py', score: 3, nodeId: '', name: 'util' }, ]) .mockResolvedValueOnce([]) // Class .mockResolvedValueOnce([]) // Method .mockResolvedValueOnce([]); // Interface const { results } = await searchFTSFromLbug('util'); expect(results).toHaveLength(1); expect(results[0].nodeIds).toEqual([]); }); it('merges hits across multiple index tables for the same file', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS) .mockResolvedValueOnce([ // File table { filePath: 'src/auth.py', score: 4, nodeId: 'file:auth', name: 'auth.py' }, ]) .mockResolvedValueOnce([ // Function table { filePath: 'src/auth.py', score: 9, nodeId: 'func:login', name: 'login' }, ]) .mockResolvedValueOnce([ // Class table { filePath: 'src/auth.py', score: 7, nodeId: 'cls:User', name: 'User' }, ]) .mockResolvedValueOnce([]) // Method .mockResolvedValueOnce([]); // Interface const { results } = await searchFTSFromLbug('auth'); expect(results).toHaveLength(1); // All 3 hits (scores 9+7+4=20) — each from a different table, all top-3 expect(results[0].score).toBe(20); expect(results[0].nodeIds).toEqual(['func:login', 'cls:User', 'file:auth']); }); it('ranks files by aggregated score descending', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS) .mockResolvedValueOnce([]) // File .mockResolvedValueOnce([ // Function — hits across two files { filePath: 'src/low.py', score: 2, nodeId: 'func:a', name: 'a' }, { filePath: 'src/high.py', score: 9, nodeId: 'func:b', name: 'b' }, ]) .mockResolvedValueOnce([]) // Class .mockResolvedValueOnce([]) // Method .mockResolvedValueOnce([]); // Interface const { results } = await searchFTSFromLbug('fn'); expect(results[0].filePath).toBe('src/high.py'); expect(results[1].filePath).toBe('src/low.py'); expect(results[0].rank).toBe(1); expect(results[1].rank).toBe(2); }); }); describe('MCP pool path', () => { const REPO = 'test-repo-readonly-fts'; beforeEach(() => { mockExecuteParameterized.mockReset(); }); it('queries existing FTS indexes without issuing CREATE_FTS_INDEX', async () => { mockExecuteParameterized.mockImplementation( async (_repo: string, cypher: string, params: Record) => { if (cypher.includes('CREATE_FTS_INDEX')) { throw new Error('query path must stay read-only'); } if (params.query === 'login' && cypher.includes("QUERY_FTS_INDEX('Function'")) { return [{ node: { filePath: 'src/auth.ts', id: 'func:login' }, score: 8 }]; } return []; }, ); const { results } = await searchFTSFromLbug('login', 5, REPO); expect(results).toEqual([ { filePath: 'src/auth.ts', score: 8, rank: 1, nodeIds: ['func:login'] }, ]); expect( mockExecuteParameterized.mock.calls.some((c) => String(c[1]).includes('CREATE_FTS_INDEX')), ).toBe(false); }); it('binds FTS user query text as a parameter in pool mode', async () => { mockExecuteParameterized.mockResolvedValue([]); const userQuery = "BrowserWindow create delete set remove 'main' window"; await searchFTSFromLbug(userQuery, 5, REPO); expect(mockExecuteParameterized).toHaveBeenCalled(); for (const call of mockExecuteParameterized.mock.calls) { const cypher = String(call[1]); expect(cypher).toContain('$query'); expect(cypher).not.toContain(userQuery); expect(cypher.toUpperCase()).not.toMatch(/\bCREATE\b/); expect(cypher.toUpperCase()).not.toMatch(/\bDELETE\b/); expect(cypher.toUpperCase()).not.toMatch(/\bSET\b/); expect(cypher.toUpperCase()).not.toMatch(/\bREMOVE\b/); expect(call[2]).toEqual({ query: userQuery }); } }); it('uses the configured FTS query set on every call', async () => { mockExecuteParameterized.mockResolvedValue([]); await searchFTSFromLbug('anything', 5, REPO); const queryCalls = mockExecuteParameterized.mock.calls.filter((c) => String(c[1]).includes('QUERY_FTS_INDEX'), ); expect(queryCalls.map((c) => String(c[1]).match(/QUERY_FTS_INDEX\('([^']+)'/)?.[1])).toEqual( FTS_INDEXES.map((i) => i.table), ); }); }); describe('classifyFtsQueryError (#2767)', () => { it('classifies the real "doesn\'t have an index" message (confirmed against a live QUERY_FTS_INDEX call) as missing-index', () => { expect( classifyFtsQueryError( "Prepare failed: Binder exception: Table File doesn't have an index with name file_fts.", ), ).toBe('missing-index'); }); it('classifies the real "table does not exist" message (confirmed against a live QUERY_FTS_INDEX call on a nonexistent table) as missing-table, distinct from missing-index (tri-review NEW-6)', () => { // Empirically confirmed: the table-missing message uses "does not // exist", NOT "doesn't have an index with name" — a genuinely different // phrasing from missing-index, not the same condition under two names. // Conflating them was the exact bug: a corrupted/partial DB (table // itself gone) would have been silently treated as the ordinary // "index not built yet" case. expect( classifyFtsQueryError( 'Prepare failed: Binder exception: Table TotallyNonexistentTable does not exist.', ), ).toBe('missing-table'); }); it('classifies a Catalog-exception "does not exist" message as missing-table too (both exception classes covered)', () => { expect(classifyFtsQueryError('Catalog exception: Table SomeTable does not exist.')).toBe( 'missing-table', ); }); it('classifies the extension-unavailable Catalog exception as other, not benign (mirrors the confirmed DROP_FTS_INDEX shape for QUERY_FTS_INDEX)', () => { // Message shape confirmed for DROP_FTS_INDEX in // drop-fts-index-error-classification.test.ts; QUERY_FTS_INDEX would // fail identically when the extension isn't loaded (same catalog). expect( classifyFtsQueryError( "Catalog exception: function QUERY_FTS_INDEX is not defined. This function exists in the FTS extension. You can install and load the extension by running 'INSTALL FTS; LOAD EXTENSION FTS;'.", ), ).toBe('other'); }); it('does not misclassify a real, differently-classed error that echoes the benign phrase in its body', () => { // Adversarial case: a Runtime exception (not Binder/Catalog) that // happens to echo the user's own search text — which could itself // contain "does not exist" — must not be anchored away as benign. expect( classifyFtsQueryError( 'Runtime exception: FTS query syntax error near "the config file does not exist here"', ), ).toBe('other'); }); it('does not misclassify a real Binder-class error unrelated to a missing FTS index', () => { expect(classifyFtsQueryError('Binder exception: column X does not match expected type')).toBe( 'other', ); }); it('classifies any other message as other', () => { expect(classifyFtsQueryError('Query execution timed out after 30000ms')).toBe('other'); expect(classifyFtsQueryError('Connection pool exhausted')).toBe('other'); }); }); describe('MCP pool path — real vs benign FTS query errors (#2767)', () => { const REPO = 'test-repo-error-classification'; beforeEach(() => { mockExecuteParameterized.mockReset(); }); it('a benign missing-index error on every table leaves nonBenignErrors unset (unchanged behavior)', async () => { mockExecuteParameterized.mockRejectedValue( new Error("Binder exception: Table Function doesn't have an index with name function_fts."), ); const response = await searchFTSFromLbug('login', 5, REPO); expect(response.ftsAvailable).toBe(false); expect(response.nonBenignErrors).toBeUndefined(); }); it('a missing-table error (table itself gone, not just its FTS index) surfaces as non-benign — schema drift is not the ordinary degraded state (tri-review NEW-6)', async () => { mockExecuteParameterized.mockRejectedValue( new Error('Binder exception: Table Function does not exist.'), ); const response = await searchFTSFromLbug('login', 5, REPO); expect(response.ftsAvailable).toBe(false); expect(response.nonBenignErrors!.length).toBeGreaterThan(0); }); it('a real error on every table surfaces it in nonBenignErrors, redacted', async () => { mockExecuteParameterized.mockRejectedValue( new Error( 'Query execution failed: connection reset at /home/alice/.gitnexus/lbug/main.lbug', ), ); const response = await searchFTSFromLbug('login', 5, REPO); expect(response.ftsAvailable).toBe(false); expect(response.nonBenignErrors).toBeDefined(); expect(response.nonBenignErrors!.length).toBeGreaterThan(0); expect(response.nonBenignErrors![0]).toContain('connection reset'); expect(response.nonBenignErrors![0]).not.toMatch(/\/home\/alice/); }); it('a real error on one table while another succeeds is still reported (partial-failure gap closed)', async () => { let call = 0; mockExecuteParameterized.mockImplementation(async (_repo: string, cypher: string) => { call++; if (cypher.includes("QUERY_FTS_INDEX('Function'")) { throw new Error('Query execution timed out after 30000ms'); } if (cypher.includes("QUERY_FTS_INDEX('File'")) { return [{ node: { filePath: 'src/index.ts', id: 'file:index' }, score: 3 }]; } return []; }); const response = await searchFTSFromLbug('login', 5, REPO); // At least one table succeeded, so the client-visible availability // signal and result set are unaffected (regression guard). expect(response.ftsAvailable).toBe(true); expect(response.results.length).toBeGreaterThan(0); // But the real error on the OTHER table is not silently dropped. expect(response.nonBenignErrors).toBeDefined(); expect(response.nonBenignErrors![0]).toContain('timed out'); expect(call).toBe(FTS_INDEXES.length); }); }); describe('short-circuits when the FTS extension is unavailable (tri-review NEW-4)', () => { const REPO = 'test-repo-extension-unavailable'; afterEach(() => { resetExtensionState(); }); it('MCP pool path: skips per-table QUERY_FTS_INDEX calls and reports no nonBenignErrors when the extension failed to load', async () => { await extensionManager.ensure( vi.fn().mockRejectedValue(new Error('invalid ELF header.')), 'fts', 'FTS', { policy: 'load-only' }, ); mockExecuteParameterized.mockReset(); const response = await searchFTSFromLbug('login', 5, REPO); // The expected degraded-capability state — not per-table query errors. expect(response.ftsAvailable).toBe(false); expect(response.nonBenignErrors).toBeUndefined(); // No redundant round-trips to a pool that can't have FTS loaded. expect(mockExecuteParameterized).not.toHaveBeenCalled(); }); it('CLI/pipeline path (no repoId): also skips per-table calls and reports no nonBenignErrors — same expected state, same silence (fixes the pool-only guard a /simplify altitude pass caught)', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); await extensionManager.ensure( vi.fn().mockRejectedValue(new Error('invalid ELF header.')), 'fts', 'FTS', { policy: 'load-only' }, ); vi.mocked(queryFTS).mockClear(); const response = await searchFTSFromLbug('login', 5); // no repoId → CLI/pipeline branch expect(response.ftsAvailable).toBe(false); expect(response.nonBenignErrors).toBeUndefined(); expect(vi.mocked(queryFTS)).not.toHaveBeenCalled(); }); }); describe('GITNEXUS_FTS_CJK_SEGMENTATION query-side transform (#2331)', () => { const CJK_REPO = 'test-repo-cjk-query'; beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.unstubAllEnvs(); }); it('leaves the query unchanged by default (mode: none)', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); await searchFTSFromLbug('审批流程'); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { expect(call[2]).toBe('审批流程'); } }); it('bigram-segments the query before it reaches queryFTS when enabled', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); await searchFTSFromLbug('审批流程'); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { expect(call[2]).toBe('审批 批流 流程'); } }); it('bigram-segments the query in pool mode too, still bound via $query', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); mockExecuteParameterized.mockResolvedValue([]); await searchFTSFromLbug('审批流程', 5, CJK_REPO); expect(mockExecuteParameterized).toHaveBeenCalled(); for (const call of mockExecuteParameterized.mock.calls) { expect(String(call[1])).toContain('$query'); expect(String(call[1])).not.toContain('审批流程'); expect(call[2]).toEqual({ query: '审批 批流 流程' }); } }); it('skips segmentation for a pathologically long query, searching it unchanged', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); const longQuery = '审批流程'.repeat(1000); // well past the 2000-char cap await searchFTSFromLbug(longQuery); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { expect(call[2]).toBe(longQuery); } }); it('segments a query at exactly the 2000-character cap', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); const atCapQuery = '审'.repeat(2000); await searchFTSFromLbug(atCapQuery); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { expect(call[2]).not.toBe(atCapQuery); // segmented, not passed through raw expect(call[2]).toContain(' '); } }); it('does not segment a query at exactly 2001 characters, one past the cap', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); const overCapQuery = '审'.repeat(2001); await searchFTSFromLbug(overCapQuery); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { expect(call[2]).toBe(overCapQuery); // passed through raw, unsegmented } }); }); // #2339: the query path previously never called normalizeFtsText (only // applyCjkSegmentationIfEnabled), unlike the write path which always // composes both — a literal tab/newline in a query wouldn't match // whitespace-normalized indexed text. describe('normalizeFtsText query-side composition (#2339)', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.unstubAllEnvs(); }); it('collapses a literal tab in the query to a space (mode: none)', async () => { const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); await searchFTSFromLbug('审批\t流程'); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { expect(call[2]).toBe('审批 流程'); } }); it('composes segmentation THEN normalization, matching the write path order (mode: bigram)', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); await searchFTSFromLbug('审批流程\t自动'); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { // "审批流程" bigram-segments to "审批 批流 流程"; the tab (untouched // by segmentCjkSpans, since neither run's boundary needs an extra // space next to an already-whitespace neighbor) is then collapsed // to a space by normalizeFtsText, keeping "自动" a separate token. expect(call[2]).toBe('审批 批流 流程 自动'); } }); it('applies normalization regardless of the 2000-char segmentation cap', async () => { vi.stubEnv('GITNEXUS_FTS_CJK_SEGMENTATION', 'bigram'); const { queryFTS } = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(queryFTS).mockResolvedValue([]); const longQueryWithTab = '审'.repeat(2001) + '\t' + '批'; await searchFTSFromLbug(longQueryWithTab); expect(vi.mocked(queryFTS).mock.calls.length).toBeGreaterThan(0); for (const call of vi.mocked(queryFTS).mock.calls) { // Segmentation is skipped (over the cap), but normalizeFtsText still // runs unconditionally — no per-character cost concern there. expect(call[2]).toBe('审'.repeat(2001) + ' ' + '批'); } }); }); });