/** * Unit tests for route extractors, tool detection patterns, and response shape parsing. */ import { describe, it, expect } from 'vitest'; import { nextjsFileToRouteURL, normalizeFetchURL, routeMatches, } from '../../src/core/ingestion/route-extractors/nextjs.js'; import { phpFileToRouteURL } from '../../src/core/ingestion/route-extractors/php.js'; import { extractMiddlewareChain, extractNextjsMiddlewareConfig, middlewareMatcherMatchesRoute, } from '../../src/core/ingestion/route-extractors/middleware.js'; import { detectStatusCode, extractResponseShapes, extractPHPResponseShapes, } from '../../src/core/ingestion/route-extractors/response-shapes.js'; // --------------------------------------------------------------------------- // Next.js route extractor // --------------------------------------------------------------------------- describe('nextjsFileToRouteURL', () => { it('extracts App Router API routes', () => { expect(nextjsFileToRouteURL('app/api/grants/route.ts')).toBe('/api/grants'); expect(nextjsFileToRouteURL('app/api/users/route.js')).toBe('/api/users'); expect(nextjsFileToRouteURL('app/api/auth/login/route.tsx')).toBe('/api/auth/login'); }); it('handles dynamic segments', () => { expect(nextjsFileToRouteURL('app/api/organizations/[slug]/grants/route.ts')).toBe( '/api/organizations/[slug]/grants', ); expect(nextjsFileToRouteURL('app/api/users/[id]/route.ts')).toBe('/api/users/[id]'); }); it('only matches api/ routes in App Router', () => { // Non-API App Router routes should be excluded expect(nextjsFileToRouteURL('app/dashboard/route.ts')).toBeNull(); expect(nextjsFileToRouteURL('app/(marketing)/about/route.ts')).toBeNull(); }); it('strips route groups from App Router paths', () => { expect(nextjsFileToRouteURL('app/(admin)/api/users/route.ts')).toBe('/api/users'); expect(nextjsFileToRouteURL('app/(marketing)/api/newsletter/route.ts')).toBe('/api/newsletter'); }); it('extracts Pages Router API routes', () => { expect(nextjsFileToRouteURL('pages/api/auth/login.ts')).toBe('/api/auth/login'); expect(nextjsFileToRouteURL('pages/api/users.ts')).toBe('/api/users'); }); it('strips /index suffix from Pages Router', () => { expect(nextjsFileToRouteURL('pages/api/index.ts')).toBe('/api'); }); it('returns null for non-route files', () => { expect(nextjsFileToRouteURL('src/components/Button.tsx')).toBeNull(); expect(nextjsFileToRouteURL('src/lib/utils.ts')).toBeNull(); expect(nextjsFileToRouteURL('app/page.tsx')).toBeNull(); }); it('handles Windows-style backslash paths', () => { expect(nextjsFileToRouteURL('app\\api\\grants\\route.ts')).toBe('/api/grants'); }); }); // --------------------------------------------------------------------------- // PHP route extractor // --------------------------------------------------------------------------- describe('phpFileToRouteURL', () => { it('extracts routes from api/ directory', () => { expect(phpFileToRouteURL('api/upload.php')).toBe('/api/upload'); expect(phpFileToRouteURL('api/next_sign.php')).toBe('/api/next_sign'); expect(phpFileToRouteURL('api/auth.php')).toBe('/api/auth'); }); it('handles nested api directories', () => { expect(phpFileToRouteURL('api/v2/users.php')).toBe('/api/v2/users'); }); it('returns null for non-api PHP files', () => { expect(phpFileToRouteURL('index.php')).toBeNull(); expect(phpFileToRouteURL('includes/database.php')).toBeNull(); expect(phpFileToRouteURL('vendor/lib/api/config.php')).toBeNull(); }); it('filters out non-handler files in api/', () => { expect(phpFileToRouteURL('api/_helpers.php')).toBeNull(); expect(phpFileToRouteURL('api/helper_utils.php')).toBeNull(); expect(phpFileToRouteURL('api/test_upload.php')).toBeNull(); expect(phpFileToRouteURL('api/fixture_data.php')).toBeNull(); }); it('does not false-filter legitimate endpoints with substring matches', () => { // "contest" contains "test", "attestation" contains "test" — should NOT be filtered // Word-boundary regex only matches _test_ not substrings expect(phpFileToRouteURL('api/contest.php')).toBe('/api/contest'); expect(phpFileToRouteURL('api/attestation.php')).toBe('/api/attestation'); expect(phpFileToRouteURL('api/latest.php')).toBe('/api/latest'); expect(phpFileToRouteURL('api/base64_encode.php')).toBe('/api/base64_encode'); }); it('returns null for non-PHP files', () => { expect(phpFileToRouteURL('api/readme.md')).toBeNull(); }); }); // --------------------------------------------------------------------------- // Fetch URL normalization // --------------------------------------------------------------------------- describe('normalizeFetchURL', () => { it('passes through clean API URLs', () => { expect(normalizeFetchURL('/api/grants')).toBe('/api/grants'); expect(normalizeFetchURL('/api/users/123')).toBe('/api/users/123'); }); it('strips query strings', () => { expect(normalizeFetchURL('/api/grants?page=1&limit=10')).toBe('/api/grants'); }); it('replaces template expressions with [param]', () => { expect(normalizeFetchURL('/api/organizations/${slug}/grants')).toBe( '/api/organizations/[param]/grants', ); }); it('strips backticks from template literals', () => { expect(normalizeFetchURL('`/api/grants`')).toBe('/api/grants'); }); it('accepts non-/api/ absolute paths', () => { expect(normalizeFetchURL('/v1/users')).toBe('/v1/users'); expect(normalizeFetchURL('/graphql')).toBe('/graphql'); expect(normalizeFetchURL('/dashboard')).toBe('/dashboard'); }); it('returns null for unresolvable patterns', () => { // String concatenation in source code: '/api/' + endpoint expect(normalizeFetchURL('/api/+endpoint')).toBeNull(); // Function call wrapper expect(normalizeFetchURL('getApiUrl()')).toBeNull(); }); }); // --------------------------------------------------------------------------- // Route matching // --------------------------------------------------------------------------- describe('routeMatches', () => { it('matches exact routes', () => { expect(routeMatches('/api/grants', '/api/grants')).toBe(true); }); it('does not match different routes', () => { expect(routeMatches('/api/grants', '/api/users')).toBe(false); }); it('does not match different segment counts', () => { expect(routeMatches('/api/grants', '/api/grants/123')).toBe(false); }); it('matches dynamic segments on either side', () => { expect(routeMatches('/api/orgs/[param]', '/api/orgs/[slug]')).toBe(true); expect(routeMatches('/api/orgs/acme', '/api/orgs/[slug]')).toBe(true); expect(routeMatches('/api/orgs/[param]/grants', '/api/orgs/[slug]/grants')).toBe(true); }); it('matches catch-all routes against longer paths', () => { expect(routeMatches('/api/docs/a/b/c', '/api/[...slug]')).toBe(true); expect(routeMatches('/api/proxy/x', '/api/[...slug]')).toBe(true); expect(routeMatches('/api/proxy/x/y/z', '/api/[...slug]')).toBe(true); }); it('does not match catch-all when prefix segments differ', () => { expect(routeMatches('/v1/docs/a', '/api/[...slug]')).toBe(false); }); it('does not match catch-all with too few segments', () => { expect(routeMatches('/', '/api/[...slug]')).toBe(false); }); it('matches optional catch-all routes [[...slug]]', () => { expect(routeMatches('/api/docs/a/b', '/api/[[...slug]]')).toBe(true); expect(routeMatches('/api/proxy/x', '/api/[[...slug]]')).toBe(true); }); }); // --------------------------------------------------------------------------- // Response shape extraction (brace-depth parser edge cases) // --------------------------------------------------------------------------- describe('response shape extraction edge cases', () => { // Helper that simulates the pipeline's brace-depth parser function extractKeysFromContent(content: string): string[] { const keys: string[] = []; const jsonPattern = /\.json\s*\(/g; let jsonMatch; while ((jsonMatch = jsonPattern.exec(content)) !== null) { const startIdx = jsonMatch.index + jsonMatch[0].length; let i = startIdx; while (i < content.length && content[i] !== '{' && content[i] !== ')') i++; if (i >= content.length || content[i] !== '{') continue; let depth = 0; let keyStart = -1; let inString: string | null = null; for (let j = i; j < content.length; j++) { const ch = content[j]; if (inString) { if (ch === '\\') { j++; continue; } if (ch === inString) inString = null; continue; } if (ch === '"' || ch === "'" || ch === '`') { inString = ch; continue; } if (ch === '{') { depth++; continue; } if (ch === '}') { depth--; if (depth === 0) break; continue; } if (depth !== 1) continue; if (keyStart === -1 && /[a-zA-Z_$]/.test(ch)) { keyStart = j; } else if (keyStart !== -1 && !/[a-zA-Z0-9_$]/.test(ch)) { const key = content.slice(keyStart, j); const rest = content.slice(j).trimStart(); if (rest[0] === ':' || rest[0] === ',' || rest[0] === '}') { keys.push(key); } keyStart = -1; } } } return [...new Set(keys)]; } it('extracts simple shorthand properties', () => { const keys = extractKeysFromContent('res.json({ data, total })'); expect(keys).toEqual(['data', 'total']); }); it('extracts key-value properties (values that look like identifiers are also captured)', () => { // Limitation: the text-based parser can't distinguish `data: grants` (key-value) // from `grants` (shorthand). Identifier values followed by , are captured as keys. // This is acceptable — false positive keys are better than missed keys. const keys = extractKeysFromContent('res.json({ data: grants, count: 5 })'); expect(keys).toContain('data'); expect(keys).toContain('count'); }); it('handles nested objects without extracting inner keys', () => { const keys = extractKeysFromContent( 'res.json({ data: grants, pagination: { page: 1, total: 10 }, meta: "ok" })', ); expect(keys).toContain('data'); expect(keys).toContain('pagination'); expect(keys).toContain('meta'); expect(keys).not.toContain('page'); expect(keys).not.toContain('total'); }); it('handles braces inside string literals', () => { const keys = extractKeysFromContent('res.json({ message: "Use { and } carefully", count: 5 })'); expect(keys).toContain('message'); expect(keys).toContain('count'); expect(keys).not.toContain('and'); expect(keys).not.toContain('carefully'); }); it('handles escaped quotes in strings', () => { const keys = extractKeysFromContent('res.json({ msg: "He said \\"hello\\"", ok: true })'); expect(keys).toContain('msg'); expect(keys).toContain('ok'); }); it('handles NextResponse.json pattern', () => { const keys = extractKeysFromContent( 'return NextResponse.json({ data: grants, pagination: { page: 1 } })', ); expect(keys).toContain('data'); expect(keys).toContain('pagination'); expect(keys).not.toContain('page'); }); it('returns empty for non-object arguments', () => { const keys = extractKeysFromContent('res.json("error")'); expect(keys).toEqual([]); }); }); // --------------------------------------------------------------------------- // Middleware chain extraction // --------------------------------------------------------------------------- describe('middleware chain extraction', () => { it('extracts triple-nested middleware chain', () => { const content = `export const POST = withRateLimit(withCSRF(withAuth(async (req) => { return NextResponse.json({ ok: true }); })));`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withRateLimit', 'withCSRF', 'withAuth'], method: 'POST', }); }); it('extracts double-nested middleware chain', () => { const content = `export const GET = withAuth(withCache(handler));`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withAuth', 'withCache'], method: 'GET', }); }); it('extracts single withX wrapper', () => { const content = `export const POST = withAuth(handler);`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withAuth'], method: 'POST' }); }); it('extracts from export default', () => { const content = `export default withAuth(handler);`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withAuth'], method: 'default' }); }); it('extracts from export default with nesting', () => { const content = `export default withRateLimit(withAuth(handler));`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withRateLimit', 'withAuth'], method: 'default', }); }); it('stops at async keyword (arrow function body)', () => { const content = `export const POST = withAuth(async (req) => { return res.json({}); });`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withAuth'], method: 'POST' }); }); it('returns undefined for plain handler without wrappers', () => { const content = `export const POST = handler;`; expect(extractMiddlewareChain(content)).toBeUndefined(); }); it('returns undefined for non-middleware single wrapper', () => { // createHandler is not a withX pattern and chain length is 1 const content = `export const POST = createHandler(config);`; expect(extractMiddlewareChain(content)).toBeUndefined(); }); it('handles multiple HTTP methods — uses first match', () => { const content = ` export const GET = withCache(handler); export const POST = withAuth(withCSRF(handler)); `; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withCache'], method: 'GET' }); }); it('handles whitespace and newlines in chain', () => { const content = `export const POST = withRateLimit(\n withAuth(\n async (req) => {}\n )\n);`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withRateLimit', 'withAuth'], method: 'POST', }); }); it('handles DELETE method', () => { const content = `export const DELETE = withAuth(withAdmin(handler));`; expect(extractMiddlewareChain(content)).toEqual({ chain: ['withAuth', 'withAdmin'], method: 'DELETE', }); }); it('returns undefined when no export pattern exists', () => { const content = `const handler = withAuth(doSomething);`; expect(extractMiddlewareChain(content)).toBeUndefined(); }); }); // --------------------------------------------------------------------------- // Error response shape detection (detectStatusCode + separation logic) // --------------------------------------------------------------------------- describe('detectStatusCode', () => { it('detects Express .status(N).json() pattern', () => { const content = 'return res.status(400).json({ error: "bad request" })'; const jsonPos = content.indexOf('.json'); expect(detectStatusCode(content, jsonPos, -1)).toBe(400); }); it('detects NextResponse.json({...}, { status: 400 }) pattern', () => { const content = 'return NextResponse.json({ error: "not found" }, { status: 404 })'; const jsonPos = content.indexOf('.json'); // Simulate closingBracePos at the end of first arg object const firstArgClose = content.indexOf('}'); expect(detectStatusCode(content, jsonPos, firstArgClose)).toBe(404); }); it('detects NextResponse.json({...}, { status: 200 }) as success', () => { const content = 'return NextResponse.json({ data: results }, { status: 200 })'; const jsonPos = content.indexOf('.json'); const firstArgClose = content.indexOf('}'); expect(detectStatusCode(content, jsonPos, firstArgClose)).toBe(200); }); it('returns undefined when no status code present', () => { const content = 'return NextResponse.json({ data: results })'; const jsonPos = content.indexOf('.json'); const firstArgClose = content.indexOf('}'); expect(detectStatusCode(content, jsonPos, firstArgClose)).toBeUndefined(); }); it('detects .status(500).json() for server error', () => { const content = 'res.status(500).json({ error: "Internal server error", code: "SERVER_ERROR" })'; const jsonPos = content.indexOf('.json'); expect(detectStatusCode(content, jsonPos, -1)).toBe(500); }); it('detects status in second arg with extra properties', () => { const content = 'return NextResponse.json({ error: "fail" }, { status: 422, headers: {} })'; const jsonPos = content.indexOf('.json'); const firstArgClose = content.indexOf('}'); expect(detectStatusCode(content, jsonPos, firstArgClose)).toBe(422); }); }); describe('error response shape separation', () => { it('separates success and error responses in NextResponse pattern', () => { const content = ` export async function GET() { try { const data = await fetchData(); return NextResponse.json({ data, pagination: { page: 1 } }); } catch (e) { return NextResponse.json({ error: "Failed", message: e.message }, { status: 500 }); } } `; const shapes = extractResponseShapes(content); expect(shapes.responseKeys).toEqual(['data', 'pagination']); expect(shapes.errorKeys).toEqual(['error', 'message']); }); it('separates success and error responses in Express pattern', () => { const content = ` app.get('/api/users', (req, res) => { try { const users = getUsers(); res.json({ users, total }); } catch (e) { res.status(500).json({ error: "Server error", code: "INTERNAL" }); } }); `; const shapes = extractResponseShapes(content); expect(shapes.responseKeys).toEqual(['users', 'total']); expect(shapes.errorKeys).toEqual(['error', 'code']); }); it('handles multiple error status codes', () => { const content = ` export async function POST(req) { if (!req.body) return NextResponse.json({ error: "No body" }, { status: 400 }); if (!authorized) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ data, id }); } `; const shapes = extractResponseShapes(content); expect(shapes.responseKeys).toEqual(['data', 'id']); expect(shapes.errorKeys).toEqual(['error']); }); it('falls back to all keys as responseKeys when no status codes present', () => { const content = ` export async function GET() { return NextResponse.json({ data, count }); } `; const shapes = extractResponseShapes(content); expect(shapes.responseKeys).toEqual(['data', 'count']); expect(shapes.errorKeys).toBeUndefined(); }); it('treats status 200 as success', () => { const content = `return NextResponse.json({ ok }, { status: 200 })`; const shapes = extractResponseShapes(content); expect(shapes.responseKeys).toEqual(['ok']); expect(shapes.errorKeys).toBeUndefined(); }); it('treats status 201 as success', () => { const content = `return NextResponse.json({ created, id }, { status: 201 })`; const shapes = extractResponseShapes(content); expect(shapes.responseKeys).toEqual(['created', 'id']); expect(shapes.errorKeys).toBeUndefined(); }); }); // --------------------------------------------------------------------------- // Next.js project-level middleware config extraction // --------------------------------------------------------------------------- describe('extractNextjsMiddlewareConfig', () => { it('extracts matcher array and named function export', () => { const content = ` import { NextResponse, type NextRequest } from "next/server"; export function middleware(request: NextRequest) { return NextResponse.next(); } export const config = { matcher: ["/api/:path*", "/dashboard/:path*"], }; `; const result = extractNextjsMiddlewareConfig(content); expect(result).toBeDefined(); expect(result!.matchers).toEqual(['/api/:path*', '/dashboard/:path*']); expect(result!.exportedName).toBe('middleware'); }); it('extracts single string matcher', () => { const content = ` export function middleware(req) {} export const config = { matcher: '/dashboard/:path*' }; `; const result = extractNextjsMiddlewareConfig(content); expect(result!.matchers).toEqual(['/dashboard/:path*']); }); it('extracts default export name as wrappedFunction', () => { const content = ` import { auth } from '@/lib/auth'; export default auth; export const config = { matcher: ['/api/:path*'] }; `; const result = extractNextjsMiddlewareConfig(content); expect(result!.exportedName).toBe('auth'); expect(result!.wrappedFunctions).toContain('auth'); expect(result!.matchers).toEqual(['/api/:path*']); }); it('extracts chain composition', () => { const content = ` export default chain([withAuth, withI18n]); export const config = { matcher: ['/((?!api|_next).*)'] }; `; const result = extractNextjsMiddlewareConfig(content); expect(result!.wrappedFunctions).toEqual(['withAuth', 'withI18n']); }); it('extracts nested wrapper composition from default export', () => { const content = ` export default withRateLimit(withAuth(handler)); export const config = { matcher: ['/api/:path*'] }; `; const result = extractNextjsMiddlewareConfig(content); expect(result!.wrappedFunctions).toEqual(['withRateLimit', 'withAuth']); }); it('extracts regex-style negative lookahead matcher', () => { const content = ` export function middleware(req) {} export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\\\..*$).*)"], }; `; const result = extractNextjsMiddlewareConfig(content); expect(result!.matchers).toHaveLength(1); expect(result!.matchers[0]).toContain('(?!api|_next'); }); it('treats middleware without config.matcher as match-all', () => { const content = `export function middleware(req) { return NextResponse.next(); }`; const result = extractNextjsMiddlewareConfig(content); expect(result).toBeDefined(); expect(result!.matchers).toEqual([]); expect(result!.exportedName).toBe('middleware'); }); it('detects arrow function const export', () => { const content = ` export const middleware = (req) => { return NextResponse.next(); }; export const config = { matcher: ['/dashboard/:path*'] }; `; const result = extractNextjsMiddlewareConfig(content); expect(result).toBeDefined(); expect(result!.exportedName).toBe('middleware'); expect(result!.matchers).toEqual(['/dashboard/:path*']); }); it('handles export default function middleware(...)', () => { const content = `export default function middleware(req) { return NextResponse.next(); }`; const result = extractNextjsMiddlewareConfig(content); expect(result).toBeDefined(); expect(result!.exportedName).toBe('middleware'); }); }); // --------------------------------------------------------------------------- // Middleware matcher to route matching // --------------------------------------------------------------------------- describe('middlewareMatcherMatchesRoute', () => { it('matches prefix pattern with :path*', () => { expect(middlewareMatcherMatchesRoute('/api/:path*', '/api/users')).toBe(true); expect(middlewareMatcherMatchesRoute('/api/:path*', '/api/auth/login')).toBe(true); expect(middlewareMatcherMatchesRoute('/api/:path*', '/api')).toBe(true); }); it('does not match unrelated routes with :path*', () => { expect(middlewareMatcherMatchesRoute('/dashboard/:path*', '/api/users')).toBe(false); expect(middlewareMatcherMatchesRoute('/api/:path*', '/v1/users')).toBe(false); }); it('matches exact route', () => { expect(middlewareMatcherMatchesRoute('/login', '/login')).toBe(true); expect(middlewareMatcherMatchesRoute('/login', '/login/callback')).toBe(false); }); it('matches regex-style negative lookahead pattern', () => { const matcher = '/((?!api|_next).*)'; expect(middlewareMatcherMatchesRoute(matcher, '/dashboard')).toBe(true); expect(middlewareMatcherMatchesRoute(matcher, '/settings/profile')).toBe(true); expect(middlewareMatcherMatchesRoute(matcher, '/api')).toBe(false); expect(middlewareMatcherMatchesRoute(matcher, '/api/users')).toBe(false); }); }); describe('PHP response shape extraction', () => { it('extracts keys from short array syntax', () => { const content = `echo json_encode(['success' => true, 'data' => $items], JSON_UNESCAPED_UNICODE);`; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['success', 'data']); expect(shapes.errorKeys).toBeUndefined(); }); it('extracts keys from long array syntax', () => { const content = `echo json_encode(array('ok' => true, 'count' => $n));`; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['ok', 'count']); }); it('classifies error responses by http_response_code', () => { const content = ` http_response_code(401); echo json_encode(['error' => 'Unauthorized'], JSON_UNESCAPED_UNICODE); `; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toBeUndefined(); expect(shapes.errorKeys).toEqual(['error']); }); it('separates success and error responses', () => { const content = ` header('Content-Type: application/json'); if (!is_logged_in()) { http_response_code(401); echo json_encode(['error' => 'Not logged in'], JSON_UNESCAPED_UNICODE); exit; } echo json_encode(['ok' => true, 'new_status' => $status], JSON_UNESCAPED_UNICODE); `; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['ok', 'new_status']); expect(shapes.errorKeys).toEqual(['error']); }); it('handles multiple error status codes with exit boundaries', () => { const content = ` if (!$user) { http_response_code(401); echo json_encode(['error' => 'Unauthorized'], JSON_UNESCAPED_UNICODE); exit; } if (!$valid) { http_response_code(400); echo json_encode(['error' => 'Invalid data', 'field' => 'name'], JSON_UNESCAPED_UNICODE); exit; } echo json_encode(['ok' => true, 'id' => $id], JSON_UNESCAPED_UNICODE); `; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['ok', 'id']); expect(shapes.errorKeys).toEqual(['error', 'field']); }); it('detects status from header() pattern', () => { const content = ` header('HTTP/1.1 403 Forbidden'); echo json_encode(['error' => 'Forbidden', 'message' => 'Access denied']); `; const shapes = extractPHPResponseShapes(content); expect(shapes.errorKeys).toEqual(['error', 'message']); }); it('skips json_encode with variable argument', () => { const content = `echo json_encode($data, JSON_UNESCAPED_UNICODE);`; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toBeUndefined(); expect(shapes.errorKeys).toBeUndefined(); }); it('extracts only top-level keys from nested arrays', () => { const content = `echo json_encode(['data' => ['nested' => true], 'total' => $count]);`; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['data', 'total']); }); it('handles json_encode with flags after array', () => { const content = `echo json_encode(['export' => $data], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);`; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['export']); }); it('recognizes exit(N) as a boundary', () => { const content = ` http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit(0); echo json_encode(['ok' => true, 'data' => $result]); `; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['ok', 'data']); expect(shapes.errorKeys).toEqual(['error']); }); it('recognizes die("msg") as a boundary', () => { const content = ` http_response_code(500); echo json_encode(['error' => 'DB error']); die('Fatal'); echo json_encode(['items' => $list]); `; const shapes = extractPHPResponseShapes(content); expect(shapes.responseKeys).toEqual(['items']); expect(shapes.errorKeys).toEqual(['error']); }); it('detects CGI Status header format', () => { const content = ` header('Status: 404 Not Found'); echo json_encode(['error' => 'Not found', 'code' => 'MISSING']); `; const shapes = extractPHPResponseShapes(content); expect(shapes.errorKeys).toEqual(['error', 'code']); expect(shapes.responseKeys).toBeUndefined(); }); });