✨ feat(vscode): 接口卡片的调用树认得出包了一层 fetch 的函数(authedFetch 这类),跨文件、多层、路径别名导入都算
This commit is contained in:
1 parent
78aa71ffd0
commit
cb561d23ad
11 files changed
+683
-64
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { INDEX_GRAMMARS, MemoryIndexHost } from '../testSupport'
|
||||
import { WorkspaceIndexes } from '../workspaceIndex'
|
||||
import { REQUESTS_INDEX } from './requests'
|
||||
import { frontendRequests } from './requests'
|
||||
|
||||
const STORE = [
|
||||
'export function useAgentStore() {',
|
||||
@@ -57,8 +57,9 @@ const AGENTS = [
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
async function requests(files: Record<string, string>, method: string): Promise<string[]> {
|
||||
const found = await new WorkspaceIndexes(new MemoryIndexHost(files, INDEX_GRAMMARS)).find(REQUESTS_INDEX, method)
|
||||
/** 方法是 method 的请求,一处一行:文件 调用者@名字位置 方法 网址 调用处。 */
|
||||
async function sent(indexes: WorkspaceIndexes, method: string): Promise<string[]> {
|
||||
const found = await frontendRequests((index, key) => indexes.find(index, key), method)
|
||||
return found.map(({ location, entry }) => {
|
||||
const file = location.uri.replace(`${MemoryIndexHost.FOLDER}/`, '')
|
||||
const call = `${entry.at.line}:${entry.at.character}`
|
||||
@@ -66,6 +67,10 @@ async function requests(files: Record<string, string>, method: string): Promise<
|
||||
})
|
||||
}
|
||||
|
||||
async function requests(files: Record<string, string>, method: string): Promise<string[]> {
|
||||
return sent(new WorkspaceIndexes(new MemoryIndexHost(files, INDEX_GRAMMARS)), method)
|
||||
}
|
||||
|
||||
describe('前端发请求的地方', () => {
|
||||
const files = { 'web/src/app/(main)/my-agents/page.tsx': PAGE, 'web/src/lib/agents.ts': AGENTS }
|
||||
|
||||
@@ -93,3 +98,185 @@ describe('前端发请求的地方', () => {
|
||||
expect(await requests(files, 'DELETE')).toEqual([`web/src/lib/agents.ts useRemoveAgent@9:16 DELETE /api/v1/agents/\${id} 10:41`])
|
||||
})
|
||||
})
|
||||
|
||||
/** 带注释、结尾逗号的 tsconfig,`@/` 指 web/src。 */
|
||||
const TSCONFIG = [
|
||||
'{',
|
||||
' "compilerOptions": {',
|
||||
' /* Bundler mode */',
|
||||
' "paths": {',
|
||||
' "@/*": ["./src/*"],',
|
||||
' },',
|
||||
' },',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** 照搬 luffy 的 authedFetch:调用处的选项展开进给 fetch 的第二个参数,401 时自己调自己重试。 */
|
||||
const AUTHED_FETCH = [
|
||||
'import { useAuthGate } from "@/lib/auth/auth-gate";',
|
||||
'',
|
||||
'export const authedFetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {',
|
||||
' const headers = new Headers(init?.headers);',
|
||||
' const res = await fetch(input, {',
|
||||
' credentials: "include",',
|
||||
' ...init,',
|
||||
' headers,',
|
||||
' });',
|
||||
' if (res.status !== 401) return res;',
|
||||
' headers.set("x-auth-retried", "1");',
|
||||
' return authedFetch(input, { ...init, headers });',
|
||||
'};',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const USE_IDE = [
|
||||
'import { useCallback } from "react";',
|
||||
'import { authedFetch } from "@/lib/api/authed-fetch";',
|
||||
'',
|
||||
'export function useIde(sessionId: string) {',
|
||||
' const start = useCallback(async () => {',
|
||||
' const res = await authedFetch(',
|
||||
` \`/api/v1/sessions/\${sessionId}/sandbox/ide/start\`,`,
|
||||
' { method: "POST" },',
|
||||
' );',
|
||||
' return res.ok;',
|
||||
' }, [sessionId]);',
|
||||
` const status = () => authedFetch(\`/api/v1/sessions/\${sessionId}/sandbox/ide\`)`,
|
||||
' const guessed = (init: RequestInit) => authedFetch("/api/v1/sessions", init)',
|
||||
' const items = () => authedFetch(itemsPath(sessionId), { method: "POST" })',
|
||||
' return { start, status, guessed, items };',
|
||||
'}',
|
||||
'',
|
||||
'void authedFetch("/api/v1/health")',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** 第二层:把网址转给 authedFetch(相对路径导入)。 */
|
||||
const HTTP = [
|
||||
'import { authedFetch } from "./api/authed-fetch";',
|
||||
'',
|
||||
'export function send(url: string, init?: RequestInit) {',
|
||||
' return authedFetch(url, { ...init, cache: "no-store" });',
|
||||
'}',
|
||||
'',
|
||||
'export const postJson = (url: string, body: unknown) =>',
|
||||
' authedFetch(url, { method: "POST", body: JSON.stringify(body) });',
|
||||
'',
|
||||
'export const putBoth = (url: string, init?: RequestInit) => authedFetch(url, { ...init, method: "PUT" });',
|
||||
'',
|
||||
'export const byMethod = (url: string, method: string) => fetch(url, { method });',
|
||||
'',
|
||||
`export const prefixed = (url: string) => { url = \`/api/v1\${url}\`; return fetch(url) };`,
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** 第三层:把网址转给 send,写死 DELETE。 */
|
||||
const REMOVE = [
|
||||
'import { send } from "@/lib/http";',
|
||||
'',
|
||||
'export const remove = (url: string) => send(url, { method: "DELETE" });',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const SAVE = [
|
||||
'import { byMethod, postJson, prefixed, putBoth, send } from "@/lib/http";',
|
||||
'import { remove } from "@/lib/agents";',
|
||||
'',
|
||||
'export async function save(id: string) {',
|
||||
` await send(\`/api/v1/agents/\${id}\`, { method: "PATCH" });`,
|
||||
' await send("/api/v1/agents");',
|
||||
' await postJson("/api/v1/agents", { name: "a" });',
|
||||
' await putBoth("/api/v1/agents/x", {});',
|
||||
' await byMethod("/api/v1/agents", "POST");',
|
||||
' await prefixed("/agents");',
|
||||
` await remove(\`/api/v1/agents/\${id}\`);`,
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** 本文件定义、没导出的包装函数,调用时带泛型实参。 */
|
||||
const REFERRAL = [
|
||||
'import { authedFetch } from "@/lib/api/authed-fetch";',
|
||||
'',
|
||||
'const getJson = async <T>(path: string): Promise<T> => {',
|
||||
' const res = await authedFetch(path, { method: "GET" });',
|
||||
' return res.json() as Promise<T>;',
|
||||
'};',
|
||||
'',
|
||||
'export const fetchCenter = () => getJson<{ code: string }>("/api/v1/referral/center");',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** 和包装函数同名、却不是包装函数的:本文件定义的、从包导入的、导入不到的、被参数挡住的。 */
|
||||
const SAME_NAMES = [
|
||||
'import { authedFetch as logged } from "some-lib";',
|
||||
'import { postJson } from "@/lib/http";',
|
||||
'import { send } from "@/lib/missing";',
|
||||
'',
|
||||
'const authedFetch = (input: string) => console.log(input);',
|
||||
'',
|
||||
'export function noise(postJson: (url: string) => void) {',
|
||||
' authedFetch("/api/v1/agents");',
|
||||
' logged("/api/v1/agents");',
|
||||
' send("/api/v1/agents");',
|
||||
' postJson("/api/v1/agents");',
|
||||
'}',
|
||||
'',
|
||||
'export const outside = () => postJson("/api/v1/agents", {});',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const CACHE = [
|
||||
'const getJson = (path: string) => localStorage.getItem(path);',
|
||||
'',
|
||||
'export const cached = () => getJson("/api/v1/referral/center");',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
describe('经包装函数发的请求', () => {
|
||||
const ide = { 'web/tsconfig.json': TSCONFIG, 'web/src/lib/api/authed-fetch.ts': AUTHED_FETCH, 'web/src/app/(main)/chat/code/use-ide.ts': USE_IDE }
|
||||
const IDE_POST = `web/src/app/(main)/chat/code/use-ide.ts useIde@3:16 POST /api/v1/sessions/\${sessionId}/sandbox/ide/start 5:22`
|
||||
|
||||
it('把收到的网址原样转给 fetch 的是包装函数:方法照 fetch 的规则从调用处的第二个参数读,网址跨行写也认,网址不是字面量的不认', async () => {
|
||||
expect(await requests(ide, 'POST')).toEqual([IDE_POST])
|
||||
expect(await requests(ide, 'GET')).toEqual([
|
||||
`web/src/app/(main)/chat/code/use-ide.ts status@11:8 GET /api/v1/sessions/\${sessionId}/sandbox/ide 11:23`,
|
||||
'web/src/app/(main)/chat/code/use-ide.ts (顶层)@17:5 GET /api/v1/health 17:5',
|
||||
])
|
||||
})
|
||||
|
||||
it('多层转手、跨文件(相对路径、路径别名导入)都认;写死方法的取写死的,方法定不下来的不认', async () => {
|
||||
const files = { ...ide, 'web/src/lib/http.ts': HTTP, 'web/src/lib/agents.ts': REMOVE, 'web/src/app/agents/page.tsx': SAVE }
|
||||
expect(await requests(files, 'PATCH')).toEqual([`web/src/app/agents/page.tsx save@3:22 PATCH /api/v1/agents/\${id} 4:8`])
|
||||
expect(await requests(files, 'DELETE')).toEqual([`web/src/app/agents/page.tsx save@3:22 DELETE /api/v1/agents/\${id} 10:8`])
|
||||
expect(await requests(files, 'POST')).toEqual([IDE_POST, 'web/src/app/agents/page.tsx save@3:22 POST /api/v1/agents 6:8'])
|
||||
expect((await requests(files, 'GET')).filter(line => line.startsWith('web/src/app/agents/'))).toEqual(['web/src/app/agents/page.tsx save@3:22 GET /api/v1/agents 5:8'])
|
||||
expect(await requests(files, 'PUT')).toEqual([])
|
||||
})
|
||||
|
||||
it('调用的名字按本文件的定义、import 来源、参数判定指的是哪个函数,判定不了的不认', async () => {
|
||||
const files = { ...ide, 'web/src/lib/http.ts': HTTP, 'web/src/lib/other.ts': SAME_NAMES, 'web/src/app/referral/api.ts': REFERRAL, 'web/src/app/cache/api.ts': CACHE }
|
||||
expect((await requests(files, 'GET')).filter(line => !line.includes('use-ide.ts'))).toEqual(['web/src/app/referral/api.ts fetchCenter@7:13 GET /api/v1/referral/center 7:33'])
|
||||
expect((await requests(files, 'POST')).filter(line => !line.includes('use-ide.ts'))).toEqual(['web/src/lib/other.ts outside@13:13 POST /api/v1/agents 13:29'])
|
||||
})
|
||||
|
||||
it('包装函数、路径别名改了,结果跟着变', async () => {
|
||||
const host = new MemoryIndexHost(ide, INDEX_GRAMMARS)
|
||||
const indexes = new WorkspaceIndexes(host)
|
||||
expect(await sent(indexes, 'POST')).toEqual([IDE_POST])
|
||||
host.emit('change', 'web/src/lib/api/authed-fetch.ts', 'export const authedFetch = async (input: string) => new Response(input)\n')
|
||||
expect(await sent(indexes, 'POST')).toEqual([])
|
||||
host.emit('change', 'web/src/lib/api/authed-fetch.ts', 'export const authedFetch = (input: string, init?: RequestInit) => fetch(input, { method: "PUT" })\n')
|
||||
expect(await sent(indexes, 'PUT')).toEqual([
|
||||
`web/src/app/(main)/chat/code/use-ide.ts useIde@3:16 PUT /api/v1/sessions/\${sessionId}/sandbox/ide/start 5:22`,
|
||||
`web/src/app/(main)/chat/code/use-ide.ts status@11:8 PUT /api/v1/sessions/\${sessionId}/sandbox/ide 11:23`,
|
||||
'web/src/app/(main)/chat/code/use-ide.ts guessed@12:8 PUT /api/v1/sessions 12:41',
|
||||
'web/src/app/(main)/chat/code/use-ide.ts (顶层)@17:5 PUT /api/v1/health 17:5',
|
||||
])
|
||||
host.emit('change', 'web/src/lib/api/authed-fetch.ts', AUTHED_FETCH)
|
||||
expect(await sent(indexes, 'POST')).toEqual([IDE_POST])
|
||||
host.emit('change', 'web/tsconfig.json', TSCONFIG.replace('./src/*', './lib/*'))
|
||||
expect(await sent(indexes, 'POST')).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user