按审查结论重构:先补测试网,再搬代码

顺序是刻意的。前端一行测试都没有,直接挪代码挪坏了 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 的测试改走生产路径,之前它自己拼了一遍三步流程,落盘链路漏掉其中一步
测试照样会绿。
This commit is contained in:
im0o
2026-08-10 18:21:23 +08:00
parent 6b208847a2
commit c5ac59ecf9
19 changed files with 781 additions and 355 deletions
+7 -32
View File
@@ -8,8 +8,9 @@
*/
import { computed } from 'vue'
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
import { lastSwitchableGroup, resolveChain } from '@shared/outbound-chain.ts'
import { latestDelay } from '../api'
import type { ProxyInfo } from '../api'
import { kernel, providerLines, runtime } from '../runtime'
import OutboundCard from '../components/OutboundCard.vue'
import IconOffline from '~icons/ri/cloud-off-line'
import IconEdit from '~icons/ri/edit-line'
@@ -19,11 +20,8 @@ const props = defineProps<{
inbound: Inbound
groups: string[]
ownProxies: string[]
providerLines: string[]
runtime: Record<string, ProxyInfo>
/** 流量最终交给哪个出站。走规则的入站要查到规则表末尾的 MATCH */
destination: string | null
kernelOnline: boolean
/** 正在提交的那个出站的名字。只有它显示忙碌,其余照常能点 */
pendingOutbound: string | null
/** 正在测延迟的线路 */
@@ -42,39 +40,16 @@ const emit = defineEmits<{
const locked = computed(() => props.inbound.isDefault)
function describe(name: string): string {
const info = props.runtime[name]
const info = runtime.value[name]
if (!info) return ''
return info.now ? `${info.type}(${info.now})` : info.type
}
const delayOf = (name: string) => latestDelay(props.runtime[name])
const delayOf = (name: string) => latestDelay(runtime.value[name])
const isTesting = (name: string) => props.testing.includes(name)
/**
* 从落脚的出站一路跟着「当前选中」往下走,直到走到一条真正的线路。
*
* 策略组可以选中另一个策略组,所以「这个端口现在走哪条线」往往要跳好几层。
* 只显示第一层等于没回答问题。
*/
const chain = computed(() => {
const hops: string[] = []
let cursor = props.destination
while (cursor && !hops.includes(cursor)) {
hops.push(cursor)
cursor = props.runtime[cursor]?.now ?? null
}
return hops
})
/** 链条上最后一个还是策略组的环节——用户能在这里换选中。 */
const switchable = computed(() => {
for (let i = chain.value.length - 1; i >= 0; i -= 1) {
const name = chain.value[i]!
const info = props.runtime[name]
if (info?.all?.length) return { group: name, members: info.all, now: info.now ?? '' }
}
return null
})
const chain = computed(() => resolveChain(runtime.value, props.destination))
const switchable = computed(() => lastSwitchableGroup(runtime.value, chain.value))
function isCurrentOutbound(mode: OutboundMode): boolean {
const now = props.inbound.outbound
@@ -205,7 +180,7 @@ const NO_PROXY_FIELD = '该入站没有 proxy 字段'
</template>
<h4 :class="locked && 'is-dim'">订阅线路</h4>
<p v-if="!kernelOnline" class="hint">
<p v-if="!kernel.online" class="hint">
<IconOffline />
内核未连接,无法获取订阅线路
</p>