mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
fix: preserve numeric override IDs before JSON cloning (#2142)
This commit is contained in:
82
src/main/config/override.test.tsx
Normal file
82
src/main/config/override.test.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
||||||
|
import { tmpdir } from 'os'
|
||||||
|
import { join } from 'path'
|
||||||
|
import React from 'react'
|
||||||
|
import { renderToString } from 'react-dom/server'
|
||||||
|
import { SortableContext } from '@dnd-kit/sortable'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import {
|
||||||
|
getOverride,
|
||||||
|
getOverrideConfig,
|
||||||
|
getOverrideItem,
|
||||||
|
setOverrideConfig,
|
||||||
|
updateOverrideItem
|
||||||
|
} from './override'
|
||||||
|
|
||||||
|
let testDir = ''
|
||||||
|
|
||||||
|
vi.mock('../utils/dirs', () => ({
|
||||||
|
overrideConfigPath: () => join(testDir, 'override.yaml'),
|
||||||
|
overridePath: (id: string, ext: string) => join(testDir, `${id}.${ext}`)
|
||||||
|
}))
|
||||||
|
vi.mock('../utils/chromeRequest', () => ({ get: vi.fn() }))
|
||||||
|
vi.mock('./controledMihomo', () => ({ getControledMihomoConfig: vi.fn() }))
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = mkdtempSync(join(tmpdir(), 'clash-party-override-test-'))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(testDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
function writeConfig(id: string): void {
|
||||||
|
writeFileSync(
|
||||||
|
join(testDir, 'override.yaml'),
|
||||||
|
`items:\n - id: ${id}\n name: Existing override\n type: local\n ext: js\n updated: 0\n`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('legacy numeric override IDs', () => {
|
||||||
|
it.each([
|
||||||
|
['.inf', 'Infinity'],
|
||||||
|
['-.inf', '-Infinity'],
|
||||||
|
['.nan', 'NaN'],
|
||||||
|
['123456', '123456'],
|
||||||
|
['19e86409178', 'Infinity'],
|
||||||
|
['"19e86409178"', '19e86409178']
|
||||||
|
])('keeps %s usable after JSON transport', async (yamlId, expectedId) => {
|
||||||
|
writeConfig(yamlId)
|
||||||
|
const config = await getOverrideConfig(true)
|
||||||
|
// SortableContext probes object IDs with `in`; null would throw here.
|
||||||
|
const ids = JSON.parse(JSON.stringify(config)).items.map((item: IOverrideItem) => item.id)
|
||||||
|
expect(() =>
|
||||||
|
renderToString(<SortableContext items={ids}>{null}</SortableContext>)
|
||||||
|
).not.toThrow()
|
||||||
|
expect(config.items[0].id).toBe(expectedId)
|
||||||
|
expect((await getOverrideItem(expectedId))?.name).toBe('Existing override')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches and persists the same ID during a queued update', async () => {
|
||||||
|
writeConfig('.inf')
|
||||||
|
const { items } = await getOverrideConfig(true)
|
||||||
|
await updateOverrideItem({ ...items[0], global: true })
|
||||||
|
const saved = await getOverrideConfig(true)
|
||||||
|
expect(saved.items).toEqual([{ ...items[0], global: true }])
|
||||||
|
expect(readFileSync(join(testDir, 'override.yaml'), 'utf8')).toContain('id: Infinity')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves access to the existing override file', async () => {
|
||||||
|
writeConfig('.inf')
|
||||||
|
writeFileSync(join(testDir, 'Infinity.js'), 'function main(config) { return config }')
|
||||||
|
const { items } = await getOverrideConfig(true)
|
||||||
|
expect(await getOverride(items[0].id, items[0].ext)).toContain('function main')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes IDs before the setter JSON clone', async () => {
|
||||||
|
writeConfig('.inf')
|
||||||
|
const { items } = await getOverrideConfig(true)
|
||||||
|
await setOverrideConfig({ items: [{ ...items[0], id: Infinity as unknown as string }] })
|
||||||
|
expect((await getOverrideConfig(true)).items[0].id).toBe('Infinity')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,6 +14,17 @@ let overrideConfigVersion = 0
|
|||||||
// 与 profile.yaml 共用(见 runtimeConfigQueue.ts)
|
// 与 profile.yaml 共用(见 runtimeConfigQueue.ts)
|
||||||
const overrideConfigWriteQueue = runtimeConfigWriteQueue
|
const overrideConfigWriteQueue = runtimeConfigWriteQueue
|
||||||
|
|
||||||
|
// Legacy YAML can contain numeric IDs (including .inf). Convert them before
|
||||||
|
// JSON cloning turns non-finite numbers into null, breaking the sortable UI.
|
||||||
|
function normalizeOverrideIds(config: IOverrideConfig): IOverrideConfig {
|
||||||
|
if (Array.isArray(config.items)) {
|
||||||
|
config.items = config.items.map((item) =>
|
||||||
|
item && typeof item.id === 'number' ? { ...item, id: String(item.id) } : item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||||
if (force || !overrideConfig) {
|
if (force || !overrideConfig) {
|
||||||
const seen = overrideConfigVersion
|
const seen = overrideConfigVersion
|
||||||
@@ -24,12 +35,12 @@ export async function getOverrideConfig(force = false): Promise<IOverrideConfig>
|
|||||||
}
|
}
|
||||||
if (typeof overrideConfig !== 'object') overrideConfig = { items: [] }
|
if (typeof overrideConfig !== 'object') overrideConfig = { items: [] }
|
||||||
if (!Array.isArray(overrideConfig.items)) overrideConfig.items = []
|
if (!Array.isArray(overrideConfig.items)) overrideConfig.items = []
|
||||||
return JSON.parse(JSON.stringify(overrideConfig)) as IOverrideConfig
|
return JSON.parse(JSON.stringify(normalizeOverrideIds(overrideConfig))) as IOverrideConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
||||||
await overrideConfigWriteQueue.run(async () => {
|
await overrideConfigWriteQueue.run(async () => {
|
||||||
const nextConfig = JSON.parse(JSON.stringify(config)) as IOverrideConfig
|
const nextConfig = JSON.parse(JSON.stringify(normalizeOverrideIds(config))) as IOverrideConfig
|
||||||
await atomicWriteFile(overrideConfigPath(), stringify(nextConfig), { encoding: 'utf8' })
|
await atomicWriteFile(overrideConfigPath(), stringify(nextConfig), { encoding: 'utf8' })
|
||||||
overrideConfig = nextConfig
|
overrideConfig = nextConfig
|
||||||
overrideConfigVersion++
|
overrideConfigVersion++
|
||||||
@@ -47,7 +58,9 @@ export async function updateOverrideConfig(
|
|||||||
throw new Error('Override config is invalid')
|
throw new Error('Override config is invalid')
|
||||||
}
|
}
|
||||||
if (!Array.isArray(currentConfig.items)) currentConfig.items = []
|
if (!Array.isArray(currentConfig.items)) currentConfig.items = []
|
||||||
const nextConfig = updater(JSON.parse(JSON.stringify(currentConfig)) as IOverrideConfig)
|
const nextConfig = updater(
|
||||||
|
JSON.parse(JSON.stringify(normalizeOverrideIds(currentConfig))) as IOverrideConfig
|
||||||
|
)
|
||||||
await atomicWriteFile(overrideConfigPath(), stringify(nextConfig), { encoding: 'utf8' })
|
await atomicWriteFile(overrideConfigPath(), stringify(nextConfig), { encoding: 'utf8' })
|
||||||
overrideConfig = nextConfig
|
overrideConfig = nextConfig
|
||||||
overrideConfigVersion++
|
overrideConfigVersion++
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'],
|
include: ['src/**/*.test.{ts,tsx}', 'scripts/**/*.test.ts'],
|
||||||
environment: 'node'
|
environment: 'node'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user