import { describe, expect, it } from 'bun:test' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { openDocument } from '../vendor/config-editor/index.ts' import type { ConfigObject } from '../vendor/config-editor/index.ts' import { rewriteYaml } from '../server/config-store.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 draft = clone(openDocument(source).data) mutate(draft) return rewriteYaml(source, draft) } 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) }) })