mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
* feat: configure prettier with pre-commit hook integration Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo root with husky pre-commit hook integration. Moves husky from gitnexus/ to root package.json for reliable hook installation. - Root package.json with prepare/format/format:check scripts - .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4 - .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md - .gitattributes enforcing LF line endings for Windows consistency - Pre-commit hook uses direct node_modules/.bin/ paths (no npx) * style: apply prettier formatting to entire codebase One-time bulk format. No logic changes. Use .git-blame-ignore-revs to skip this commit in git blame. * chore: add .git-blame-ignore-revs for prettier format commit * perf: pre-commit hook runs only tests related to staged files Use vitest --related to scope test execution to tests that import the changed files, instead of running the full suite on every commit. * perf: remove vitest from pre-commit hook, keep in CI only Pre-commit now runs lint-staged + tsc only. Tests run in CI (ci-tests.yml) where they belong — keeps commits fast. * ci: add prettier format check to quality workflow PRs will now fail if code isn't formatted with prettier.
174 lines
7.4 KiB
TypeScript
174 lines
7.4 KiB
TypeScript
/**
|
|
* Pipeline-level integration tests for deep flow detection features.
|
|
*
|
|
* Runs runPipelineFromRepo on the api-e2e-test fixture and verifies
|
|
* that the extraction pipeline produces correct Route node properties
|
|
* (responseKeys, errorKeys, middleware) and FETCHES edge reason encoding
|
|
* (consumer accessed keys, multi-fetch attribution).
|
|
*
|
|
* This complements the seed-based api-impact-e2e.test.ts which tests
|
|
* the query/tool layer. Together they cover extraction → storage → query.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import path from 'path';
|
|
import {
|
|
FIXTURES,
|
|
getRelationships,
|
|
getNodesByLabel,
|
|
getNodesByLabelFull,
|
|
runPipelineFromRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
describe('deep flow detection pipeline', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'api-e2e-test'), () => {});
|
|
}, 60000);
|
|
|
|
// ─── Route nodes ────────────────────────────────────────────────
|
|
|
|
it('creates Route nodes for /api/grants and /api/secure', () => {
|
|
const routes = getNodesByLabel(result, 'Route');
|
|
expect(routes).toContain('/api/grants');
|
|
expect(routes).toContain('/api/secure');
|
|
});
|
|
|
|
// ─── Response shape extraction ─────────────────────────────────
|
|
|
|
it('extracts responseKeys from NextResponse.json() success path', () => {
|
|
const routes = getNodesByLabelFull(result, 'Route');
|
|
const grants = routes.find((r) => r.name === '/api/grants');
|
|
expect(grants).toBeDefined();
|
|
expect(grants!.properties.responseKeys).toEqual(expect.arrayContaining(['data', 'pagination']));
|
|
});
|
|
|
|
it('extracts errorKeys from NextResponse.json() with status >= 400', () => {
|
|
const routes = getNodesByLabelFull(result, 'Route');
|
|
const grants = routes.find((r) => r.name === '/api/grants');
|
|
expect(grants).toBeDefined();
|
|
expect(grants!.properties.errorKeys).toEqual(expect.arrayContaining(['error', 'message']));
|
|
});
|
|
|
|
it('keeps success and error keys separate (no cross-contamination)', () => {
|
|
const routes = getNodesByLabelFull(result, 'Route');
|
|
const grants = routes.find((r) => r.name === '/api/grants');
|
|
expect(grants).toBeDefined();
|
|
|
|
const successKeys = new Set(grants!.properties.responseKeys ?? []);
|
|
const errorKeys = new Set(grants!.properties.errorKeys ?? []);
|
|
|
|
// 'error' and 'message' should only be in errorKeys
|
|
expect(successKeys.has('error')).toBe(false);
|
|
expect(successKeys.has('message')).toBe(false);
|
|
// 'data' and 'pagination' should only be in responseKeys
|
|
expect(errorKeys.has('data')).toBe(false);
|
|
expect(errorKeys.has('pagination')).toBe(false);
|
|
});
|
|
|
|
// ─── Middleware chain extraction ───────────────────────────────
|
|
|
|
it('extracts middleware chain for withAuth(withRateLimit(...)) wrapper', () => {
|
|
const routes = getNodesByLabelFull(result, 'Route');
|
|
const secure = routes.find((r) => r.name === '/api/secure');
|
|
expect(secure).toBeDefined();
|
|
expect(secure!.properties.middleware).toBeDefined();
|
|
expect(secure!.properties.middleware).toContain('withAuth');
|
|
expect(secure!.properties.middleware).toContain('withRateLimit');
|
|
});
|
|
|
|
it('stores middleware in outermost-first order', () => {
|
|
const routes = getNodesByLabelFull(result, 'Route');
|
|
const secure = routes.find((r) => r.name === '/api/secure');
|
|
expect(secure).toBeDefined();
|
|
const mw = secure!.properties.middleware ?? [];
|
|
const authIdx = mw.indexOf('withAuth');
|
|
const rlIdx = mw.indexOf('withRateLimit');
|
|
// withAuth wraps withRateLimit, so withAuth should come first
|
|
expect(authIdx).toBeLessThan(rlIdx);
|
|
});
|
|
|
|
// ─── FETCHES edges ────────────────────────────────────────────
|
|
|
|
it('creates FETCHES edges from consumer files to Route nodes', () => {
|
|
const edges = getRelationships(result, 'FETCHES');
|
|
expect(edges.length).toBeGreaterThanOrEqual(2);
|
|
|
|
// GrantsList → /api/grants
|
|
const grantsListEdge = edges.find(
|
|
(e) => e.sourceFilePath.includes('GrantsList') && e.target === '/api/grants',
|
|
);
|
|
expect(grantsListEdge).toBeDefined();
|
|
});
|
|
|
|
it('encodes consumer accessed keys in FETCHES reason field', () => {
|
|
const edges = getRelationships(result, 'FETCHES');
|
|
|
|
// GrantsList destructures { data, pagination } from the response
|
|
const grantsListEdge = edges.find(
|
|
(e) => e.sourceFilePath.includes('GrantsList') && e.target === '/api/grants',
|
|
);
|
|
expect(grantsListEdge).toBeDefined();
|
|
expect(grantsListEdge!.rel.reason).toContain('keys:');
|
|
|
|
// Parse the keys from the reason field
|
|
const keysMatch = grantsListEdge!.rel.reason?.match(/keys:([^|]+)/);
|
|
expect(keysMatch).not.toBeNull();
|
|
const keys = keysMatch![1].split(',');
|
|
expect(keys).toContain('data');
|
|
expect(keys).toContain('pagination');
|
|
});
|
|
|
|
it('encodes multi-fetch count when consumer fetches multiple routes', () => {
|
|
const edges = getRelationships(result, 'FETCHES');
|
|
|
|
// useMulti fetches both /api/grants and /api/secure
|
|
const useMultiGrants = edges.find(
|
|
(e) => e.sourceFilePath.includes('useMulti') && e.target === '/api/grants',
|
|
);
|
|
const useMultiSecure = edges.find(
|
|
(e) => e.sourceFilePath.includes('useMulti') && e.target === '/api/secure',
|
|
);
|
|
expect(useMultiGrants).toBeDefined();
|
|
expect(useMultiSecure).toBeDefined();
|
|
|
|
// Both edges should have fetches:2 in reason
|
|
expect(useMultiGrants!.rel.reason).toContain('fetches:2');
|
|
expect(useMultiSecure!.rel.reason).toContain('fetches:2');
|
|
});
|
|
|
|
// ─── HANDLES_ROUTE edges ──────────────────────────────────────
|
|
|
|
it('creates HANDLES_ROUTE edges from handler functions to Route nodes', () => {
|
|
const edges = getRelationships(result, 'HANDLES_ROUTE');
|
|
expect(edges.length).toBeGreaterThanOrEqual(2);
|
|
|
|
const grantsHandler = edges.find((e) => e.target === '/api/grants');
|
|
expect(grantsHandler).toBeDefined();
|
|
expect(grantsHandler!.sourceFilePath).toContain('app/api/grants/route.ts');
|
|
});
|
|
|
|
// ─── Mismatch-detectable data ─────────────────────────────────
|
|
|
|
it('useGrants accesses items which is NOT in grants responseKeys (mismatch scenario)', () => {
|
|
const routes = getNodesByLabelFull(result, 'Route');
|
|
const grants = routes.find((r) => r.name === '/api/grants');
|
|
const edges = getRelationships(result, 'FETCHES');
|
|
const useGrantsEdge = edges.find(
|
|
(e) => e.sourceFilePath.includes('useGrants') && e.target === '/api/grants',
|
|
);
|
|
expect(useGrantsEdge).toBeDefined();
|
|
|
|
// useGrants.ts does data.items — 'items' should be in consumer keys
|
|
const keysMatch = useGrantsEdge!.rel.reason?.match(/keys:([^|]+)/);
|
|
expect(keysMatch).not.toBeNull();
|
|
const consumerKeys = keysMatch![1].split(',');
|
|
expect(consumerKeys).toContain('items');
|
|
|
|
// But /api/grants responseKeys are [data, pagination] — 'items' is NOT there
|
|
const responseKeys = new Set(grants!.properties.responseKeys ?? []);
|
|
expect(responseKeys.has('items')).toBe(false);
|
|
// This is the mismatch the api_impact tool would detect
|
|
});
|
|
});
|