mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
* feat(ingestion/routes): give Route nodes a (method, url) identity (#2289) Route node identity was URL-only, so a same-URL multi-verb pair (GET /x + POST /x) collapsed into a single node and the second verb's handler and execution flow were silently lost. Route identity is now (method, url) via routeNodeKey(method, url): a known, specific verb keys as "METHOD url", while a method-less route (filesystem routes — Next.js / Expo / PHP — and Laravel resource/apiResource) or a wildcard "*" route (e.g. Django function views) falls back to URL-only. The fallback is byte-identical to the previous URL-only ids, so only genuine declaration-style multi-verb routes split into separate nodes. The identity key is shared across the three phases that must agree on the Route node id: - routes phase: registry key + node id + handler-symbol lookup; the Route node still carries the bare URL as its display name. - call-processor: resolveRouteHandlerSymbols re-keyed by identity so each verb resolves its own handler; a verb-less fetch() consumer matches by URL and connects to every Route node at that URL (one per verb). - processes phase: ENTRY_POINT_OF targets the identity-keyed node id. Bumps INCREMENTAL_SCHEMA_VERSION 4 -> 5: persisted pre-v5 Route nodes use the old url-only ids, so an incremental top-up would strand them alongside new composite-keyed nodes — force a full re-analyze instead. Part of #2280. * fix(ingestion/routes): address PR #2302 review (P1/P2/P3) P1 — Schema v5 fast-path bypass (run-analyze.ts): Adds a schemaVersion-mismatch guard above the alreadyUpToDate early-return, mirroring the pdgModeMismatch slot. Without it, a same-commit re-analyze on a pre-v5 stamp returned alreadyUpToDate without ever reaching the isIncremental gate, defeating the v5 schema bump's migration intent. Regression test covers: analyze (stamps v5) → meta downgrade to v4 → same commit re-analyze must NOT early-return and meta restamps to v5. P2 — ENTRY_POINT_OF handler-aware linking (processes.ts): Pre-fix routesByFile fanned every same-file Route to every same-file process, cross-wiring same-file GET/POST handlers. Now reads handlerSymbolId off the Route graph node (the source of truth routes.ts stamps) into routesByHandlerId, with a routesWithoutHandlerByFile fallback — mirrors the Tool linking precedent 10 lines below. Two regression tests: weak form (only one handler has a process; sibling verb does not get spuriously attached) and strong form (both handlers form distinct processes; each Route links to exactly its own entryPoint, 2 edges not pre-fix 4). P2 — Roundtrip composite-id (route-{method,handler-symbol}-roundtrip): Both tests now seed the Route node with generateId('Route', routeNodeKey('POST', '/api/orders')) and run the Cypher MATCH against the composite id, exercising the literal-space-in-id through CSV→COPY→HANDLES_ROUTE_QUERY. A space-in-id escape regression would surface here instead of being silently swallowed by the extractor's catch. P3 — doc-drift + test if: - route-path.ts:4 — header updated to "(method, url) via routeNodeKey" - java.ts:684 — drop "Route nodes are URL-keyed"; #2289 closes that gap - manifest-extractor.ts:196 — explicit that Route node *id* is composite while route.name remains the bare URL - multi-verb-route-identity.test.ts:88 — forEachRelationship+if rewritten as a .filter().map() chain (no test-level conditional). New route-process-linking tests are also if-free. Validation: tsc clean, prettier clean, 9 touched suites / 43 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ingestion/routes): drop routes.ts re-export, fix CI fast-path tests Two follow-ups on PR #2302's CHANGES_REQUESTED review: 1. Drop `routes.ts` re-export of `normalizeExtractedRoutePath` / `normalizeRouteMethod` / `routeNodeKey` (per @magyargergo's inline comment at routes.ts:153 — the symbols already live in `route-extractors/route-path.ts` and consumers should import them from the source, not via a routes-phase indirection that was kept only as a compat shim during the #2289 refactor). Updated the two remaining callers (blade-template-routes / spring-route-extractor- parity tests) to import directly from `route-extractors/route-path.js`. `call-processor.ts` and `processes.ts` already import from the source. 2. Fix two `run-analyze.test.ts` fast-path tests that started failing on CI after the schema-version mismatch guard landed ( "creates .gitnexus/.gitignore on the already-up-to-date fast path" and "reports isPrimaryBranch false for an up-to-date non-primary branch"). The test fixtures hand-built a RepoMeta with NO schemaVersion field; with the guard now checking `existingMeta.schemaVersion !== INCREMENTAL_SCHEMA_VERSION`, that pre-versioning shape was treated as a mismatch and forced a rebuild, short-circuiting the fast path the tests exercise. Stamp the current schemaVersion on those fixtures so they reflect the post-#2289 meta shape production actually writes (`runFullAnalysis` always stamps the field on git repos — see meta save site). Validation: tsc clean, prettier clean, 11 touched suites / 80 tests pass. --------- Co-authored-by: henry <zhangwei2017@unipus.cn> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
173 lines
6.5 KiB
TypeScript
173 lines
6.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import fs from 'fs/promises';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import {
|
|
extractTemplateStaticFetchCalls,
|
|
isTemplateRouteCandidate,
|
|
routesPhase,
|
|
} from '../../src/core/ingestion/pipeline-phases/routes.js';
|
|
import {
|
|
normalizeExtractedRoutePath,
|
|
routeNodeKey,
|
|
} from '../../src/core/ingestion/route-extractors/route-path.js';
|
|
import type { ParseOutput } from '../../src/core/ingestion/pipeline-phases/parse.js';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
|
import { generateId } from '../../src/lib/utils.js';
|
|
|
|
describe('Blade/template static route extraction', () => {
|
|
it('keeps Blade files as template route candidates', () => {
|
|
expect(isTemplateRouteCandidate('resources/views/orders/index.blade.php')).toBe(true);
|
|
expect(isTemplateRouteCandidate('resources\\views\\Orders\\INDEX.BLADE.PHP')).toBe(true);
|
|
});
|
|
|
|
it('extracts safe static form, href, AJAX, and Blade URL helper URLs', () => {
|
|
const calls = extractTemplateStaticFetchCalls(
|
|
'resources/views/orders/index.blade.php',
|
|
`<form action="/orders" method="POST">
|
|
<a href='/orders/history'>History</a>
|
|
<script>
|
|
$.ajax({ url: '/api/orders', method: 'POST' });
|
|
</script>
|
|
<a href="{{ url('/checkout') }}">Checkout</a>
|
|
<a href="{!! url('/checkout/raw') !!}">Raw checkout</a>
|
|
</form>`,
|
|
);
|
|
|
|
expect(new Set(calls.map((call) => call.fetchURL))).toEqual(
|
|
new Set(['/orders', '/orders/history', '/api/orders', '/checkout', '/checkout/raw']),
|
|
);
|
|
expect(new Set(calls.map((call) => call.filePath))).toEqual(
|
|
new Set(['resources/views/orders/index.blade.php']),
|
|
);
|
|
});
|
|
|
|
it('does not treat Laravel asset helper URLs as route signals', () => {
|
|
const calls = extractTemplateStaticFetchCalls(
|
|
'resources/views/layouts/app.blade.php',
|
|
`<link href="{{ asset('/css/app.css') }}" rel="stylesheet">
|
|
<script src="{!! asset('/js/app.js') !!}"></script>`,
|
|
);
|
|
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it('resolves parameterless Blade named route helpers from extracted route names', () => {
|
|
const calls = extractTemplateStaticFetchCalls(
|
|
'resources/views/auth/login.blade.php',
|
|
`<a href="{{ route('login') }}">Login</a>
|
|
<form method="POST" action="{!! route('log-viewer.login.submit') !!}"></form>
|
|
<a href="{{ route('orders.show') }}">Missing parameter</a>
|
|
<a href="{{ route('orders.show', $order) }}">Dynamic order</a>`,
|
|
new Map([
|
|
['login', '/login'],
|
|
['log-viewer.login.submit', '/logs/login'],
|
|
['orders.show', '/orders/{order}'],
|
|
]),
|
|
);
|
|
|
|
expect(calls.map((call) => call.fetchURL)).toEqual(['/login', '/logs/login']);
|
|
});
|
|
|
|
it('does not turn dynamic Blade expressions or parameterized named routes into static URL signals', () => {
|
|
const calls = extractTemplateStaticFetchCalls(
|
|
'resources/views/orders/show.blade.php',
|
|
`<a href="{{ $url }}">Dynamic</a>
|
|
<a href="{{ route('orders.show', $order) }}">Named route follow-up</a>
|
|
<a href="{{ url($dynamic) }}">Dynamic helper</a>
|
|
<script>
|
|
$.ajax({ url: '/orders/' + id, method: 'GET' });
|
|
axios({ url: \`/orders/\${id}\`, method: 'GET' });
|
|
</script>`,
|
|
);
|
|
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it('normalizes Laravel route prefixes before matching template URL signals', () => {
|
|
expect(normalizeExtractedRoutePath('/orders', 'admin')).toBe('/admin/orders');
|
|
expect(normalizeExtractedRoutePath('orders', '/admin/')).toBe('/admin/orders');
|
|
expect(normalizeExtractedRoutePath('/', 'admin')).toBe('/admin');
|
|
expect(normalizeExtractedRoutePath('/orders', null)).toBe('/orders');
|
|
});
|
|
|
|
it('links Blade static URL signals to matching route graph nodes without PHP parsing', async () => {
|
|
const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-blade-routes-'));
|
|
try {
|
|
await fs.mkdir(path.join(repoPath, 'routes'), { recursive: true });
|
|
await fs.mkdir(path.join(repoPath, 'resources/views/orders'), { recursive: true });
|
|
await fs.writeFile(path.join(repoPath, 'routes/web.php'), `<?php\nRoute::post('/orders');\n`);
|
|
await fs.writeFile(
|
|
path.join(repoPath, 'resources/views/orders/index.blade.php'),
|
|
`<form action="/admin/orders" method="POST">
|
|
<a href="{{ route('admin.orders') }}">Orders</a>
|
|
<link href="{{ asset('/css/app.css') }}" rel="stylesheet">
|
|
</form>`,
|
|
);
|
|
|
|
const graph = createKnowledgeGraph();
|
|
const parseOutput = {
|
|
allPaths: ['routes/web.php', 'resources/views/orders/index.blade.php'],
|
|
allFetchCalls: [],
|
|
allExtractedRoutes: [
|
|
{
|
|
filePath: 'routes/web.php',
|
|
httpMethod: 'post',
|
|
routePath: '/orders',
|
|
routeName: 'admin.orders',
|
|
controllerName: null,
|
|
methodName: null,
|
|
middleware: [],
|
|
prefix: 'admin',
|
|
lineNumber: 1,
|
|
},
|
|
{
|
|
filePath: 'routes/web.php',
|
|
httpMethod: 'get',
|
|
routePath: '/css/app.css',
|
|
routeName: 'assets.css',
|
|
controllerName: null,
|
|
methodName: null,
|
|
middleware: [],
|
|
prefix: null,
|
|
lineNumber: 2,
|
|
},
|
|
],
|
|
allDecoratorRoutes: [],
|
|
routeHandlerSymbols: new Map(),
|
|
} as unknown as ParseOutput;
|
|
|
|
const output = await routesPhase.execute(
|
|
{
|
|
repoPath,
|
|
graph,
|
|
onProgress: () => {},
|
|
pipelineStart: Date.now(),
|
|
},
|
|
new Map([['parse', { phaseName: 'parse', output: parseOutput, durationMs: 0 }]]),
|
|
);
|
|
|
|
// The registry is keyed by the `(method, url)` identity (#2289); the URL
|
|
// is carried on the entry's `url` field (the Route node's display name).
|
|
expect(output.routeRegistry.get(routeNodeKey('POST', '/admin/orders'))).toEqual({
|
|
filePath: 'routes/web.php',
|
|
source: 'framework-route',
|
|
url: '/admin/orders',
|
|
method: 'POST',
|
|
});
|
|
|
|
const fetchEdges = graph.relationships.filter((rel) => rel.type === 'FETCHES');
|
|
expect(fetchEdges).toHaveLength(1);
|
|
expect(fetchEdges.map((rel) => graph.getNode(rel.targetId)?.properties.name)).toEqual([
|
|
'/admin/orders',
|
|
]);
|
|
const target = graph.getNode(fetchEdges[0]!.targetId);
|
|
expect(fetchEdges[0]!.sourceId).toBe(
|
|
generateId('File', 'resources/views/orders/index.blade.php'),
|
|
);
|
|
expect(target?.properties.name).toBe('/admin/orders');
|
|
} finally {
|
|
await fs.rm(repoPath, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|