搭起项目骨架,验证配置文件往返
核心是确认「改配置而不弄乱文件」这件事成立:新增一路入站之后,其余 52 行逐字节不变。 搬进来的改写引擎用默认输出参数时会重排整个文件——行尾注释的对齐空格消失、流式数组被加上内边距—— 连根本没改动的段落也一起波及。不修的话「改一处只动一处」这个承诺直接落空,所以关掉了它的自动排版, 并在写回之后补了一步逐行还原。
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# 测试样本 — 仿照真实配置的形状,不含任何真实密钥或订阅地址
|
||||
# 关键特征:整行注释、带对齐空格的行尾注释、流式写法的数组
|
||||
|
||||
mixed-port: 7777 # HTTP 和 SOCKS5 共用这一个端口
|
||||
allow-lan: true # true = 同局域网的其它设备也能连这个代理
|
||||
mode: rule
|
||||
|
||||
profile:
|
||||
store-selected: true # 记住你在面板里选的节点,重启不丢
|
||||
|
||||
external-controller: 127.0.0.1:9090
|
||||
secret: "占位密钥-不是真的"
|
||||
|
||||
proxy-providers:
|
||||
subs:
|
||||
type: http
|
||||
url: "http://127.0.0.1:3001/占位路径/download/示例" # 来源标记
|
||||
path: ./providers/subs.yaml
|
||||
interval: 86400 # 每 24 小时自动拉取一次订阅
|
||||
|
||||
proxy-groups:
|
||||
- name: PROXY # 总开关,手动选;默认跟着 AUTO 走
|
||||
type: select
|
||||
proxies: [AUTO, DIRECT]
|
||||
use: [subs]
|
||||
- name: AUTO # 全部节点里自动挑延迟最低的
|
||||
type: url-test
|
||||
use: [subs]
|
||||
interval: 300
|
||||
|
||||
rules:
|
||||
# 本机和内网地址一律直连,不然会绕出去再回来
|
||||
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
||||
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
||||
- MATCH,PROXY
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Binary file not shown.
Reference in New Issue
Block a user