From e32f81d4454484808eabf9bc306b27a3bea80e16 Mon Sep 17 00:00:00 2001 From: BlueX888 <140241684+BlueX888@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:35:55 +0800 Subject: [PATCH] fix(router): prefer exact tier match over word-prefix fallback in parseTier The token-saver fallback parser iterated knownTiers in order with a word-boundary regex. Because '-' is a word boundary, a tier that is a word-prefix of a hyphenated tier (e.g. 'fast' vs 'fast-pro') matched first and won even when the judge output named the longer tier exactly. Check exact (case-insensitive) equality against known tiers before the regex fallback, and sort fallback candidates longest-first so free-text mentions also resolve to the most specific tier. Fixes #428 --- src/router/tokenSaver/parseTier.ts | 9 ++++++++- tests/router/parseTier.spec.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/router/parseTier.spec.ts diff --git a/src/router/tokenSaver/parseTier.ts b/src/router/tokenSaver/parseTier.ts index f863780bb..7893794b1 100644 --- a/src/router/tokenSaver/parseTier.ts +++ b/src/router/tokenSaver/parseTier.ts @@ -10,7 +10,14 @@ export function parseTier(judgeOutput: string, knownTiers: string[]): string | u if (found) return found; } - for (const tier of knownTiers) { + const exact = knownTiers.find(t => t.toLowerCase() === cleaned.toLowerCase()); + if (exact) return exact; + + // Longest tiers first: `-` is a word boundary, so a shorter tier that is a + // word-prefix of a hyphenated tier (e.g. "fast" vs "fast-pro") would + // otherwise match inside the longer name. + const byLength = [...knownTiers].sort((a, b) => b.length - a.length); + for (const tier of byLength) { const pattern = new RegExp(`\\b${escapeRegex(tier)}\\b`, "i"); if (pattern.test(cleaned)) { return tier; diff --git a/tests/router/parseTier.spec.ts b/tests/router/parseTier.spec.ts new file mode 100644 index 000000000..dd62d59b3 --- /dev/null +++ b/tests/router/parseTier.spec.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { parseTier } from "../../src/router/tokenSaver/parseTier.js"; + +test("prefers an exact tier over a word-prefix of a hyphenated tier", () => { + assert.equal(parseTier("a-a", ["a", "a-a"]), "a-a"); + assert.equal(parseTier("fast-pro", ["fast", "fast-pro"]), "fast-pro"); +}); + +test("keeps exact matching case-insensitive", () => { + assert.equal(parseTier("Fast-Pro", ["fast", "fast-pro"]), "fast-pro"); +}); + +test("tagged output still resolves the longer tier", () => { + assert.equal(parseTier("a-a", ["a", "a-a"]), "a-a"); +}); + +test("fallback text matching picks the longest tier mentioned", () => { + assert.equal(parseTier("please use a-a", ["a", "a-a"]), "a-a"); +}); + +test("fallback text matching still resolves a plain tier mention", () => { + assert.equal(parseTier("the answer is fast", ["fast", "fast-pro"]), "fast"); +}); + +test("returns undefined when no tier matches", () => { + assert.equal(parseTier("no such tier", ["fast", "fast-pro"]), undefined); +});