Files
superwork/vscode/src/codeReader/diff/blockDiff.test.ts
T
cruldra 582ac4c987 ✨ feat(vscode): 代码阅读的注释挂到它讲的代码块上成「注释」一行,重写注释改用技能一句话加结构化结果
- 普通注释不再单独成卡片:同一层挂到后面最近的块(隔空行也算),后面没有挂到前面那块,整层没代码挂到父块;只有文件开头隔着空行的注释、模块 docstring、//!、@file 单独成「文件开头的说明」
- 各语言(Python、TypeScript、Rust、Shell、Dockerfile)建好树后统一调 attachComments;块的编辑范围连着挂上的注释,差异、跳转、钉住卡片都按它
- 宿主统一给每张卡片(语言卡片、翻译插件的属性表、库规则卡片、一行译文的语句卡片)插「注释」一行,排在名称、说明后面
- TypeScript 的 JSDoc 也能当常量、变量的说明;sqlx、React 不再单独处理前面的注释
- 「重写注释」每块都有:提示只发 /ecc:writing-code-comments 路径:起-止,模型只读、按 --json-schema 交 body,没交结构化正文就失败不写;扩展换掉块前面的注释或在块上面新写一段
2026-10-08 07:51:21 +08:00

249 lines
10 KiB
TypeScript

