GitNexus/gitnexus/test/unit/group/thrift-extractor.test.ts
Hector Prats d69eadfb7f
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
fix(windows): 32767-char tree-sitter crash + VECTOR extension SIGSEGV (#1433)
* fix(windows): 32767-char tree-sitter crash + VECTOR extension SIGSEGV

tree-sitter 0.21.x on Windows crashes with SIGSEGV when parsing source
strings longer than 32 767 chars (signed 16-bit integer overflow in the
native binding). Five call sites passed raw file content without any
length guard:

  - captures.ts (C# scope extraction)
  - namespace-siblings.ts (extractFileStructure)
  - parse-worker.ts (worker thread parse path)
  - parsing-processor.ts (sequential parse fallback)

Fix: truncate at the last newline before the limit so the fragment stays
syntactically coherent. Files truncated mid-class produce ERROR roots;
captures.ts returns [] for any ERROR-root tree so the legacy DAG handles
the file silently without orphaned scope errors.

Additional C# scope fixes:
  - scope-tree.ts: Module scopes may share the same range as a top-level
    namespace_declaration (files with no leading `using` directives). The
    rangeStrictlyContains check rejects equal ranges. Added
    rangeNonStrictlyContains for Module parents.
  - scope-extractor.ts: pass1BuildScopes stack-pop used strict containment;
    same Module == Namespace range case caused orphaned scopes. Added
    moduleAwareContains helper.
  - scope-extractor-bridge.ts: empty captures from ERROR-root files still
    called extractScope -> "no Module scope found" warning. Added early
    return for empty/non-array captures.
  - namespace-siblings.ts: three sites pushed onto binding arrays frozen by
    finalize-algorithm. Fixed with spread-copy before mutation.

lbug-adapter.ts: INSTALL VECTOR in loadVectorExtension calls the KuzuDB
native extension installer, which crashes with SIGSEGV on Windows via an
unhandled error path in native code. JS try/catch cannot intercept native
signals. Skip extension loading on win32 — vector/embedding search is
unavailable on Windows but all graph index queries work correctly.

Verified on: Windows 11, Node.js 24, gitnexus 1.6.3, pcf8-game codebase
(61 757 nodes / 111 796 edges / 300 flows after fix).

* fix(windows): skip FTS extension load in pool-adapter on Windows to prevent SIGSEGV

LOAD EXTENSION fts crashes the process with SIGSEGV on Windows when the
FTS extension binary is not installed locally. This is an @ladybugdb/core
native bug — the extension loader hits an unhandled error path that raises
a native signal instead of a JS exception, so try/catch cannot protect here.

Add a process.platform === 'win32' guard in both doInitLbug and
initLbugWithDb. When skipped, bm25-index.js catches the resulting
Kuzu catalog errors (CREATE_FTS_INDEX not defined) and returns empty
BM25 results gracefully. All graph queries (cypher, context, impact)
are unaffected.

This is patch 9 of the Windows fix series for gitnexus on Windows:
patch 8 (same PR) already fixed INSTALL VECTOR SIGSEGV in lbug-adapter.ts.
pool-adapter.ts is the separate MCP-server code path that was not covered.

* fix: address codeql findings on PR #1433

The four `lastIndexOf('\n', ...)` calls were committed with a literal
newline inside the single-quoted string instead of the `\n` escape, so
the files do not parse — `tsc` and CodeQL both flagged them. Replace
the embedded newline with `'\n'`.

Also remove the two helpers that were superseded during review and
became dead code: `rangeNonStrictlyContains` in scope-tree.ts (the
equal-range carve-out is handled by `rangeStrictlyContains` +
`rangesEqual` in `canParentScope`) and `moduleAwareContains` in
scope-extractor.ts (`pass1BuildScopes` calls `canParentScope` directly).

* fix(windows): replace 32767-char truncation with chunked-input parsing

The tree-sitter 0.21.x Node binding crashes (SIGSEGV) on Windows when
parser.parse(string, ...) is handed a JS string longer than 32 767 chars.
The crash is in the bindings V8 string-to-buffer conversion and cannot
be intercepted from JS. Previous mitigation truncated source at the last
newline before that boundary, silently losing the file tail and producing
ERROR-root trees from mid-class cuts.

Switch to the callback (Parser.Input) overload via a new parseSourceSafe
helper. tree-sitter pulls source in 16 KiB chunks via repeated callback
invocations, bypassing the broken conversion path. Files are parsed in
full, no data loss, no platform-specific code path.

Removes the now-unnecessary ERROR-root short-circuit in csharp/captures.ts
and the empty-captures shim in scope-extractor-bridge.ts; both existed only
to swallow truncation-induced parse failures.

* fix(windows): cover all parse sites and correct vector-extension state

Address adversarial review on PR #1433:

1. Extend parseSourceSafe to all remaining parser.parse() call sites that
   handle full file content. The first commit only converted the four
   sites with active truncation hacks; cache-miss paths in
   call-processor (x2), heritage-processor (x2), import-processor, and
   the Go/Python/TypeScript captures + Go range-binding still called
   parser.parse() directly. On Windows those would still SIGSEGV for
   files > 32767 chars.

2. Stop setting vectorExtensionLoaded = true on the win32 short-circuit
   in lbug-adapter.ts. The flag means "successfully loaded" and is
   checked by an early-return at the top of loadVectorExtension; setting
   it on the skip path made the second call return true and let
   QUERY_VECTOR_INDEX run against a DB without the extension.

3. Drop the placeholder issues/... URL in the same comment.

4. Add unit tests for parseSourceSafe at boundary values: 16 KiB
   (direct/callback boundary), the 32 767 Windows crash boundary,
   single-line > chunk size, CRLF near boundary, and large all-Chinese
   source. Confirms the callback path is correct for non-ASCII content,
   which is also exercised by the existing csharp-captures large-file
   test.

Researched the chunking concern: tree-sitter Node binding sets
TSInputEncodingUTF16 and divides byte_index by 2 in ByteCountToJS before
calling the JS callback, so the index argument is a UTF-16 code-unit
offset — matching String.prototype.slice. Splitting tokens across chunks
is safe by API contract; the lexer is chunk-agnostic.

* fix(windows): extend parseSourceSafe to group/embeddings + lint enforcement

Closes the remaining Windows SIGSEGV exposure flagged by the Codex
adversarial review on PR #1433. Six pre-existing parser.parse(content)
call sites bypassed parseSourceSafe and could crash the process on
Windows when a contract IDL, route file, or embedding-target source
exceeded 32 767 chars. Adds a lint rule so the regression vector closes
permanently.

Production code:
- Relocate parseSourceSafe from ingestion/utils/ to core/tree-sitter/
  so group/ and embeddings/ can import without crossing into ingestion
  internals. core/tree-sitter/ already houses parser-loader.ts and is
  the natural shared facade. All 11 existing importers updated; no shim
  left behind in the old location.
- Route through parseSourceSafe in 5 group extractors (grpc, thrift,
  http-route, include, tree-sitter-scanner) and the embeddings
  ensureAndParse helper.
- The seventh direct .parse() call in grpc-patterns/proto.ts:49 is a
  module-load grammar smoke test parsing a 36-char literal. Trivially
  safe by inspection, intentionally direct, filtered out by the lint
  rule via the string-literal-arg skip.

Tests:
- 5 caller-side regression tests with a vi.spyOn assertion on
  parseSourceSafe. The spy is what catches a regression: parser.parse
  on a 40 000-char input succeeds on Linux/macOS, so a "no throw"
  assertion alone would silently pass with the bypass reintroduced.
- The vi.mock boilerplate is centralised in
  gitnexus/test/helpers/parse-source-safe-mock.ts, dynamic-imported
  inside each mock factory so vitest's hoister does not race the
  static import binding.

Lint:
- New custom ESLint rule gitnexus/require-safe-parse, scoped to
  gitnexus/src/core/**, fails on direct <parser>.parse(<non-literal>,
  ...) calls and auto-fixes them to parseSourceSafe(<parser>, ...).
  Skips JSON/URL/marked/Number/Math, string-literal first args
  (smoke tests), test files, and the helper itself. Auto-fix rewrites
  the call site only; the developer adds the import after tsc
  surfaces the missing identifier — same tradeoff as
  unused-imports/no-unused-imports.

Plan: docs/plans/2026-05-10-001-fix-windows-parse-safety-group-and-embeddings-plan.md

* fix(test): use mkdtempSync in http-route-extractor regression test

Address CodeQL js/insecure-temporary-file warning on the new Windows-
SIGSEGV regression test. The test was using path.join(tmpDir, "large-input")
which, when nested inside a Date.now()-based parent tmpDir, lets CodeQL flag
the directory as a predictable-name temp file with race-condition risk.
Switch to fs.mkdtempSync(path.join(tmpDir, "large-input-")) so the suffix
is a secure unique random string.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-10 16:00:36 +01:00

692 lines
21 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import fsp from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
const { parseSourceSafeSpy } = vi.hoisted(() => ({ parseSourceSafeSpy: vi.fn() }));
vi.mock('../../../src/core/tree-sitter/safe-parse.js', async () => {
const { buildSafeParseMock } = await import('../../helpers/parse-source-safe-mock.js');
return buildSafeParseMock(parseSourceSafeSpy);
});
import {
ThriftExtractor,
buildThriftContext,
thriftMethodContractId,
thriftServiceContractId,
} from '../../../src/core/group/extractors/thrift-extractor.js';
import type { RepoHandle } from '../../../src/core/group/types.js';
describe('ThriftExtractor', () => {
let tmpDir: string;
let extractor: ThriftExtractor;
beforeEach(async () => {
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'gitnexus-thrift-'));
extractor = new ThriftExtractor();
});
afterEach(async () => {
await fsp.rm(tmpDir, { recursive: true, force: true });
});
function writeFile(relPath: string, content: string): void {
const full = path.join(tmpDir, relPath);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content);
}
const makeRepo = (repoPath: string): RepoHandle => ({
id: 'test-repo',
path: 'test/app',
repoPath,
storagePath: path.join(repoPath, '.gitnexus'),
});
it('test_extract_thrift_single_method_returns_idl_provider', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts).toHaveLength(1);
expect(contracts[0]).toMatchObject({
contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
type: 'thrift',
role: 'provider',
symbolName: 'OrderService.PlaceOrder',
confidence: 0.85,
meta: {
namespace: 'billing.v1',
service: 'OrderService',
method: 'PlaceOrder',
source: 'thrift_idl',
},
});
expect(contracts[0].symbolRef).toEqual({
filePath: 'idl/order.thrift',
name: 'OrderService.PlaceOrder',
});
});
it('test_extract_thrift_multiple_services_and_methods_returns_all', async () => {
writeFile(
'contracts/orders.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
OrderStatus GetOrderStatus(1: string orderId)
}
service InvoiceService {
Invoice CreateInvoice(1: string orderId)
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts.map((c) => c.contractId).sort()).toEqual([
'thrift::billing.v1.InvoiceService/CreateInvoice',
'thrift::billing.v1.OrderService/GetOrderStatus',
'thrift::billing.v1.OrderService/PlaceOrder',
]);
});
it('test_extract_thrift_prefers_java_namespace_over_other_namespaces', async () => {
writeFile(
'order.thrift',
`namespace py billing_python.v1
namespace java billing.v1
namespace go billinggo
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts[0].contractId).toBe('thrift::billing.v1.OrderService/PlaceOrder');
expect(contracts[0].meta.namespace).toBe('billing.v1');
});
it('test_extract_thrift_uses_first_non_java_namespace_when_java_missing', async () => {
writeFile(
'order.thrift',
`namespace py billing_python.v1
namespace go billinggo
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts[0].contractId).toBe('thrift::billing_python.v1.OrderService/PlaceOrder');
expect(contracts[0].meta.namespace).toBe('billing_python.v1');
});
it('test_extract_thrift_without_namespace_uses_service_only', async () => {
writeFile(
'order.thrift',
`service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts[0].contractId).toBe('thrift::OrderService/PlaceOrder');
expect(contracts[0].meta.namespace).toBe('');
});
it('test_extract_thrift_ignores_braces_inside_comments_and_strings', async () => {
writeFile(
'idl/tricky.thrift',
`namespace java billing.v1
service OrderService {
// A comment with } should not close the service.
/* A block comment with { and } should not affect depth. */
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
const string NOTE = "literal with } and { braces"
OrderStatus GetOrderStatus(1: string orderId)
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts.map((c) => c.symbolName).sort()).toEqual([
'OrderService.GetOrderStatus',
'OrderService.PlaceOrder',
]);
});
it('test_extract_thrift_malformed_unclosed_service_is_skipped', async () => {
writeFile(
'idl/broken.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
`,
);
await expect(extractor.extract(null, tmpDir, makeRepo(tmpDir))).resolves.toEqual([]);
});
it('test_extract_repo_without_thrift_returns_empty', async () => {
writeFile('src/index.ts', 'console.log("hello")');
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts).toEqual([]);
});
it('test_extract_java_thrift_consumers_from_iface_client_and_service_fields', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/BillingWorkflow.java',
`package example;
class BillingWorkflow {
private OrderService.Iface orderService;
private OrderService.Client orderClient;
private OrderService generatedOrderService;
void submit(PlaceOrderRequest request) throws Exception {
orderService.PlaceOrder(request);
orderClient.PlaceOrder(request);
generatedOrderService.PlaceOrder(request);
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const consumers = contracts
.filter((c) => c.role === 'consumer')
.sort((a, b) => a.symbolName.localeCompare(b.symbolName));
expect(consumers).toHaveLength(3);
expect(consumers.map((c) => c.symbolName)).toEqual([
'generatedOrderService.PlaceOrder',
'orderClient.PlaceOrder',
'orderService.PlaceOrder',
]);
for (const contract of consumers) {
expect(contract).toMatchObject({
contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
type: 'thrift',
role: 'consumer',
confidence: 0.75,
meta: {
namespace: 'billing.v1',
service: 'OrderService',
method: 'PlaceOrder',
source: 'java_thrift_consumer',
},
});
expect(contract.symbolRef.filePath).toBe('src/main/java/example/BillingWorkflow.java');
}
});
it('test_extract_java_thrift_consumers_from_this_field_access', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/BillingWorkflow.java',
`package example;
class BillingWorkflow {
private OrderService.Client orderClient;
void submit(PlaceOrderRequest request) throws Exception {
this.orderClient.PlaceOrder(request);
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const consumers = contracts.filter((c) => c.role === 'consumer');
expect(consumers).toHaveLength(1);
expect(consumers[0]).toMatchObject({
contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
type: 'thrift',
role: 'consumer',
symbolName: 'orderClient.PlaceOrder',
confidence: 0.75,
meta: {
namespace: 'billing.v1',
service: 'OrderService',
method: 'PlaceOrder',
source: 'java_thrift_consumer',
},
});
});
it('test_extract_java_thrift_consumers_from_fully_qualified_generated_types', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/BillingWorkflow.java',
`package example;
class BillingWorkflow {
private billing.v1.OrderService.Iface orderService;
private billing.v1.OrderService.Client orderClient;
void submit(PlaceOrderRequest request) throws Exception {
orderService.PlaceOrder(request);
orderClient.PlaceOrder(request);
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const consumers = contracts
.filter((c) => c.role === 'consumer')
.sort((a, b) => a.symbolName.localeCompare(b.symbolName));
expect(consumers).toHaveLength(2);
expect(consumers.map((c) => c.symbolName)).toEqual([
'orderClient.PlaceOrder',
'orderService.PlaceOrder',
]);
expect(new Set(consumers.map((c) => c.contractId))).toEqual(
new Set(['thrift::billing.v1.OrderService/PlaceOrder']),
);
});
it('test_extract_java_thrift_consumers_from_local_variables', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/BillingWorker.java',
`package example;
class BillingWorker {
void submit(OrderService.Iface iface, OrderService.Client client, OrderService service) throws Exception {
OrderService.Iface orderService = iface;
OrderService.Client orderClient = client;
OrderService generatedOrderService = service;
orderService.PlaceOrder(new PlaceOrderRequest());
orderClient.PlaceOrder(new PlaceOrderRequest());
generatedOrderService.PlaceOrder(new PlaceOrderRequest());
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const consumers = contracts.filter((c) => c.role === 'consumer');
expect(consumers.map((c) => c.symbolName).sort()).toEqual([
'generatedOrderService.PlaceOrder',
'orderClient.PlaceOrder',
'orderService.PlaceOrder',
]);
expect(new Set(consumers.map((c) => c.contractId))).toEqual(
new Set(['thrift::billing.v1.OrderService/PlaceOrder']),
);
});
it('test_extract_java_thrift_consumers_resolve_receiver_by_nearest_scope', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}
service InvoiceService {
Invoice CreateInvoice(1: string orderId)
}`,
);
writeFile(
'src/main/java/example/BillingWorker.java',
`package example;
class BillingWorker {
void submitOrder(OrderService.Iface client, PlaceOrderRequest request) throws Exception {
client.PlaceOrder(request);
}
void submitInvoice() throws Exception {
InvoiceService.Client client = new InvoiceService.Client(null);
client.CreateInvoice("order-1");
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const consumers = contracts
.filter((c) => c.role === 'consumer')
.sort((a, b) => a.contractId.localeCompare(b.contractId));
expect(consumers.map((c) => c.contractId)).toEqual([
'thrift::billing.v1.InvoiceService/CreateInvoice',
'thrift::billing.v1.OrderService/PlaceOrder',
]);
expect(consumers.map((c) => c.symbolName).sort()).toEqual([
'client.CreateInvoice',
'client.PlaceOrder',
]);
});
it('test_extract_java_thrift_providers_from_iface_and_service_implements', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/IfaceOrderHandler.java',
`package example;
class IfaceOrderHandler implements OrderService.Iface {
public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
return new PlaceOrderResponse();
}
}`,
);
writeFile(
'src/main/java/example/GeneratedOrderHandler.java',
`package example;
class GeneratedOrderHandler implements OrderService {
public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
return new PlaceOrderResponse();
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const providers = contracts
.filter((c) => c.meta.source === 'java_thrift_provider')
.sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath));
expect(providers).toHaveLength(2);
for (const contract of providers) {
expect(contract).toMatchObject({
contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
type: 'thrift',
role: 'provider',
symbolName: 'OrderService.PlaceOrder',
confidence: 0.8,
meta: {
namespace: 'billing.v1',
service: 'OrderService',
method: 'PlaceOrder',
source: 'java_thrift_provider',
},
});
}
});
it('test_extract_thrift_source_scan_contracts_have_stable_distinct_symbol_uids', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/IfaceOrderHandler.java',
`package example;
class IfaceOrderHandler implements OrderService.Iface {
public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
return new PlaceOrderResponse();
}
}`,
);
const first = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const second = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const providers = first
.filter((c) => c.role === 'provider')
.sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath));
const repeatedProviders = second
.filter((c) => c.role === 'provider')
.sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath));
expect(providers).toHaveLength(2);
expect(providers.map((c) => c.symbolUid)).toEqual(repeatedProviders.map((c) => c.symbolUid));
expect(providers.every((c) => c.symbolUid.length > 0)).toBe(true);
expect(new Set(providers.map((c) => c.symbolUid)).size).toBe(2);
expect(providers.every((c) => !c.symbolUid.includes('::thrift::billing.v1'))).toBe(true);
});
it('test_extract_java_thrift_providers_from_fully_qualified_generated_iface', async () => {
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
writeFile(
'src/main/java/example/IfaceOrderHandler.java',
`package example;
class IfaceOrderHandler implements billing.v1.OrderService.Iface {
public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
return new PlaceOrderResponse();
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
const providers = contracts.filter((c) => c.meta.source === 'java_thrift_provider');
expect(providers).toHaveLength(1);
expect(providers[0]).toMatchObject({
contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
type: 'thrift',
role: 'provider',
symbolName: 'OrderService.PlaceOrder',
confidence: 0.8,
meta: {
namespace: 'billing.v1',
service: 'OrderService',
method: 'PlaceOrder',
source: 'java_thrift_provider',
},
});
});
it('test_extract_java_thrift_consumer_without_idl_emits_weak_method_contract', async () => {
writeFile(
'src/main/java/example/BillingWorkflow.java',
`package example;
class BillingWorkflow {
private OrderService.Iface orderService;
void submit(PlaceOrderRequest request) throws Exception {
orderService.PlaceOrder(request);
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts).toHaveLength(1);
expect(contracts[0]).toMatchObject({
contractId: 'thrift::OrderService/PlaceOrder',
type: 'thrift',
role: 'consumer',
symbolName: 'orderService.PlaceOrder',
confidence: 0.45,
meta: {
service: 'OrderService',
method: 'PlaceOrder',
source: 'java_thrift_consumer_weak',
},
});
expect(contracts[0].symbolRef.filePath).toBe('src/main/java/example/BillingWorkflow.java');
});
it('test_extract_java_thrift_direct_service_consumer_without_idl_returns_empty', async () => {
writeFile(
'src/main/java/example/PaymentWorkflow.java',
`package example;
class PaymentWorkflow {
private PaymentService paymentService;
void submit() {
paymentService.charge();
}
}`,
);
const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(contracts).toEqual([]);
});
describe('Windows SIGSEGV regression — large input must route through parseSourceSafe', () => {
it('routes >32 767-char source file through parseSourceSafe (not direct parser.parse)', async () => {
parseSourceSafeSpy.mockClear();
// Need a base .thrift file so buildThriftContext finds at least one
// service to scan; without it the source-scan loop short-circuits.
writeFile(
'idl/order.thrift',
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
);
// >40 000-char Java client file. Direct parser.parse(content) on a
// string this size SIGSEGVs the process on Windows. The spy assertion
// catches the regression — a "no throw" assertion alone is satisfied
// by the bypass on Linux/macOS where parser.parse(40 000 chars) succeeds.
const padding = Array.from(
{ length: 600 },
(_, i) => ` public String helper${i}() { return "padding-${i}-aaaaaaaaaaaaaaaaaaa"; }\n`,
).join('');
const largeJava = `package com.example;\n\nimport billing.v1.OrderService;\n\npublic class BigClient {\n private OrderService.Iface client;\n${padding}}\n`;
expect(largeJava.length).toBeGreaterThan(40_000);
writeFile('src/main/java/com/example/BigClient.java', largeJava);
await extractor.extract(null, tmpDir, makeRepo(tmpDir));
expect(parseSourceSafeSpy).toHaveBeenCalled();
});
});
});
describe('buildThriftContext', () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'gitnexus-thrift-context-'));
});
afterEach(async () => {
await fsp.rm(tmpDir, { recursive: true, force: true });
});
it('test_buildThriftContext_parses_namespace_service_methods_and_path', async () => {
await fsp.mkdir(path.join(tmpDir, 'idl'), { recursive: true });
await fsp.writeFile(
path.join(tmpDir, 'idl', 'order.thrift'),
`namespace java billing.v1
service OrderService {
PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
OrderStatus GetOrderStatus(1: string orderId)
}`,
);
const context = await buildThriftContext(tmpDir);
expect(context.namespacesByThrift.get('idl/order.thrift')).toBe('billing.v1');
expect(context.servicesByName.get('OrderService')).toEqual([
{
namespace: 'billing.v1',
serviceName: 'OrderService',
methods: ['PlaceOrder', 'GetOrderStatus'],
thriftPath: 'idl/order.thrift',
},
]);
});
it('test_buildThriftContext_without_files_returns_empty_maps', async () => {
const context = await buildThriftContext(tmpDir);
expect(context.namespacesByThrift.size).toBe(0);
expect(context.servicesByName.size).toBe(0);
});
});
describe('Thrift contract id helpers', () => {
it('test_thriftMethodContractId_with_namespace', () => {
expect(thriftMethodContractId('billing.v1', 'OrderService', 'PlaceOrder')).toBe(
'thrift::billing.v1.OrderService/PlaceOrder',
);
});
it('test_thriftMethodContractId_without_namespace', () => {
expect(thriftMethodContractId('', 'OrderService', 'PlaceOrder')).toBe(
'thrift::OrderService/PlaceOrder',
);
});
it('test_thriftServiceContractId_with_namespace', () => {
expect(thriftServiceContractId('billing.v1', 'OrderService')).toBe(
'thrift::billing.v1.OrderService/*',
);
});
it('test_thriftServiceContractId_without_namespace', () => {
expect(thriftServiceContractId('', 'OrderService')).toBe('thrift::OrderService/*');
});
});