搭起项目骨架,验证配置文件往返

核心是确认「改配置而不弄乱文件」这件事成立:新增一路入站之后,其余 52 行逐字节不变。

搬进来的改写引擎用默认输出参数时会重排整个文件——行尾注释的对齐空格消失、流式数组被加上内边距——
连根本没改动的段落也一起波及。不修的话「改一处只动一处」这个承诺直接落空,所以关掉了它的自动排版,
并在写回之后补了一步逐行还原。
This commit is contained in:
im0o
2026-08-10 02:43:04 +08:00
commit f7db96fb0e
13 changed files with 1906 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'bun:test'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { openDocument, replaceDocumentData } from '../vendor/config-editor/index.ts'
import type { ConfigObject } from '../vendor/config-editor/index.ts'
import { restoreInlineCommentAlignment } from './yaml-cosmetics.ts'
const fixture = readFileSync(
join(import.meta.dir, '__fixtures__', 'config-with-comments.yaml'),
'utf8',
)
const clone = (v: unknown) => JSON.parse(JSON.stringify(v)) as ConfigObject
/** 走一遍真实的写回链路:解析 → 改 → 写回 → 修复格式。 */
function roundtrip(source: string, mutate: (draft: ConfigObject) => void): string {
const doc = openDocument(source)
const draft = clone(doc.data)
mutate(draft)
return restoreInlineCommentAlignment(source, replaceDocumentData(doc, draft).yaml)
}
describe('写回一份带注释的配置', () => {
it('原样写回时逐字节不变', () => {
expect(roundtrip(fixture, () => {})).toBe(fixture)
})
it('新增一个此前不存在的段落时,其余每一行都不变', () => {
const out = roundtrip(fixture, (draft) => {
draft.listeners = [{ name: '容器A', type: 'mixed', port: 7778, proxy: 'AUTO' }]
})
// 这是这个项目最核心的操作,也是取信于用户的底线:只应多出新段落,别的一行都不许动。
const before = fixture.split('\n')
const after = out.split('\n')
expect(after.slice(0, before.length - 1)).toEqual(before.slice(0, before.length - 1))
expect(out).toContain('listeners:')
expect(out).toContain('port: 7778')
})
it('改动某个段落时,不碰其它段落的注释和写法', () => {
const out = roundtrip(fixture, (draft) => {
const rules = draft.rules as string[]
rules[rules.length - 1] = 'MATCH,AUTO'
})
// rules 之外的段落必须原封不动,含对齐空格和流式数组。
expect(out).toContain('mixed-port: 7777 # HTTP 和 SOCKS5 共用这一个端口')
expect(out).toContain(' - name: PROXY # 总开关,手动选;默认跟着 AUTO 走')
expect(out).toContain(' proxies: [AUTO, DIRECT]')
expect(out).toContain(' interval: 86400 # 每 24 小时自动拉取一次订阅')
expect(out).toContain('MATCH,AUTO')
})
it('被改动的段落内部注释会丢失,这是已知缺陷(见 ADR 0002)', () => {
const out = roundtrip(fixture, (draft) => {
const rules = draft.rules as string[]
rules[rules.length - 1] = 'MATCH,AUTO'
})
// 记录现状而非期望。将来若实现了段落内的就地修改,这条断言应当反转。
expect(out).not.toContain('# 本机和内网地址一律直连')
})
})
describe('restoreInlineCommentAlignment', () => {
it('内容与注释都相同时还原原本的对齐', () => {
const source = 'a: 1 # 说明\n'
const output = 'a: 1 # 说明\n'
expect(restoreInlineCommentAlignment(source, output)).toBe(source)
})
it('注释文字变了就不还原,避免掩盖真实改动', () => {
const source = 'a: 1 # 旧说明\n'
const output = 'a: 1 # 新说明\n'
expect(restoreInlineCommentAlignment(source, output)).toBe(output)
})
it('值变了就不还原', () => {
const source = 'a: 1 # 说明\n'
const output = 'a: 2 # 说明\n'
expect(restoreInlineCommentAlignment(source, output)).toBe(output)
})
it('相同的行出现多次时按顺序依次配对', () => {
const source = 'a: 1 # 同\nb: 2\na: 1 # 同\n'
const output = 'a: 1 # 同\nb: 2\na: 1 # 同\n'
expect(restoreInlineCommentAlignment(source, output)).toBe(source)
})
it('不把字符串里的井号当成注释', () => {
const line = 'url: "http://example.com/a#b"\n'
expect(restoreInlineCommentAlignment(line, line)).toBe(line)
})
})