GitNexus/gitnexus/test/unit/framework-detection.test.ts
Adam B. 7376347058
feat(ingestion): add tRPC procedure detection and MCP chain/route surfacing (#3339)
* feat(ingestion): tRPC pattern detection — procedures, curried calls, route extraction

Three changes to properly index tRPC router files:

1. HOC-in-pair patterns: detect procedures like
   'create: procedure.mutation(async ({ input }) => {...})'
   as named Function nodes (pair > call_expression > arguments > arrow_function)

2. Curried call detection: capture 'workflow(db)(input)' chained calls
   where call_expression.function is itself a call_expression

3. tRPC route extraction: new route-extractors/trpc.ts detects
   .query()/.mutation()/.subscription() procedures, maps to /trpc/* routes
   with prefix inference from router variable names

4. Function/Const dedup: structural check skips Const nodes when
   variable_declarator value is arrow_function/function_expression

5. Process-route linking: match by (filePath, methodName) instead of
   filePath only, preventing shared flows across procedures in same router

(cherry picked from commit eced15e60a39a181a48bff8c5bbe278d26aa53b8)

* fix(route-extractors): line-by-line scanner for chained tRPC procedures

Original regex only matched direct patterns (list: proc.query()) but not
chained patterns (create: proc.input(z.object({...})).mutation()). Only
41/256 routes were detected on Jurialis.

Rewrote extractTrpcRoutes() as a line-by-line scanner that:
- Detects procedure keys starting with *Procedure builders
- Tracks currentProcedure forward until terminal .query()/.mutation()
- Handles .input() chaining naturally
- Deduplicates via seen Set on procedurePath

Result: 245 routes from 30 routers (was 41), 256 total after re-index.
(cherry picked from commit 49edf953e93e6b67b77932e866fcbfb8d089b1f3)

* feat(mcp): expose tRPC chains via context/query tools

Eight fixes to make the tRPC route->procedure->workflow->sub-workflow chain
visible through the standard MCP tools (context, query) that AI agents use,
instead of requiring raw Cypher queries.

- A context: order incoming/outgoing CALLS test-last, raise LIMIT 30->100
- B query: batched ENTRY_POINT_OF lookup, surface route URL on processes
- C query: mark process_symbols entry point with is_entry_point=true
- D entry-point-scoring: skip UTILITY_PATTERNS (get*/set*) penalty for
  symbols in tRPC router files so framework boost (3.0x) is preserved