import type { BlockView } from '../blockView'
import type { SourcedBlocks } from './blockDiff'
import { describe, expect, it } from 'vitest'
import { readBlockViews } from '../blockView'
import { createPythonAdapter } from '../languages/python'
import { runtime, WASM_DIR } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { alignSiblings, diffBlockTrees, similarity } from './blockDiff'
const py = createPythonAdapter(WASM_DIR)
async function python(source: string): Promise<SourcedBlocks> {
const { blocks } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source)
return { blocks, source }
}
/** 手写的一块:只填比对用得到的几样。 */
function view(anchor: string, kind: string, code: string, extra: Partial<BlockView> = {}): BlockView {
return {
id: anchor,
anchor,
kind,
startLine: 0,
endLine: 0,
headerEndLine: 0,
code,
codeTokens: [],
attributes: { kind },
children: [],
translation: [{ text: code, role: 'plain' }],
...extra,
} as BlockView
}
/** 名字写在源码开头:defines 指向它。 */
function named(anchor: string, kind: string, name: string, code: string, source: { text: string }): BlockView {
const startIndex = source.text.length
source.text += name
return view(anchor, kind, code, { defines: { startIndex, endIndex: startIndex + name.length } })
}
const RELEASE_LUA = `_RELEASE_LUA = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
"""`
const BEFORE = `"""Redis 互斥锁: SET NX EX 抢锁, Lua 比对 token 再删."""
from __future__ import annotations
import uuid
from typing import Any
${RELEASE_LUA}
async def try_acquire(redis: Any, key: str, ttl_sec: int) -> bool:
"""抢锁; 抢到返回持有者 token, 已被占用返回 None."""
token = uuid.uuid4().hex
ok = await redis.set(key, token, nx=True, ex=ttl_sec)
return token if ok else None
async def _is_owner(redis: Any, key: str, token: str) -> bool:
"""锁仍归 token 时返回真."""
return await redis.get(key) == token
async def release(redis: Any, key: str, token: str) -> bool:
"""只在锁仍归 token 时删掉; 返回是否真的删了."""
result = await redis.eval(_RELEASE_LUA, 1, key)
return int(result) == 1
`
const AFTER = `"""Redis 互斥锁: SET NX EX 抢锁, Lua 比对 token 再续期 / 再删."""
from __future__ import annotations
import uuid
from typing import Any
${RELEASE_LUA}
_RENEW_LUA = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('EXPIRE', KEYS[1], ARGV[2])
else
return 0
end
"""
async def try_acquire(redis: Any, key: str, ttl_sec: int, token_prefix: str = "") -> str | None:
"""抢锁; 抢到返回持有者 token, 已被占用返回 None."""
token = f"{token_prefix}{uuid.uuid4().hex}"
ok = await redis.set(key, token, nx=True, ex=ttl_sec)
return token if ok else None
async def renew(redis: Any, key: str, token: str, ttl_sec: int) -> bool:
"""只在锁仍归 token 时把 TTL 续回 ttl_sec; 返回是否续上."""
result = await redis.eval(_RENEW_LUA, 1, key, token, str(ttl_sec))
return int(result) == 1
async def release(redis: Any, key: str, token: str) -> bool:
"""只在锁仍归 token 时删掉; 返回是否真的删了."""
result = await redis.eval(_RELEASE_LUA, 1, key, token)
return int(result) == 1
`
describe('similarity', () => {
it('一样是 1,毫不相干是 0,改了一个词在中间', () => {
expect(similarity('a = 1', 'a = 1')).toBe(1)
expect(similarity('foo(bar)', 'x')).toBe(0)
const score = similarity('print(a, b)', 'print(a, c)')
expect(score).toBeGreaterThan(0.5)
expect(score).toBeLessThan(1)
})
})
describe('alignSiblings', () => {
it('有名字的按种类 + 名字配,挪了位置也配得上;改名算一删一增', () => {
const source = { text: '' }
const before = [named('f', 'function', 'f', 'def f(): pass', source), named('g', 'function', 'g', 'def g(): pass', source)]
const after = [named('g2', 'function', 'g', 'def g(): return 1', source), named('h', 'function', 'h', 'def f(): pass', source)]
const { toOld, toNew } = alignSiblings(before, source.text, after, source.text)
expect(toOld).toEqual([1, undefined])
expect(toNew).toEqual([undefined, 0])
})
it('同名的照先后配', () => {
const source = { text: '' }
const before = [named('a', 'function', 'f', 'one', source), named('b', 'function', 'f', 'two', source)]
const after = [named('c', 'function', 'f', 'one', source), named('d', 'function', 'f', 'two!', source)]
expect(alignSiblings(before, source.text, after, source.text).toOld).toEqual([0, 1])
})
it('种类不同的同名块不配', () => {
const source = { text: '' }
const before = [named('a', 'const', 'x', 'const x = 1', source)]
const after = [named('b', 'variable', 'x', 'let x = 1', source)]
expect(alignSiblings(before, source.text, after, source.text).toOld).toEqual([undefined])
})
it('没名字的:源码没变的按最长公共子序列配,中间的按相似度配成修改,不像的算一删一增', () => {
const before = [view('1', 'statement', 'print(a)'), view('2', 'statement', 'call(alpha, beta, gamma)'), view('3', 'statement', 'print(b)')]
const after = [
view('1', 'statement', 'print(a)'),
view('x', 'statement', 'totally_new()'),
view('2', 'statement', 'call(alpha, beta, delta)'),
view('3', 'statement', 'print(b)'),
]
expect(alignSiblings(before, '', after, '').toOld).toEqual([0, undefined, 1, 2])
})
it('没名字的块种类不同就不按相似度配', () => {
const before = [view('1', 'statement', 'call(alpha, beta)')]
const after = [view('2', 'return', 'call(alpha, beta, gamma)')]
expect(alignSiblings(before, '', after, '').toOld).toEqual([undefined])
})
it('有名字的块不按相似度配:改名算一删一增', () => {
const source = { text: '' }
const before = [named('a', 'function', 'old_name', 'def old_name(x): return x + 1', source)]
const after = [named('b', 'function', 'new_name', 'def new_name(x): return x + 1', source)]
expect(alignSiblings(before, source.text, after, source.text).toOld).toEqual([undefined])
})
})
describe('diffBlockTrees', () => {
it('原型的 mutex.py:新增 2 · 修改 3 · 删除 1,只数最外层', async () => {
const diff = diffBlockTrees(await python(BEFORE), await python(AFTER))
expect(diff.stats).toEqual({ add: 2, mod: 3, del: 1 })
})
it('新增的块连同子孙都算新增,只有最外面那块挂小标', async () => {
const after = await python(AFTER)
const diff = diffBlockTrees(await python(BEFORE), after)
const renew = after.blocks.find(block => block.anchor === 'function renew')!
expect(diff.blocks[renew.anchor]).toEqual({ change: 'add', outermost: true })
for (const child of renew.children)
expect(diff.blocks[child.anchor]).toEqual({ change: 'add' })
})
it('修改的块:自己的内容改了带标记,子块照样比;只有子块改了的父块不算自己改了', async () => {
const after = await python(AFTER)
const diff = diffBlockTrees(await python(BEFORE), after)
const tryAcquire = after.blocks.find(block => block.anchor === 'function try_acquire')!
expect(diff.blocks[tryAcquire.anchor]).toMatchObject({ change: 'mod', own: true })
expect(diff.blocks[tryAcquire.anchor].marks?.rows).toBeDefined()
const [token, ok, back] = tryAcquire.children
expect(diff.blocks[token.anchor]).toMatchObject({ change: 'mod', own: true })
expect(diff.blocks[ok.anchor]).toBeUndefined()
expect(diff.blocks[back.anchor]).toBeUndefined()
const release = after.blocks.find(block => block.anchor === 'function release')!
expect(diff.blocks[release.anchor]).toEqual({ change: 'mod' })
expect(diff.blocks[release.children[0].anchor]).toMatchObject({ change: 'mod', own: true })
})
it('删掉的块放回原来的位置:前面配上的兄弟之后,再越过紧跟着的新增块;只带它自己的内容', async () => {
const after = await python(AFTER)
const diff = diffBlockTrees(await python(BEFORE), after)
expect(diff.removed).toHaveLength(1)
const [removed] = diff.removed
expect(removed).toMatchObject({ parent: '', after: 'function renew' })
expect(removed.block.anchor).toBe('function _is_owner')
expect(removed.block.children).toEqual([])
expect(removed.block.defines).toBeUndefined()
const renew = after.blocks.find(block => block.anchor === 'function renew')!
expect(removed.line).toBe(renew.endLine + 1)
const names = JSON.stringify(removed.block)
expect(names).not.toContain('"offset"')
})
it('没变的文件没有改动', async () => {
const before = await python(BEFORE)
expect(diffBlockTrees(before, await python(BEFORE))).toEqual({ blocks: {}, removed: [], stats: { add: 0, mod: 0, del: 0 } })
})
it('子块里删掉的块挂在父块下面,排在最前面时没有 after', async () => {
const before = await python('def f():\n a = 1\n b = 2\n')
const after = await python('def f():\n b = 2\n')
const diff = diffBlockTrees(before, after)
expect(diff.stats).toEqual({ add: 0, mod: 1, del: 0 })
expect(diff.removed).toHaveLength(1)
expect(diff.removed[0]).toMatchObject({ parent: 'function f', line: 1 })
expect(diff.removed[0].after).toBeUndefined()
})
})
describe('改了挂在块上的注释', () => {
it('改的是讲这块的注释:这块算修改、自己改了,「注释」一行逐词标;一行译文的卡片标在它下面那几行(rows)上', async () => {
const before = await python('import os\n\n# 讲 load\ndef load(path):\n # 先检查\n return path\n')
const after = await python('import os\n\n# 讲 load,改过\ndef load(path):\n # 先检查一下\n return path\n')
const diff = diffBlockTrees(before, after)
const [, load] = after.blocks
const [returned] = load.children
expect(diff.stats).toEqual({ add: 0, mod: 1, del: 0 })
expect(diff.blocks[load.anchor]).toMatchObject({ change: 'mod', own: true, marks: { rows: { changed: { 1: { value: { markdown: expect.any(Array) } } } } } })
expect(diff.blocks[returned.anchor]).toMatchObject({ change: 'mod', own: true, marks: { rows: { changed: { 0: { value: { markdown: expect.any(Array) } } } } } })
expect(diff.blocks[returned.anchor].marks).not.toHaveProperty('translation')
})
})