1
0

spring-route-callers.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /** Qualified CLI lookup must not hide a correctly linked Spring route (#1442). */
  2. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  3. import { spawnSync } from 'child_process';
  4. import * as fs from 'fs';
  5. import * as os from 'os';
  6. import * as path from 'path';
  7. import { CodeGraph } from '../src';
  8. const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
  9. const CONTROLLERS = ['StartFlowController', 'StartFlowExcelCdController'];
  10. const ROUTE = 'POST /excelStart/startFlowExcelCd';
  11. const JAVA_DIR = 'src/main/java/com/ideal/devops/controller';
  12. let projectRoot: string;
  13. let cg: CodeGraph;
  14. function runCli(command: string, symbol: string, args: string[] = []) {
  15. const result = spawnSync(process.execPath, [BIN, command, symbol, '--path', projectRoot, ...args], {
  16. encoding: 'utf-8',
  17. env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' },
  18. timeout: 30_000,
  19. });
  20. expect(result.status, result.stderr).toBe(0);
  21. return result.stdout;
  22. }
  23. beforeAll(async () => {
  24. projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-spring-callers-1442-'));
  25. const javaDir = path.join(projectRoot, JAVA_DIR);
  26. fs.mkdirSync(javaDir, { recursive: true });
  27. fs.writeFileSync(path.join(projectRoot, 'pom.xml'),
  28. '<project><dependencies><dependency><groupId>org.springframework.boot</groupId>' +
  29. '<artifactId>spring-boot-starter-web</artifactId></dependency></dependencies></project>');
  30. for (const controller of CONTROLLERS) {
  31. fs.writeFileSync(path.join(javaDir, `${controller}.java`), `package com.ideal.devops.controller;
  32. import org.springframework.web.bind.annotation.*;
  33. @RestController
  34. @RequestMapping("/excelStart")
  35. public class ${controller} {
  36. private ExecutePlanFillService executePlanFillService;
  37. ${controller === 'StartFlowExcelCdController' ? 'public void startFlowExcelCdPreview() {}' : ''}
  38. /** @param excelFlowStartDTO 启动参数 */
  39. @PostMapping("startFlowExcelCd")
  40. public R<Void> startFlowExcelCd(@Validated(Insert.class) @RequestBody ExcelFlowStartDto excelFlowStartDTO) {
  41. executePlanFillService.fillExecPlanModule();
  42. return null;
  43. }
  44. }
  45. `);
  46. }
  47. fs.writeFileSync(path.join(javaDir, 'ExecutePlanFillService.java'), `package com.ideal.devops.controller;
  48. public class ExecutePlanFillService {
  49. public void fillExecPlanModule() { fillModuleList(); }
  50. public void fillModuleList() {}
  51. }
  52. `);
  53. cg = await CodeGraph.init(projectRoot, { index: true });
  54. }, 30_000);
  55. afterAll(() => {
  56. cg?.close();
  57. if (projectRoot) fs.rmSync(projectRoot, { recursive: true, force: true });
  58. });
  59. describe('Spring route callers (#1442)', () => {
  60. it('keeps both route-to-handler edges and both routes in downstream impact', () => {
  61. const methods = cg.getNodesByName('startFlowExcelCd');
  62. expect(methods).toHaveLength(2);
  63. for (const method of methods) {
  64. const callers = cg.getCallers(method.id);
  65. expect(callers.map(c => [c.node.name, c.node.filePath])).toEqual([[ROUTE, method.filePath]]);
  66. expect(cg.getCallees(callers[0].node.id).map(c => c.node.id)).toEqual([method.id]);
  67. }
  68. const impact = JSON.parse(runCli('impact', 'fillModuleList', ['-d', '5', '--json']));
  69. for (const controller of CONTROLLERS) {
  70. const filePath = `${JAVA_DIR}/${controller}.java`;
  71. expect(impact.affected).toEqual(expect.arrayContaining([
  72. expect.objectContaining({ kind: 'method', name: 'startFlowExcelCd', filePath }),
  73. expect.objectContaining({ kind: 'route', name: ROUTE, filePath }),
  74. ]));
  75. }
  76. });
  77. it('has a higher-ranked prefix lookalike while node still selects the annotated handler', () => {
  78. // Before #1801, callers compared a qualified query to bare node.name, then
  79. // fell back to this FTS hit. Only the second controller has a lookalike:
  80. // its callers appeared empty even though node and impact found the route.
  81. const symbol = 'StartFlowExcelCdController.startFlowExcelCd';
  82. const hits = cg.searchNodes(symbol, { limit: 50 });
  83. expect(hits[0].node.name).toBe('startFlowExcelCdPreview');
  84. expect(cg.getCallers(hits[0].node.id)).toEqual([]);
  85. expect(hits.some(h => h.node.name === 'startFlowExcelCd')).toBe(true);
  86. const node = runCli('node', symbol);
  87. expect(node).toContain('@PostMapping("startFlowExcelCd")');
  88. expect(node).toContain('public R<Void> startFlowExcelCd(');
  89. expect(node).toContain(`${JAVA_DIR}/StartFlowExcelCdController.java`);
  90. expect(node).not.toContain('public void startFlowExcelCdPreview()');
  91. });
  92. it.each(CONTROLLERS)('callers %s.startFlowExcelCd returns its own route', (controller) => {
  93. const symbol = `${controller}.startFlowExcelCd`;
  94. const out = JSON.parse(runCli('callers', symbol, ['--json']));
  95. expect(out.callers).toEqual([
  96. expect.objectContaining({ kind: 'route', name: ROUTE, filePath: `${JAVA_DIR}/${controller}.java` }),
  97. ]);
  98. const text = runCli('callers', symbol);
  99. expect(text).toContain(ROUTE);
  100. expect(text).toContain(`${JAVA_DIR}/${controller}.java`);
  101. expect(text).not.toContain('No callers found');
  102. });
  103. });