- E fts-schema: index Route nodes so /trpc/* URLs are keyword-searchable
- F tools: bump max_symbols default 10->25 to fit procedure->workflow chain
- G context: optional chain_depth (0-3) param walks CALLS edges in both
  directions and returns layered chain field
- H LadybugDB bug workaround: WHERE r.type IN [...] silently drops edges
  on relationship properties; replace with OR chains (7 occurrences in
  local-backend.ts, pdg-impact.ts, graph-queries.ts)

Validated on Jurialis: setProviderCap procedure now appears as caller of
setProviderCapWorkflow, /trpc/cabinet.setProviderCap Route is searchable,
chain_depth=3 returns the layered call graph.

(cherry picked from commit c24df0adce91e80f191a5c5d2e8b14e9da677366)

* feat(mcp): surface is_entry_point + routes in context, raise query limit

context() is the mandatory pre-edit tool (AGENTS.md). Until now an agent
had to issue a separate query() call just to learn whether its symbol is
a process entry point or which HTTP route it handles. These two fields
make context() self-sufficient for the bmad-dev flow.

- context: query STEP_IN_PROCESS now returns p.entryPointId; new
  ENTRY_POINT_OF lookup attributes routes only from processes where the
  symbol is the entry point (middle steps do not own the route) plus any
  direct Route->symbol handler edge (tRPC procedures outside any Process)
- context: new top-level is_entry_point (true only) and routes[] fields
  ({url, method?}), emitted only when non-empty to keep the diff additive
- query: raise default limit 5->10 so dense domains (tRPC action router
  with 20 chains, data-export with 9 flows) surface more of their flow
  set without an explicit param

(cherry picked from commit 004f5b0ba985fcef3dfac5e67a2f2f7262184215)

* docs(mcp): document chain_depth, routes, and entry-point flag; neutralize examples in comments

* fix(mcp): address code-review findings on tRPC entry points, queries, and dispatch

- entry-point scoring: optional-chain framework detection and normalize
  path separators before router-pattern matching (P1 crash on .js routers)
- tRPC extractor: emit controllerName as null (callers resolve via
  lookupClassByName; a router object stringified as name corrupted lookups)
- route dedup: key seenRoutes by method:url so GET/POST pairs survive
- legacy TS queries: drop curried-call patterns (arity-corrupting for
  overload resolution) and require non-array callee on pair member calls
- registry-primary TS query: mirror the non-array-callee predicate on the
  new pair member-expression patterns (fixes query compile error)
- group tool port: forward chain_depth into per-tool context args

* fix(mcp): nested tRPC router paths, controller-less route binding, query chain_depth

- trpc extractor: brace-depth nesting stack composes sibling router paths (bare router() import style included); merge-prefix dot normalization; strict publicProcedure allowlist gate; drop phantom router metadata

- call-processor: bind controller-less tRPC routes to same-file handlers via exact single-match symbol lookup; ambiguous or missing handlers skipped

- query/group query: optional chain_depth (0-3) enriches ranked processes with their context chain; fix _computeContextChain layer docstring

- tree-sitter TS/JS scope queries capture string-key function declarations; tRPC pattern scoring narrowed to server router files

- parse-cache schema bump to v103 for extractor and route-binding changes

- add trpc route extractor regression tests (9 cases incl. sibling nesting and merge prefix)

* Address PR review feedback (#3339)

Close remaining review threads: compact tRPC keys, comment-safe terminals,
quoted-key identifier HOC captures, group-query default alignment, and
LadybugDB label scalars on context chains.

* chore(autofix): apply prettier + eslint fixes via /autofix command

* test: pin PARSE_CACHE schema bump to 103 (PR #3339 review fixes)

* fix(ingestion): bind same-name tRPC handlers and surface HANDLES_ROUTE

Same-name procedures resolve by startLine, the extractor keeps nested paths
through multiline schemas, and context/query UNION HANDLES_ROUTE for leaf
procedures that never become Process entries.

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

* Address PR review feedback (#3339)

Clamp group query bounds, drop HOC pair false positives, emit tRPC
terminal lines and hyphenated quoted keys, cap chain BFS concurrency,
and restrict the router utility exemption to accessor names.

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

* Address PR review feedback (#3339)

Score JS/JSX tRPC routers like TypeScript, ignore inner db.query and
unrelated .merge calls, mask regex braces, and assert clamp tests invoke
the query mock.

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

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(ingestion,mcp): emit-side HOC callback guards + MCP schema prettier (PR #3339 round 2)

* Address PR review feedback (#3339)

Treat `/` after return-style keywords as a regex, drop the synthetic
appRouter path prefix, and bind the create mutation via an inline callback.

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

* Address PR review feedback (#3339)

Qualify query() docs so is_entry_point is promised only when the entry
symbol is among the search hits.

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

* Address PR review feedback (#3339)

Emit tRPC terminals only at procedure paren depth, drop the filename
prefix on bare appRouter = router(), mask regex after if (), and cap
groupQuery member fan-out.

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

* Address PR review feedback (#3339)

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

* Address PR review feedback (#3339)

De-duplicate context-chain BFS nodes reached through multiple frontier
edges, and bind tRPC identifier callbacks (`.mutation(handler)`) to the
handler symbol instead of the procedure key.

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

* Address PR review feedback (#3339)

Apply LIMIT 50 after DISTINCT neighbors in the context-chain BFS, and
treat the official lowercase `procedure` builder as a tRPC procedure key.

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

* Address PR review feedback (#3339)

Keep the later duplicate tRPC object-literal key so route binding matches
the handler JavaScript actually ships.

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

* Address PR review feedback (#3339)

Compose same-file identifier-mounted tRPC subrouters so admin: adminRouter
emits the live admin.list path instead of an unprefixed /trpc/list.

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

* Address PR review feedback (#3339)

Memoize tRPC identifier-mount paths so a depth-N chain stays linear, and
gate it with the build-free measure.mjs / baselines.json harness.

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

* Address PR review feedback (#3339)

Compose identifier mounts without phantom /trpc URLs, bind wrapped and
multiline handlers, fail-close missing bench budgets, and reject query
page bounds before search.

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

* Address PR review feedback (#3339)

Reject advertised MCP query bounds in group mode and chain_depth
instead of clamping or accepting non-integers.

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

* Address PR review feedback (#3339)

Reject invalid groupContext chain_depth and ignore non-callable
same-file tRPC handler candidates.

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

* Address PR review feedback (#3339)

Memoize live tRPC mount paths so the depth-N chain bench stays linear,
and render invalid group/MCP bounds without throwing.

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

* Address PR review feedback (#3339)

Keep t.merge('prefix.', namedRouter) procedures live by recording a
zero-hop mount so the unmounted-router drop does not hide them.

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

* Address PR review feedback (#3339)

Recognize type-annotated `const adminRouter: AppRouter = t.router(`
bindings so identifier mounts still compose.

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 09:44:42 +01:00

487 lines
17 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { SupportedLanguages } from 'gitnexus-shared';
import {
detectFrameworkFromPath,
detectFrameworkFromAST,
} from '../../src/core/ingestion/framework-detection.js';
import { providers } from '../../src/core/ingestion/languages/index.js';
describe('detectFrameworkFromPath', () => {
describe('Next.js', () => {
it('detects Pages Router pages', () => {
const result = detectFrameworkFromPath('pages/users.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-pages');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('ignores _app and _document pages', () => {
expect(detectFrameworkFromPath('pages/_app.tsx')).toBeNull();
});
it('detects App Router page.tsx', () => {
const result = detectFrameworkFromPath('app/dashboard/page.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-app');
});
it('detects API routes in pages', () => {
const result = detectFrameworkFromPath('pages/api/users.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-api');
});
it('detects App Router API route.ts', () => {
const result = detectFrameworkFromPath('app/api/users/route.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-api');
});
it('detects layout files', () => {
const result = detectFrameworkFromPath('app/layout.tsx');
expect(result).not.toBeNull();
expect(result!.entryPointMultiplier).toBe(2.0);
});
});
describe('Expo Router', () => {
it('detects _layout files', () => {
const result = detectFrameworkFromPath('app/_layout.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
expect(result!.reason).toBe('expo-layout');
});
it('detects +api routes', () => {
const result = detectFrameworkFromPath('app/users+api.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
expect(result!.reason).toBe('expo-api-route');
});
it('detects screen files', () => {
const result = detectFrameworkFromPath('app/(tabs)/settings.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
expect(result!.reason).toBe('expo-screen');
expect(result!.entryPointMultiplier).toBe(2.5);
});
});
describe('tRPC', () => {
it('detects /trpc/routers/ files at 3.0x', () => {
const result = detectFrameworkFromPath('src/server/trpc/routers/user.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('trpc');
expect(result!.entryPointMultiplier).toBe(3.0);
expect(result!.reason).toBe('trpc-router');
});
it('detects T3 /api/routers/ files at 3.0x', () => {
const result = detectFrameworkFromPath('src/server/api/routers/settings.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('trpc');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects T3 /api/routers/ JavaScript files at 3.0x', () => {
const result = detectFrameworkFromPath('src/server/api/routers/settings.js');
expect(result).not.toBeNull();
expect(result!.framework).toBe('trpc');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('classifies /app/trpc/routers/ as tRPC, not expo-router', () => {
const result = detectFrameworkFromPath('src/app/trpc/routers/user.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('trpc');
expect(result!.framework).not.toBe('expo-router');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('classifies /app/trpc/routers/ JavaScript as tRPC, not expo-router', () => {
const result = detectFrameworkFromPath('src/app/trpc/routers/user.js');
expect(result).not.toBeNull();
expect(result!.framework).toBe('trpc');
expect(result!.framework).not.toBe('expo-router');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('does not treat a generic /routers/ folder as tRPC', () => {
expect(detectFrameworkFromPath('src/routers/vue.ts')).toBeNull();
});
});
describe('Express / Node.js', () => {
it('detects route files', () => {
const result = detectFrameworkFromPath('routes/auth.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('express');
expect(result!.entryPointMultiplier).toBe(2.5);
});
});
describe('MVC controllers', () => {
it('detects controller folder', () => {
const result = detectFrameworkFromPath('controllers/UserController.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('mvc');
});
it('detects handlers folder', () => {
const result = detectFrameworkFromPath('handlers/auth.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('handlers');
});
});
describe('React', () => {
it('has React component detection rule for views/components folders', () => {
const result = detectFrameworkFromPath('views/Button.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('react');
expect(result!.entryPointMultiplier).toBe(1.5);
expect(result!.reason).toBe('react-component');
});
});
describe('Python frameworks', () => {
it('detects Django views', () => {
const result = detectFrameworkFromPath('myapp/views.py');
expect(result).not.toBeNull();
expect(result!.framework).toBe('django');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects Django URLs', () => {
const result = detectFrameworkFromPath('myapp/urls.py');
expect(result).not.toBeNull();
expect(result!.framework).toBe('django');
});
it('detects FastAPI routers', () => {
const result = detectFrameworkFromPath('routers/users.py');
expect(result).not.toBeNull();
expect(result!.framework).toBe('fastapi');
});
});
describe('Java frameworks', () => {
it('detects Spring controllers folder', () => {
const result = detectFrameworkFromPath('controller/UserController.java');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects Spring controller by filename', () => {
const result = detectFrameworkFromPath('src/UserController.java');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects Java service layer', () => {
const result = detectFrameworkFromPath('service/UserService.java');
expect(result).not.toBeNull();
expect(result!.framework).toBe('java-service');
});
});
describe('C# / .NET', () => {
it('detects ASP.NET controllers', () => {
const result = detectFrameworkFromPath('controllers/UsersController.cs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('aspnet');
});
it('detects Blazor pages', () => {
const result = detectFrameworkFromPath('pages/Index.razor');
expect(result).not.toBeNull();
expect(result!.framework).toBe('blazor');
});
});
describe('Go frameworks', () => {
it('detects Go handlers', () => {
const result = detectFrameworkFromPath('handlers/user.go');
expect(result).not.toBeNull();
expect(result!.framework).toBe('go-http');
});
it('detects Go main.go', () => {
const result = detectFrameworkFromPath('cmd/server/main.go');
expect(result).not.toBeNull();
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('does NOT treat Go helper files under cmd/ as entry points', () => {
expect(detectFrameworkFromPath('cmd/server/internal/util.go')).toBeNull();
expect(detectFrameworkFromPath('cmd/foo/config/setup.go')).toBeNull();
});
});
describe('Rust frameworks', () => {
it('detects Rust handlers', () => {
const result = detectFrameworkFromPath('handlers/auth.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust-web');
});
it('detects main.rs', () => {
const result = detectFrameworkFromPath('src/main.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects bin folder', () => {
const result = detectFrameworkFromPath('src/bin/cli.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust');
});
});
describe('C / C++', () => {
it('detects main.c', () => {
const result = detectFrameworkFromPath('src/main.c');
expect(result).not.toBeNull();
expect(result!.framework).toBe('c-cpp');
});
it('detects main.cpp', () => {
const result = detectFrameworkFromPath('src/main.cpp');
expect(result).not.toBeNull();
expect(result!.framework).toBe('c-cpp');
});
});
describe('PHP / Laravel', () => {
it('detects Laravel routes', () => {
const result = detectFrameworkFromPath('routes/web.php');
expect(result).not.toBeNull();
expect(result!.framework).toBe('laravel');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects Laravel controllers', () => {
const result = detectFrameworkFromPath('http/controllers/UserController.php');
expect(result).not.toBeNull();
expect(result!.framework).toBe('laravel');
});
it('detects Laravel jobs', () => {
const result = detectFrameworkFromPath('jobs/SendEmail.php');
expect(result).not.toBeNull();
expect(result!.reason).toBe('laravel-job');
});
it('detects Laravel middleware', () => {
const result = detectFrameworkFromPath('http/middleware/Auth.php');
expect(result).not.toBeNull();
expect(result!.reason).toBe('laravel-middleware');
});
it('detects Laravel models', () => {
const result = detectFrameworkFromPath('models/User.php');
expect(result).not.toBeNull();
expect(result!.entryPointMultiplier).toBe(1.5);
});
});
describe('Swift / iOS', () => {
it('detects AppDelegate', () => {
const result = detectFrameworkFromPath('Sources/AppDelegate.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('ios');
});
it('detects ViewControllers folder', () => {
const result = detectFrameworkFromPath('ViewControllers/LoginVC.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('uikit');
});
it('detects Coordinator pattern', () => {
const result = detectFrameworkFromPath('Coordinators/AppCoordinator.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('ios-coordinator');
});
it('detects SwiftUI views folder', () => {
const result = detectFrameworkFromPath('views/ContentView.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('swiftui');
});
});
describe('generic patterns', () => {
it('returns null for unknown paths', () => {
expect(detectFrameworkFromPath('src/internal/crypto.ts')).toBeNull();
});
it('normalizes Windows backslashes', () => {
const result = detectFrameworkFromPath('routes\\auth.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('express');
});
});
});
describe('detectFrameworkFromAST', () => {
it('returns null for empty inputs', () => {
expect(detectFrameworkFromAST('', '')).toBeNull();
expect(detectFrameworkFromAST('typescript', '')).toBeNull();
expect(detectFrameworkFromAST('', 'some code')).toBeNull();
});
it('detects NestJS decorators in TypeScript', () => {
const result = detectFrameworkFromAST('typescript', '@Controller("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nestjs');
expect(result!.entryPointMultiplier).toBe(3.2);
});
it('detects NestJS decorators in JavaScript', () => {
const result = detectFrameworkFromAST('javascript', '@Get("/")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nestjs');
});
it('detects FastAPI decorators in Python', () => {
const result = detectFrameworkFromAST('python', '@app.get("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('fastapi');
});
it('detects Flask decorators in Python', () => {
const result = detectFrameworkFromAST('python', '@app.route("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('flask');
});
it('detects Spring annotations in Java', () => {
const result = detectFrameworkFromAST('java', '@RestController');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects ASP.NET attributes in C#', () => {
const result = detectFrameworkFromAST('csharp', '[ApiController]');
expect(result).not.toBeNull();
expect(result!.framework).toBe('aspnet');
});
it('detects Laravel route definitions in PHP', () => {
const result = detectFrameworkFromAST(
'php',
"Route::get('/users', [UserController::class, 'index'])",
);
expect(result).not.toBeNull();
expect(result!.framework).toBe('laravel');
});
it('detects Actix-web route attributes in Rust', () => {
const result = detectFrameworkFromAST('rust', '#[get("/")]');
expect(result).not.toBeNull();
expect(result!.framework).toBe('actix-web');
});
it('returns null for language with no matching pattern', () => {
expect(detectFrameworkFromAST('c', 'int main() { return 0; }')).toBeNull();
});
it('is case-insensitive', () => {
const result = detectFrameworkFromAST('TypeScript', '@controller("/")');
expect(result).not.toBeNull();
});
it('detects Expo Router useRouter hook', () => {
const result = detectFrameworkFromAST('typescript', 'const router = useRouter()');
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
});
it('detects Expo Router router.push', () => {
const result = detectFrameworkFromAST('javascript', "router.push('/settings')");
expect(result).not.toBeNull();
expect(result!.framework).toBe('expo-router');
});
});
describe('provider registry exhaustiveness', () => {
// Compile-time exhaustiveness is enforced by `satisfies Record<SupportedLanguages, ...>`
// in languages/index.ts. This runtime guard catches drift if the enum and the
// providers map ever diverge at runtime (e.g. via build-artifact mismatch),
// since both shared lookup maps are built from `Object.entries(providers)`.
it('has one provider per SupportedLanguages enum member', () => {
expect(Object.keys(providers).sort()).toEqual(Object.values(SupportedLanguages).sort());
});
});
describe('provider astFrameworkPatterns', () => {
it('preserves multiplier and reason for high-priority frameworks', () => {
// Pin the exact multiplier and reason strings to catch silent drift
// during the per-language pattern relocation. These three frameworks
// are the most-used decorator-driven entry-point boosters.
const expectedConfigs: Record<string, { multiplier: number; reason: string }> = {
nestjs: { multiplier: 3.2, reason: 'nestjs-decorator' },
spring: { multiplier: 3.2, reason: 'spring-annotation' },
fastapi: { multiplier: 3.0, reason: 'fastapi-decorator' },
};
const seen = new Set<string>();
for (const provider of Object.values(providers)) {
for (const cfg of provider.astFrameworkPatterns ?? []) {
const expected = expectedConfigs[cfg.framework];
if (!expected) continue;
expect(cfg.entryPointMultiplier).toBe(expected.multiplier);
expect(cfg.reason).toBe(expected.reason);
seen.add(cfg.framework);
}
}
for (const fw of Object.keys(expectedConfigs)) {
expect(seen.has(fw), `missing framework config: ${fw}`).toBe(true);
}
});
it('covers all expected frameworks across providers', () => {
const expectedFrameworks = new Set([
'nestjs',
'expo-router',
'fastapi',
'flask',
'spring',
'jaxrs',
'aspnet',
'signalr',
'blazor',
'efcore',
'go-http',
'gin',
'echo',
'fiber',
'go-grpc',
'laravel',
'actix-web',
'axum',
'rocket',
'tokio',
'qt',
'uikit',
'swiftui',
'vapor',
'rails',
'sinatra',
'spring-kotlin',
'ktor',
'android-kotlin',
'flutter',
'riverpod',
]);
const foundFrameworks = new Set<string>();
for (const provider of Object.values(providers)) {
for (const cfg of provider.astFrameworkPatterns ?? []) {
foundFrameworks.add(cfg.framework);
expect(cfg.patterns.length).toBeGreaterThan(0);
}
}
for (const fw of expectedFrameworks) {
expect(foundFrameworks.has(fw), `missing framework: ${fw}`).toBe(true);
}
});
});