GitNexus/gitnexus/test/integration/route-runtime-evidence-roundtrip.test.ts
Abhigyan Patwari 9df1e98350
fix(routes): scope inline Express handlers and report api_impact lower bound (#3643)
* fix(routes): scope inline Express handlers and report api_impact lower bound

Inline Express handlers are anonymous Function nodes named by position, but
the route facts only recorded a handler name for bare identifiers. Routes
with inline handlers therefore had no handlerSymbolId: every route in a file
got the union of its handlers' response keys and every flow starting there.

Record the positional name for inline handlers, read response keys from the
resolved handler when a file registers several routes, and let processes
link exactly by handlerSymbolId as they already do for named handlers.

Count, per route, the files that pass its URL to a callee not proven to be
a fetch wrapper (excluding proven consumers and the route's own file). The
count is stored as Route.unverifiedConsumers; api_impact reports
epistemic: 'lower-bound' with that count when it is non-zero. Older indexes
without the column fall back to the previous query.

Closes #3488

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(routes): match unverified consumers once per URL

Memoize route matching per distinct fetch URL, skip the proven-consumer
scan when no unproven call matches a route, and build the Route query's
optional columns from a list instead of two booleans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(group): keep inline handlers attributed to their registrar

Inline Express handlers now carry their positional Function name in the
route facts so ingestion can resolve a handlerSymbolId. Group HTTP
contracts read the same facts, but that name is a graph identity rather
than a symbol name the contract lookup can match, so the provider lost its
enclosing-registrar attribution. Ignore the name for inline handlers there.

Rebaseline the two bench guards the branch moved:
- emit-persistence: route.csv gains the header-only unverifiedConsumers
  field; stripping it restores the prior fingerprint exactly.
- receiver-resolution: the new fixture's req.query.path read on an untyped
  inline-handler parameter adds one receiver-unresolved read (191 -> 192).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-20 17:44:08 +01:00

71 lines
2.6 KiB
TypeScript

/**
* Real-LadybugDB round trip for Spring Actuator Route evidence (#2418).
* Pins the schema, CSV, COPY, and graph-read projections that must agree for
* runtime-confirmed markers to survive beyond the in-memory ingestion graph.
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { expect, it } from 'vitest';
import { getNodeQuery } from '../../src/server/api.js';
import { streamAllCSVsToDisk } from '../../src/core/lbug/csv-generator.js';
import { generateId } from '../../src/lib/utils.js';
import { buildTestGraph } from '../helpers/test-graph.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
const ROUTE_ID = generateId('Route', 'GET /runtime-confirmed');
withTestLbugDB('route-runtime-evidence-roundtrip', (handle) => {
it('persists and projects Route runtime evidence through CSV→COPY→query', async () => {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
const graph = buildTestGraph([
{
id: ROUTE_ID,
label: 'Route',
name: '/runtime-confirmed',
filePath: 'RuntimeController.java',
extra: {
method: 'GET',
responseKeys: [],
errorKeys: [],
middleware: [],
runtimeConfirmed: true,
runtimeSource: 'spring-actuator',
runtimeStatus: 'runtime-confirmed',
unverifiedConsumers: 3,
},
},
]);
const csvDir = path.join(handle.tmpHandle.dbPath, 'csv-runtime-evidence');
const repoDir = path.join(handle.tmpHandle.dbPath, 'repo-runtime-evidence');
await fs.mkdir(repoDir, { recursive: true });
await streamAllCSVsToDisk(graph, repoDir, csvDir);
const routeCsv = await fs.readFile(path.join(csvDir, 'route.csv'), 'utf8');
expect(routeCsv.split('\n')[0]).toContain(
'runtimeConfirmed,runtimeSource,runtimeStatus,unverifiedConsumers',
);
expect(routeCsv).toContain('spring-actuator');
const routeCsvPath = path.join(csvDir, 'route.csv').replace(/\\/g, '/');
await adapter.executeQuery(adapter.getCopyQuery('Route', routeCsvPath));
const rows = (await adapter.executeQuery(getNodeQuery('Route', false))) as Record<
string,
unknown
>[];
expect(rows).toContainEqual(
expect.objectContaining({
id: ROUTE_ID,
runtimeConfirmed: true,
runtimeSource: 'spring-actuator',
runtimeStatus: 'runtime-confirmed',
}),
);
expect(
await adapter.executeQuery(
`MATCH (n:Route) WHERE n.id = '${ROUTE_ID}' RETURN n.unverifiedConsumers AS unverifiedConsumers`,
),
).toEqual([{ unverifiedConsumers: 3 }]);
});
});