核心是确认「改配置而不弄乱文件」这件事成立:新增一路入站之后,其余 52 行逐字节不变。 搬进来的改写引擎用默认输出参数时会重排整个文件——行尾注释的对齐空格消失、流式数组被加上内边距—— 连根本没改动的段落也一起波及。不修的话「改一处只动一处」这个承诺直接落空,所以关掉了它的自动排版, 并在写回之后补了一步逐行还原。
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
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)
|
|
})
|
|
})
|