import type { CallEdge, CallRange, CallSymbol, CallTreeNode } from './model' import { describe, expect, it } from 'vitest' import { buildCallTrees, nextPageUrl } from './model' function symbol(name: string, line = 0, extra: Partial = {}): CallSymbol { return { uri: `file:///${name}`, line, character: 0, name, kind: 'function', path: `${name}.py`, ...extra, } } function at(line: number, character = 0): CallRange { return { line, character, endLine: line, endCharacter: character + 4 } } function edge(caller: CallSymbol, callee: CallSymbol, line: number, character = 0): CallEdge { return { caller, callee, at: at(line, character) } } function currents(node: CallTreeNode): CallTreeNode[] { return [...(node.current ? [node] : []), ...node.children.flatMap(currents)] } describe('调用边收成调用树', () => { const mask = symbol('mask_phone', 27, { path: 'utils/phone.py' }) it('两个没有更上层调用者的根是两棵树,每棵记下当前块出现几次', () => { const read = symbol('读取用户资料', 0, { path: 'routes/user.py' }) const profile = symbol('get_profile', 10, { path: 'services/user.py' }) const serialize = symbol('serialize_user', 20, { path: 'services/user.py' }) const load = symbol('load_user', 30, { path: 'services/user.py' }) const exp = symbol('导出通讯录', 0, { path: 'routes/export.py' }) const trees = buildCallTrees([ edge(serialize, mask, 40), edge(load, mask, 50), edge(profile, serialize, 12), edge(profile, load, 14), edge(read, profile, 3), edge(exp, mask, 8), ], mask) expect(trees.map(tree => tree.name)).toEqual(['读取用户资料', '导出通讯录']) expect(trees.map(tree => tree.hits)).toEqual([2, 1]) expect(trees[0].root.detail).toBe('入口') expect(trees[0].root.where).toBe('routes/user.py:1') expect(currents(trees[0].root).map(node => node.where)).toEqual(['utils/phone.py:28', 'utils/phone.py:28']) expect(currents(trees[1].root)).toHaveLength(1) }) it('同一个调用者的两段不同范围是当前块的两次出现', () => { const caller = symbol('serialize_user', 20, { path: 'services/user.py' }) const root = symbol('读取用户资料', 0, { path: 'routes/user.py' }) const trees = buildCallTrees([ edge(caller, mask, 10), edge(caller, mask, 20), edge(root, caller, 3), ], mask) expect(trees).toHaveLength(1) expect(trees[0].hits).toBe(2) const parent = trees[0].root.children[0] expect(parent.name).toBe('serialize_user') expect(parent.children.map(node => node.current)).toEqual([true, true]) expect(parent.otherCalls).toBeUndefined() }) it('不在通向当前块的路径上的调用收成一行,不把那些符号画出来', () => { const caller = symbol('serialize_user', 20, { path: 'services/user.py' }) const root = symbol('读取用户资料', 0, { path: 'routes/user.py' }) const trees = buildCallTrees([ edge(caller, mask, 10), edge(caller, symbol('format_name'), 11), edge(caller, symbol('format_name'), 12, 2), edge(root, caller, 3), edge(root, symbol('sideways'), 4), edge(root, symbol('leftover'), 5), ], mask) expect(trees).toHaveLength(1) expect(trees[0].root.otherCalls).toBe(2) expect(trees[0].root.children).toHaveLength(1) expect(trees[0].root.children[0].otherCalls).toBe(2) expect(trees[0].root.children[0].children.map(node => node.name)).toEqual(['mask_phone']) const text = JSON.stringify(trees) expect(text).not.toContain('format_name') expect(text).not.toContain('sideways') expect(text).not.toContain('leftover') }) it('调用边绕回去就停,不把当前块再画到自己上面', () => { const caller = symbol('get_profile') const trees = buildCallTrees([ edge(caller, mask, 1), edge(mask, caller, 2), ], mask) expect(trees).toHaveLength(1) expect(trees[0].name).toBe('get_profile') expect(trees[0].root.children.map(node => node.name)).toEqual(['mask_phone']) expect(trees[0].root.children[0].current).toBe(true) expect(trees[0].root.children[0].children).toEqual([]) expect(trees[0].hits).toBe(1) }) it('往上最多 8 层,再高的调用者不进这棵树', () => { const above = Array.from({ length: 9 }, (_, index) => symbol(`n${index}`, index)) const edges = [edge(above[0], mask, 1)] for (let index = 1; index < above.length; index++) edges.push(edge(above[index], above[index - 1], index + 1)) const trees = buildCallTrees(edges, mask) expect(trees).toHaveLength(1) expect(trees[0].name).toBe('n7') expect(JSON.stringify(trees)).not.toContain('n8') }) it('重复的同一段调用范围只算一次', () => { const caller = symbol('serialize_user') const trees = buildCallTrees([ edge(caller, mask, 10), edge(caller, mask, 10), ], mask) expect(trees).toHaveLength(1) expect(trees[0].hits).toBe(1) }) it('同一次查询里种类变了仍是同一个符号,不拆成两棵', () => { const asFunction = symbol('get_profile', 4, { uri: 'file:///services/user.py', path: 'services/user.py', kind: 'function' }) const asClass = symbol('get_profile', 4, { uri: 'file:///services/user.py', path: 'services/user.py', kind: 'class' }) const trees = buildCallTrees([ edge(asFunction, mask, 8), edge(asClass, mask, 8), ], mask) expect(trees).toHaveLength(1) expect(trees[0].hits).toBe(1) expect(trees[0].root.kind).toBe('function') }) it('没有任何调用者就没有树', () => { expect(buildCallTrees([], mask)).toEqual([]) }) it('选中的是类时,直接调用它的那一层是「新建」,类自己标当前,不编造方法', () => { const order = symbol('OrderService', 13, { uri: 'file:///billing/order.py', path: 'billing/order.py', kind: 'class' }) const create = symbol('create_order', 20, { path: 'billing/order.py', detail: '创建一笔订单' }) const post = symbol('POST /orders', 0, { path: 'routes/order.py' }) const trees = buildCallTrees([ edge(create, order, 39), edge(post, create, 5), edge(create, symbol('charge'), 41), ], order, { classTarget: true }) expect(trees).toHaveLength(1) expect(trees[0].hits).toBe(1) const created = trees[0].root.children[0] expect(created.name).toBe('create_order') expect(created.detail).toBe('创建一笔订单') expect(created.otherCalls).toBe(1) const made = created.children[0] expect(made.name).toBe('新建 OrderService') expect(made.detail).toBe('进入这个类') expect(made.where).toBe('billing/order.py:40') expect(made.jump).toEqual({ uri: 'file:///create_order', line: 39, character: 0 }) expect(made.current).toBe(false) expect(made.kind).toBe('class') expect(made.children).toEqual([{ name: 'OrderService', kind: 'class', where: 'billing/order.py:14', jump: { uri: 'file:///billing/order.py', line: 13, character: 0 }, current: true, children: [], }]) expect(JSON.stringify(trees)).not.toContain('方法') }) }) describe('接口的调用树', () => { const route = symbol('get_default_agent', 78, { uri: 'file:///api/agent/routes.py', path: 'api/agent/routes.py' }) it('前端发请求的那一行单独成「请求」节点,点了跳到那一行;Next.js 页面入口第二行写页面网址', () => { const fetchDefault = symbol('fetchDefaultAgent', 9, { uri: 'file:///web/src/lib/agents.ts', path: 'web/src/lib/agents.ts' }) const page = symbol('MyAgentsPage', 4, { uri: 'file:///web/src/app/(main)/my-agents/page.tsx', path: 'web/src/app/(main)/my-agents/page.tsx' }) const sidebar = symbol('useSidebar', 3, { uri: 'file:///web/src/components/sidebar/useSidebar.ts', path: 'web/src/components/sidebar/useSidebar.ts' }) const trees = buildCallTrees([ { ...edge(fetchDefault, route, 10, 20), request: 'GET /api/v1/agents/default' }, edge(page, fetchDefault, 6), edge(sidebar, fetchDefault, 8), ], route) expect(trees.map(tree => [tree.name, tree.root.detail])).toEqual([['MyAgentsPage', '页面 /my-agents'], ['useSidebar', '入口']]) const request = trees[0].root.children[0].children[0] expect(request).toEqual({ name: '请求 GET /api/v1/agents/default', kind: 'request', where: 'web/src/lib/agents.ts:11', jump: { uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 }, current: false, children: [{ name: 'get_default_agent', kind: 'function', where: 'api/agent/routes.py:79', jump: { uri: 'file:///api/agent/routes.py', line: 78, character: 0 }, current: true, children: [], }], }) expect(trees[0].hits).toBe(1) expect(trees[0].root.jump).toEqual({ uri: 'file:///web/src/app/(main)/my-agents/page.tsx', line: 4, character: 0 }) }) it('在 Python 里直接调用路由函数的不加「请求」节点', () => { const job = symbol('nightly_job', 2, { path: 'jobs/nightly.py' }) const trees = buildCallTrees([edge(job, route, 5)], route) expect(trees[0].root.children.map(node => node.name)).toEqual(['get_default_agent']) }) }) describe('按 Next.js 的约定算页面网址', () => { it('按 app 目录下的目录算:去掉路由分组和并行路由,动态段原样留', () => { expect(nextPageUrl('web/src/app/(main)/my-agents/page.tsx')).toBe('/my-agents') expect(nextPageUrl('app/page.tsx')).toBe('/') expect(nextPageUrl('client/src/app/(main)/billing/orders/[orderId]/page.tsx')).toBe('/billing/orders/[orderId]') expect(nextPageUrl('app/docs/[[...section]]/page.js')).toBe('/docs/[[...section]]') expect(nextPageUrl('app/dashboard/@team/settings/page.jsx')).toBe('/dashboard/settings') expect(nextPageUrl('app/(marketing)/page.ts')).toBe('/') }) it('不是 app 目录下的 page 文件就不是页面', () => { expect(nextPageUrl('app/layout.tsx')).toBeUndefined() expect(nextPageUrl('pages/index.tsx')).toBeUndefined() expect(nextPageUrl('src/apps/page.tsx')).toBeUndefined() expect(nextPageUrl('app/page.mdx')).toBeUndefined() expect(nextPageUrl('page.tsx')).toBeUndefined() }) })