diff --git a/src/main/config/override.test.tsx b/src/main/config/override.test.tsx
new file mode 100644
index 00000000..9fb59e76
--- /dev/null
+++ b/src/main/config/override.test.tsx
@@ -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({null})
+ ).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')
+ })
+})
diff --git a/src/main/config/override.ts b/src/main/config/override.ts
index e4359309..27cce9d5 100644
--- a/src/main/config/override.ts
+++ b/src/main/config/override.ts
@@ -14,6 +14,17 @@ let overrideConfigVersion = 0
// 与 profile.yaml 共用(见 runtimeConfigQueue.ts)
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 {
if (force || !overrideConfig) {
const seen = overrideConfigVersion
@@ -24,12 +35,12 @@ export async function getOverrideConfig(force = false): Promise
}
if (typeof overrideConfig !== 'object') 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 {
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' })
overrideConfig = nextConfig
overrideConfigVersion++
@@ -47,7 +58,9 @@ export async function updateOverrideConfig(
throw new Error('Override config is invalid')
}
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' })
overrideConfig = nextConfig
overrideConfigVersion++
diff --git a/vitest.config.ts b/vitest.config.ts
index 6045e2e7..a90ed3d3 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
- include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'],
+ include: ['src/**/*.test.{ts,tsx}', 'scripts/**/*.test.ts'],
environment: 'node'
}
})