Skip to content

Commit dba16e4

Browse files
committed
test: organize regression suites and raise coverage gate
1 parent cab1dcb commit dba16e4

75 files changed

Lines changed: 6608 additions & 167 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/ui/src/product/converter/advanced-mode/sections/rules-management-section.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@ describe("RulesManagementSection", () => {
165165
{
166166
key: "custom-rule:ip",
167167
editable: true,
168-
summary: "35.212.230.0/24",
168+
summary: "203.0.113.0/24",
169169
sourceLabel: "自定义规则",
170170
target: "🚀 节点选择",
171171
noResolve: false,
172-
text: "IP-CIDR,35.212.230.0/24,🚀 节点选择",
172+
text: "IP-CIDR,203.0.113.0/24,🚀 节点选择",
173173
},
174174
{
175175
key: "special:match",
@@ -224,14 +224,14 @@ describe("RulesManagementSection", () => {
224224
element.props.className.includes("min-w-[8.75rem]"),
225225
)[0];
226226

227-
expect(collectText(titleLine)).toContain("35.212.230.0/24");
227+
expect(collectText(titleLine)).toContain("203.0.113.0/24");
228228
expect(collectText(titleLine)).toContain("自定义规则");
229229
expect(collectText(titleLine)).toContain("🚀 节点选择");
230-
expect(collectText(titleLine)).not.toContain("IP-CIDR 35.212.230.0/24");
231-
expect(collectText(detail)).toBe("IP-CIDR,35.212.230.0/24");
230+
expect(collectText(titleLine)).not.toContain("IP-CIDR 203.0.113.0/24");
231+
expect(collectText(detail)).toBe("IP-CIDR,203.0.113.0/24");
232232
expect(collectText(detail)).not.toContain("🚀 节点选择");
233233
expect(collectText(detail)).not.toContain("no-resolve");
234-
expect(detail.props.title).toBe("IP-CIDR,35.212.230.0/24,🚀 节点选择");
234+
expect(detail.props.title).toBe("IP-CIDR,203.0.113.0/24,🚀 节点选择");
235235
expect(entryRow.props.className).toContain("border-white/10");
236236
expect(entryRow.props.className).toContain("bg-white/5");
237237
expect(row.props.className).not.toContain("sm:grid");

test/coverage-cases/public-core-config-refresh-branch-coverage.test.ts renamed to test/regression/core/config-and-source-refresh.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { describe, expect, it } from "vitest";
2-
import { buildGenerateOptionsFromConfig } from "../../packages/core/src/subscription/config-utils";
2+
import { buildGenerateOptionsFromConfig } from "../../../packages/core/src/subscription/config-utils";
33
import {
44
mergeParsedSourceNodes,
55
prepareSourceParsedNodes,
6-
} from "../../packages/core/src/subscription/source-node-refresh";
7-
import { ORIGIN_NAME_KEY, SOURCE_IDS_KEY } from "../../packages/core/src/subscription/node-source-state";
6+
} from "../../../packages/core/src/subscription/source-node-refresh";
7+
import { ORIGIN_NAME_KEY, SOURCE_IDS_KEY } from "../../../packages/core/src/subscription/node-source-state";
88
import {
99
isMihomoSupportedProxyNode,
1010
normalizeMihomoVlessForGeneration,
1111
sanitizeMihomoProxyNode,
12-
} from "../../packages/core/src/mihomo/proxy-sanitizer";
13-
import { parsePlatformProxyLine } from "../../packages/core/src/parser/platform/parse-platform-proxy-line";
14-
import type { ParsedNode } from "../../packages/core/src/types/node";
12+
} from "../../../packages/core/src/mihomo/proxy-sanitizer";
13+
import { parsePlatformProxyLine } from "../../../packages/core/src/parser/platform/parse-platform-proxy-line";
14+
import type { ParsedNode } from "../../../packages/core/src/types/node";
1515

1616
const REALITY_PUBLIC_KEY = "B".repeat(43);
1717
const WIREGUARD_KEY = `${"C".repeat(43)}=`;

test/coverage-cases/public-core-generator-refresh-extra-coverage.test.ts renamed to test/regression/core/generator-and-source-refresh.test.ts

File renamed without changes.
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { describe, expect, it } from "vitest";
2+
import { generateClashConfig } from "../../../packages/core/src/generator";
3+
import {
4+
GroupListenerError,
5+
resolveGroupListenerEntries,
6+
} from "../../../packages/core/src/generator/group-listeners";
7+
import {
8+
getEffectiveModuleRuleItems,
9+
getEffectiveModuleRules,
10+
isModuleRuleMovedFrom,
11+
normalizeHiddenPresetRuleIds,
12+
} from "../../../packages/core/src/generator/module-rules";
13+
import { PROXY_GROUP_MODULES } from "../../../packages/core/src/generator/proxy-group-modules";
14+
import {
15+
chooseFallbackPolicyTarget,
16+
createPolicyTargetResolver,
17+
uniquePolicyTargets,
18+
} from "../../../packages/core/src/generator/policy-targets";
19+
import { generateProxyGroups } from "../../../packages/core/src/generator/proxy-groups";
20+
import { buildTypedProxyGroup } from "../../../packages/core/src/generator/proxy-group-type";
21+
import {
22+
hasFullRuleOrderKeys,
23+
normalizePersistedRuleOrder,
24+
resolveAppliedRuleOrder,
25+
} from "../../../packages/core/src/generator/rules";
26+
import { collectDnsPolicyEntries, configToYaml } from "../../../packages/core/src/generator/yaml";
27+
import {
28+
normalizeProxyGroupAdvancedConfig,
29+
resolveProxyGroupMembers,
30+
} from "../../../packages/core/src/proxy-group-advanced";
31+
import {
32+
normalizeGroupNameWithDefaultEmoji,
33+
resolveProxyGroupModuleName,
34+
splitLeadingEmoji,
35+
} from "../../../packages/core/src/proxy-group-name";
36+
import { resolveProxyGroupTargetName } from "../../../packages/core/src/proxy-group-targets";
37+
import type {
38+
CustomProxyGroup,
39+
GroupListenerBinding,
40+
} from "../../../packages/core/src/types/config";
41+
import type { ParsedNode } from "../../../packages/core/src/types/node";
42+
43+
function ssNode(name: string, patch: Record<string, unknown> = {}): ParsedNode {
44+
return {
45+
name,
46+
type: "ss",
47+
server: `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "node"}.example.com`,
48+
port: 8388,
49+
cipher: "aes-128-gcm",
50+
password: "secret",
51+
...patch,
52+
} as unknown as ParsedNode;
53+
}
54+
55+
function binding(patch: Partial<GroupListenerBinding> = {}): GroupListenerBinding {
56+
return {
57+
id: "listener",
58+
target: { kind: "module", id: "auto" },
59+
port: 7891,
60+
...patch,
61+
};
62+
}
63+
64+
describe("core generator edge regressions", () => {
65+
it("validates malformed listener collections before emitting a listener", () => {
66+
const resolveTarget = () => ({ exists: true, active: true, name: "Target" });
67+
68+
expect(
69+
resolveGroupListenerEntries({
70+
bindings: null as never,
71+
resolveTarget,
72+
nodeListenerPorts: [],
73+
baseListenerPorts: [],
74+
usedNames: new Set(),
75+
}),
76+
).toEqual([]);
77+
78+
expect(() =>
79+
resolveGroupListenerEntries({
80+
bindings: [null as never, binding()],
81+
resolveTarget: () => ({ exists: true, active: true }),
82+
effectiveMixedPort: 0,
83+
nodeListenerPorts: [0, 7100, 7100],
84+
baseListenerPorts: [Number.NaN, 7200, 7200],
85+
usedNames: new Set(),
86+
}),
87+
).toThrow(//);
88+
89+
expect(() =>
90+
resolveGroupListenerEntries({
91+
bindings: [binding({ port: 1.5 })],
92+
resolveTarget,
93+
nodeListenerPorts: [],
94+
baseListenerPorts: [],
95+
usedNames: new Set(),
96+
}),
97+
).toThrow(//);
98+
});
99+
100+
it("rejects every stale listener target kind through the public generator", () => {
101+
const base = {
102+
nodes: [ssNode("Node")],
103+
userConfig: { dnsYaml: "", enabledGroups: ["select", "auto", "final"] },
104+
};
105+
const staleTargets: unknown[] = [
106+
null,
107+
{ kind: "module", id: "missing" },
108+
{ kind: "custom", id: "missing" },
109+
{ kind: "dialer", id: "missing" },
110+
{ kind: "future", id: "missing" },
111+
];
112+
113+
for (const target of staleTargets) {
114+
expect(() =>
115+
generateClashConfig({
116+
...base,
117+
groupListeners: [binding({ target: target as never })],
118+
}),
119+
).toThrow(GroupListenerError);
120+
}
121+
});
122+
123+
it("keeps name collision resolution and sparse base listeners deterministic", () => {
124+
const config = generateClashConfig({
125+
nodes: [ssNode("Dup"), ssNode("Dup (2)"), ssNode("Dup")],
126+
userConfig: {
127+
dnsYaml: [
128+
"listeners:",
129+
" - null",
130+
" - 7",
131+
" - name: 42",
132+
" type: mixed",
133+
" port: text",
134+
].join("\n"),
135+
enabledGroups: ["select", "auto", "final"],
136+
},
137+
groupListeners: [binding({ port: 7892 })],
138+
});
139+
140+
expect(config.proxies?.map((node) => node.name)).toEqual(["Dup", "Dup (2)", "Dup (3)"]);
141+
expect(config.listeners).toEqual([
142+
null,
143+
7,
144+
{ name: 42, type: "mixed", port: "text" },
145+
expect.objectContaining({ name: "group-mixed-0", port: 7892 }),
146+
]);
147+
});
148+
149+
it("handles blank custom and dialer metadata without leaking invalid groups", () => {
150+
const config = generateClashConfig({
151+
nodes: [ssNode("Node")],
152+
customProxyGroups: [
153+
{ id: "blank", name: 9, emoji: "", groupType: "select" } as never,
154+
],
155+
dialerProxyGroups: [
156+
{
157+
id: "blank-dialer",
158+
name: " ",
159+
type: "select",
160+
relayNodes: ["Node"],
161+
targetNodes: [],
162+
} as never,
163+
],
164+
proxyGroupOrder: ["missing", "missing"],
165+
userConfig: { dnsYaml: "", enabledGroups: ["select", "auto", "final"] },
166+
});
167+
168+
expect(config.proxies).toHaveLength(1);
169+
expect(config["proxy-groups"]?.length).toBeGreaterThan(0);
170+
});
171+
172+
it("normalizes module rules with sparse hidden and moved rule data", () => {
173+
const cn = PROXY_GROUP_MODULES.find((module) => module.id === "cn")!;
174+
const firstRule = cn.rules[0];
175+
176+
expect(normalizeHiddenPresetRuleIds({ cn: ["", firstRule.id, firstRule.id], empty: [] })).toEqual({
177+
cn: [firstRule.id],
178+
});
179+
expect(
180+
isModuleRuleMovedFrom(cn.id, firstRule.id, {
181+
"": [null as never],
182+
missing: [{ id: firstRule.id }],
183+
global: [{ id: firstRule.id }],
184+
}),
185+
).toBe(true);
186+
expect(
187+
getEffectiveModuleRuleItems(cn, {
188+
cn: [null as never, { ...firstRule, id: "extra" }],
189+
}, {
190+
cn: [firstRule.id],
191+
}).some((item) => item.id === "extra"),
192+
).toBe(true);
193+
expect(
194+
getEffectiveModuleRules(cn, {
195+
ruleSetsByTarget: { cn: null as never },
196+
}),
197+
).toEqual(cn.rules);
198+
});
199+
200+
it("keeps policy target and group display fallbacks explicit", () => {
201+
expect(uniquePolicyTargets(["", " DIRECT ", 1, "DIRECT", "Proxy"])).toEqual(["DIRECT", "Proxy"]);
202+
expect(chooseFallbackPolicyTarget([null, " Missing ", "DIRECT"], ["DIRECT"])).toBe("DIRECT");
203+
expect(chooseFallbackPolicyTarget([], [])).toBe("DIRECT");
204+
const resolve = createPolicyTargetResolver({ availablePolicyTargets: ["DIRECT"], fallbackPolicyTarget: "" });
205+
expect(resolve("missing")).toBe("DIRECT");
206+
207+
expect(splitLeadingEmoji("Label")).toEqual({ emoji: "", label: "Label", hasEmojiPrefix: false });
208+
expect(splitLeadingEmoji(12 as never)).toEqual({ emoji: "", label: "", hasEmojiPrefix: false });
209+
expect(normalizeGroupNameWithDefaultEmoji("", "F")).toEqual({ full: "", emoji: "F" });
210+
expect(normalizeGroupNameWithDefaultEmoji("🔥 Ready", "F")).toEqual({ full: "🔥 Ready", emoji: "🔥" });
211+
expect(resolveProxyGroupModuleName({ name: "X", emoji: "E" }, 1 as never)).toBe("X");
212+
});
213+
214+
it("resolves malformed policy references through documented fallbacks", () => {
215+
const customProxyGroups = [
216+
{ id: "custom", name: " Custom ", emoji: "", groupType: "select" },
217+
] as CustomProxyGroup[];
218+
const options = {
219+
moduleNames: { auto: "Auto" },
220+
customProxyGroups,
221+
fallbackTarget: "DIRECT",
222+
};
223+
224+
expect(resolveProxyGroupTargetName(" ", options)).toBe("DIRECT");
225+
expect(resolveProxyGroupTargetName({ kind: "module", id: "missing" }, options)).toBe("DIRECT");
226+
expect(resolveProxyGroupTargetName({ kind: "custom", id: "custom" }, options)).toBe("Custom");
227+
expect(resolveProxyGroupTargetName({ kind: "custom", id: "missing" }, options)).toBe("DIRECT");
228+
});
229+
230+
it("covers sparse advanced members and load-balance defaults", () => {
231+
expect(normalizeProxyGroupAdvancedConfig({ sourceIds: [null, " source "] })).toMatchObject({
232+
sourceIds: ["source"],
233+
});
234+
expect(
235+
resolveProxyGroupMembers({
236+
defaultProxyNames: [null, "", "Node"] as never,
237+
nodes: [ssNode("Node")],
238+
advanced: {},
239+
}).proxyNames,
240+
).toContain("Node");
241+
242+
expect(
243+
buildTypedProxyGroup({
244+
name: "LB",
245+
groupType: "load-balance",
246+
proxies: ["Node"],
247+
url: "https://example.com/generate_204",
248+
interval: 300,
249+
}),
250+
).toMatchObject({ type: "load-balance", strategy: "consistent-hashing" });
251+
});
252+
253+
it("serializes empty values and comparator ties without dropping valid DNS policy", () => {
254+
const yaml = configToYaml({
255+
proxies: [{ name: "Node", type: "ss", server: "n.example.com", port: 8388, cipher: "x", password: "p", aa: 1, bb: 2 }],
256+
"proxy-groups": [{ name: "G", type: "select", proxies: ["Node"], aa: 1, bb: 2 }],
257+
"rule-providers": {},
258+
rules: [],
259+
listeners: undefined,
260+
} as never);
261+
expect(yaml).toContain("aa: 1");
262+
expect(collectDnsPolicyEntries({ "+.same.example": ["", "1.1.1.1"] })).toEqual([
263+
["+.same.example", ["1.1.1.1"]],
264+
]);
265+
266+
expect(
267+
generateProxyGroups({
268+
nodes: [ssNode("Node")],
269+
enabledModules: ["select", "unknown", "final"],
270+
ruleProviderBaseUrl: "https://rules.example",
271+
testUrl: "https://example.com/generate_204",
272+
testInterval: 300,
273+
customProxyGroups: [{ id: "late", name: "Late", emoji: "", groupType: "select" }],
274+
}).some((group) => group.name === "Late"),
275+
).toBe(true);
276+
});
277+
278+
it("preserves unusual persisted rule orders without inventing keys", () => {
279+
const options = {
280+
enabledModules: ["cn", "final"],
281+
customRules: [
282+
{ id: "one", type: "DOMAIN" as const, value: "one.example", target: "DIRECT" },
283+
{ id: "two", type: "DOMAIN" as const, value: "two.example", target: "DIRECT" },
284+
],
285+
ruleOrder: ["custom-rule:one"],
286+
};
287+
expect(normalizePersistedRuleOrder(options)).toEqual(["custom-rule:one", "custom-rule:two"]);
288+
expect(resolveAppliedRuleOrder(options)).toEqual(expect.arrayContaining(["custom-rule:one", "custom-rule:two"]));
289+
expect(hasFullRuleOrderKeys(["module:cn:", "special:match"])).toBe(true);
290+
});
291+
});

test/coverage-cases/public-core-extra-branch-coverage.test.ts renamed to test/regression/core/parser-and-template-edge-cases.test.ts

File renamed without changes.

test/coverage-cases/public-core-branch-coverage.test.ts renamed to test/regression/core/parser-and-template-regressions.test.ts

File renamed without changes.

test/coverage-cases/public-core-parser-remaining-edge-coverage.test.ts renamed to test/regression/core/parser-edge-cases.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
import { describe, expect, it } from "vitest";
2-
import { parseClashYaml } from "../../packages/core/src/parser/clash-yaml";
3-
import { canonicalizeParsedNode, pickAliasValue } from "../../packages/core/src/parser/canonical-fields";
4-
import { parseConfigLine } from "../../packages/core/src/parser/config-line-parser";
2+
import { parseClashYaml } from "../../../packages/core/src/parser/clash-yaml";
3+
import { canonicalizeParsedNode, pickAliasValue } from "../../../packages/core/src/parser/canonical-fields";
4+
import { parseConfigLine } from "../../../packages/core/src/parser/config-line-parser";
55
import {
66
applyCommonNodeParams,
77
applyTransport,
88
inferSkipCertVerify,
99
isUuidLike,
1010
parseWsHeaders,
1111
tokenizeConfigLine,
12-
} from "../../packages/core/src/parser/config-line-tokenizer";
13-
import { parsePlatformProxyLine } from "../../packages/core/src/parser/platform/parse-platform-proxy-line";
12+
} from "../../../packages/core/src/parser/config-line-tokenizer";
13+
import { parsePlatformProxyLine } from "../../../packages/core/src/parser/platform/parse-platform-proxy-line";
1414
import {
1515
parseLineBasedSubscriptionContent,
1616
parseSubscriptionContentByRegistry,
1717
splitNodeLinkSegments,
18-
} from "../../packages/core/src/parser/content-parsers";
19-
import { preprocessSubscriptionContent } from "../../packages/core/src/parser/preprocess";
20-
import { parseHysteria2 } from "../../packages/core/src/parser/protocols/hysteria2";
21-
import { parseNetch } from "../../packages/core/src/parser/protocols/netch";
22-
import { normalizeSsPlugin, parseSS } from "../../packages/core/src/parser/protocols/ss";
23-
import { parseSSR } from "../../packages/core/src/parser/protocols/ssr";
18+
} from "../../../packages/core/src/parser/content-parsers";
19+
import { preprocessSubscriptionContent } from "../../../packages/core/src/parser/preprocess";
20+
import { parseHysteria2 } from "../../../packages/core/src/parser/protocols/hysteria2";
21+
import { parseNetch } from "../../../packages/core/src/parser/protocols/netch";
22+
import { normalizeSsPlugin, parseSS } from "../../../packages/core/src/parser/protocols/ss";
23+
import { parseSSR } from "../../../packages/core/src/parser/protocols/ssr";
2424
import {
2525
parseHttp,
2626
parseSimpleProxy,
2727
parseSocks,
2828
parseSsh,
2929
parseTelegramProxyLink,
30-
} from "../../packages/core/src/parser/protocols/simple-proxy";
31-
import { parseVMess } from "../../packages/core/src/parser/protocols/vmess";
32-
import { parseVLESS } from "../../packages/core/src/parser/protocols/vless";
30+
} from "../../../packages/core/src/parser/protocols/simple-proxy";
31+
import { parseVMess } from "../../../packages/core/src/parser/protocols/vmess";
32+
import { parseVLESS } from "../../../packages/core/src/parser/protocols/vless";
3333

3434
const UUID = "11111111-1111-4111-8111-111111111111";
3535

test/coverage-cases/public-core-last-branch-coverage.test.ts renamed to test/regression/core/parser-generator-refresh-regressions.test.ts

File renamed without changes.

0 commit comments

Comments
 (0)