顺序是刻意的。前端一行测试都没有,直接挪代码挪坏了 bun test 不会红,所以先把纯逻辑 抽到能被测试的位置并补上用例,最后才动组件。测试从 37 条增加到 77 条。 抽出来的四块: - shared/outbound-chain.ts 「这个端口现在走哪条线」的链路解析,带防环,9 条测试 - web/delay-test.ts 延迟测试失败时说什么,6 个分支原本一条测试都没有 - web/runtime.ts 运行时状态的模块级单例,照 prefs.ts 已有的模式 - web/components/InboundList.vue 入站列表 出站的文本原本散成四份(读一份、显示一份、写回一份、表单又抄一份),加一种出站模式 要同时改四处,漏一个就是显示与写回不一致。合并成 OUTBOUND_PRESETS 一张表。 给认证和透传白名单补了 20 条测试。两者都是安全边界、都是纯函数、都不需要内核在线, 之前却只靠需要活内核的冒烟脚本间接覆盖。其中「形似但不是健康检查的路径」几条确认了 正则没写松。 另外:配置快照不再把整份文件原文下发(前端从不读,白白让响应体积翻倍); yaml-cosmetics 的测试改走生产路径,之前它自己拼了一遍三步流程,落盘链路漏掉其中一步 测试照样会绿。
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
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)
|
|
})
|
|
})
|