✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下
This commit is contained in:
1 parent
badc722640
commit
fb53bb727c
34 files changed
+2726
-43
No files matched your search
@@ -0,0 +1,117 @@
|
||||
import type { QaEntry, QaMap } from './qaMap'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { dropEntries, moveEntries, readPathMap, writePathMap } from '../pathMap'
|
||||
import { QA_FILE, withEntry, withoutEntry } from './qaMap'
|
||||
|
||||
function entry(id: string, createdAt: string, question = `问 ${id}`): QaEntry {
|
||||
return { id, question, answer: `答 ${id}`, createdAt, sessionId: `s-${id}` }
|
||||
}
|
||||
|
||||
const DISPLAY = 'class ReferralRelation > function display'
|
||||
const IF = `${DISPLAY} > if @3fa1c2d0`
|
||||
|
||||
let root: string
|
||||
const qaFile = (): string => path.join(root, '.spx', 'code-qa.json')
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await fsp.mkdtemp(path.join(os.tmpdir(), 'code-qa-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await fsp.rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function writeRaw(content: unknown): Promise<void> {
|
||||
await fsp.mkdir(path.dirname(qaFile()), { recursive: true })
|
||||
await fsp.writeFile(qaFile(), typeof content === 'string' ? content : JSON.stringify(content), 'utf8')
|
||||
}
|
||||
|
||||
describe('记一次、删一次', () => {
|
||||
it('记在文件的那块下面:块按记号排,问答按时间排', () => {
|
||||
let map: QaMap = {}
|
||||
map = withEntry(map, 'app/models.py', IF, entry('b', '2026-09-26T10:00:00.000Z'))
|
||||
map = withEntry(map, 'app/models.py', DISPLAY, entry('c', '2026-09-26T11:00:00.000Z'))
|
||||
map = withEntry(map, 'app/models.py', IF, entry('a', '2026-09-25T10:00:00.000Z'))
|
||||
expect(map).toEqual({
|
||||
'app/models.py': [
|
||||
{ anchor: DISPLAY, entries: [entry('c', '2026-09-26T11:00:00.000Z')] },
|
||||
{ anchor: IF, entries: [entry('a', '2026-09-25T10:00:00.000Z'), entry('b', '2026-09-26T10:00:00.000Z')] },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('删一次只删它;块下面删光了这块不挂了,文件里删光了这份文件不记了;不改传进来的那份', () => {
|
||||
const map = withEntry(withEntry(withEntry({}, 'a.py', IF, entry('a', '2026-09-25T10:00:00.000Z')), 'a.py', IF, entry('b', '2026-09-26T10:00:00.000Z')), 'a.py', DISPLAY, entry('c', '2026-09-26T11:00:00.000Z'))
|
||||
const once = withoutEntry(map, 'a.py', IF, 'a')
|
||||
expect(once['a.py'].map(thread => [thread.anchor, thread.entries.map(item => item.id)])).toEqual([[DISPLAY, ['c']], [IF, ['b']]])
|
||||
const twice = withoutEntry(once, 'a.py', IF, 'b')
|
||||
expect(twice['a.py'].map(thread => thread.anchor)).toEqual([DISPLAY])
|
||||
expect(withoutEntry(twice, 'a.py', DISPLAY, 'c')).toEqual({})
|
||||
expect(map['a.py'].flatMap(thread => thread.entries)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('删不存在的那条不动', () => {
|
||||
const map = withEntry({}, 'a.py', IF, entry('a', '2026-09-25T10:00:00.000Z'))
|
||||
expect(withoutEntry(map, 'a.py', IF, 'nope')).toEqual(map)
|
||||
expect(withoutEntry(map, 'b.py', IF, 'a')).toEqual(map)
|
||||
})
|
||||
|
||||
it('文件改名、挪目录、删除时问答跟着走(同文件注释的规矩)', () => {
|
||||
const map = withEntry(withEntry({}, 'app/models.py', IF, entry('a', '2026-09-25T10:00:00.000Z')), 'app/views.py', DISPLAY, entry('b', '2026-09-25T10:00:00.000Z'))
|
||||
expect(Object.keys(moveEntries(map, 'app/models.py', 'app/orm.py'))).toEqual(['app/views.py', 'app/orm.py'])
|
||||
expect(Object.keys(moveEntries(map, 'app', 'backend/app')).sort()).toEqual(['backend/app/models.py', 'backend/app/views.py'])
|
||||
expect(Object.keys(dropEntries(map, 'app/models.py'))).toEqual(['app/views.py'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('读写 .spx/code-qa.json', () => {
|
||||
it('文件不存在、解析失败、不是对象都当没有', async () => {
|
||||
expect(await readPathMap(root, QA_FILE)).toEqual({})
|
||||
await writeRaw('{ "a.py": [')
|
||||
expect(await readPathMap(root, QA_FILE)).toEqual({})
|
||||
await writeRaw('[]')
|
||||
expect(await readPathMap(root, QA_FILE)).toEqual({})
|
||||
})
|
||||
|
||||
it('跳过认不出的块和问答:没有问题 / 回答、时间不对、没有记号、没有问答的块、不是数组的文件', async () => {
|
||||
await writeRaw({
|
||||
'a.py': [
|
||||
{ anchor: IF, entries: [entry('ok', '2026-09-25T10:00:00.000Z'), { id: 'x', question: '', answer: 'A', createdAt: '2026-09-25T10:00:00.000Z' }, { ...entry('bad', 'not a date') }] },
|
||||
{ anchor: '', entries: [entry('no-anchor', '2026-09-25T10:00:00.000Z')] },
|
||||
{ anchor: DISPLAY, entries: [] },
|
||||
'junk',
|
||||
],
|
||||
'b.py': { anchor: IF },
|
||||
'c.py': [{ anchor: IF, entries: [{ id: 'y', question: 'Q', answer: 'A', createdAt: '2026-09-25T10:00:00.000Z' }] }],
|
||||
'../outside.py': [{ anchor: IF, entries: [entry('z', '2026-09-25T10:00:00.000Z')] }],
|
||||
})
|
||||
expect(await readPathMap(root, QA_FILE)).toEqual({
|
||||
'a.py': [{ anchor: IF, entries: [entry('ok', '2026-09-25T10:00:00.000Z')] }],
|
||||
// 没有会话 id 的照样认,追问时重新开会话
|
||||
'c.py': [{ anchor: IF, entries: [{ id: 'y', question: 'Q', answer: 'A', createdAt: '2026-09-25T10:00:00.000Z', sessionId: '' }] }],
|
||||
})
|
||||
})
|
||||
|
||||
it('手改过的顺序、重复的块读进来整理好', async () => {
|
||||
await writeRaw({ 'a.py': [
|
||||
{ anchor: IF, entries: [entry('b', '2026-09-26T10:00:00.000Z')] },
|
||||
{ anchor: DISPLAY, entries: [entry('c', '2026-09-26T11:00:00.000Z')] },
|
||||
{ anchor: IF, entries: [entry('a', '2026-09-25T10:00:00.000Z')] },
|
||||
] })
|
||||
expect(await readPathMap(root, QA_FILE)).toEqual(withEntry(withEntry(withEntry({}, 'a.py', IF, entry('b', '2026-09-26T10:00:00.000Z')), 'a.py', DISPLAY, entry('c', '2026-09-26T11:00:00.000Z')), 'a.py', IF, entry('a', '2026-09-25T10:00:00.000Z')))
|
||||
})
|
||||
|
||||
it('写:建 .spx、文件按路径排、两格缩进、末尾换行,读得回来;删光留 {}', async () => {
|
||||
const map = withEntry(withEntry({}, 'z.py', IF, entry('a', '2026-09-25T10:00:00.000Z', '含 "引号" 和 `代码`')), 'a.py', DISPLAY, entry('b', '2026-09-25T10:00:00.000Z'))
|
||||
await writePathMap(root, QA_FILE, map)
|
||||
const raw = await fsp.readFile(qaFile(), 'utf8')
|
||||
expect(raw).toBe(`${JSON.stringify({ 'a.py': map['a.py'], 'z.py': map['z.py'] }, null, 2)}\n`)
|
||||
expect(raw.indexOf('"id"')).toBeLessThan(raw.indexOf('"question"'))
|
||||
expect(await readPathMap(root, QA_FILE)).toEqual(map)
|
||||
await writePathMap(root, QA_FILE, {})
|
||||
expect(await fsp.readFile(qaFile(), 'utf8')).toBe('{}\n')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user