From fda66315addb1ea6fd1c79cb7414d0ea16b2e58d Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:09:37 +0200 Subject: [PATCH] test(sse): cover the 14 exported specificity-rules detectors (#9063) Validated in local merge-train T5 (base49+contributors+pacocartones) --- .../9063-cover-specificity-rules.md | 7 + tests/unit/specificity-rules.test.ts | 394 ++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 changelog.d/maintenance/9063-cover-specificity-rules.md create mode 100644 tests/unit/specificity-rules.test.ts diff --git a/changelog.d/maintenance/9063-cover-specificity-rules.md b/changelog.d/maintenance/9063-cover-specificity-rules.md new file mode 100644 index 0000000000..f4e0ef5ce6 --- /dev/null +++ b/changelog.d/maintenance/9063-cover-specificity-rules.md @@ -0,0 +1,7 @@ +- **test(sse):** added the first test suite for `open-sse/services/specificityRules.ts` — the + module was hotspot #7 in the coverage plan at 11.28% lines with no dedicated test; the 14 pure + detectors are now pinned edge-to-edge (token ladders, per-domain max-not-sum scoring, and the + min/max clamps), taking the file to ~100% line coverage. The suite also documents two quirks + left unchanged: `detectReasoningDepth` scores above 0 on marker-free input via its + always-applied message-depth bonus, and `detectErrorContext` returns non-integer scores because + it never rounds ([#9063](https://github.com/diegosouzapw/OmniRoute/pull/9063)) diff --git a/tests/unit/specificity-rules.test.ts b/tests/unit/specificity-rules.test.ts new file mode 100644 index 0000000000..7ee5ae38ab --- /dev/null +++ b/tests/unit/specificity-rules.test.ts @@ -0,0 +1,394 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { + estimateTokens, + estimateMessageTokens, + detectCodeComplexity, + detectMathComplexity, + detectReasoningDepth, + detectContextSize, + detectToolCalling, + detectDomainSpecificity, + getSpecificityBreakdown, + detectConversationDepth, + detectFileReferences, + detectErrorContext, + detectEnhancedContextSize, + getEnhancedSpecificityBreakdown, +} from "../../open-sse/services/specificityRules.ts"; +import type { RuleInput } from "../../open-sse/services/specificityTypes.ts"; + +// Helpers. Every expected value below is derived by hand from the source logic in +// open-sse/services/specificityRules.ts, not recorded from a live run. +const repeat = (n: number): string => "a".repeat(n); +const stringMessages = (content: string): RuleInput => ({ messages: [{ content }] }); + +describe("estimateTokens", () => { + test("empty string is 0 tokens", () => { + assert.equal(estimateTokens(""), 0); + }); + + test("exactly 4 chars is 1 token (4/4)", () => { + assert.equal(estimateTokens("abcd"), 1); + }); + + test("rounds up: 5 chars -> ceil(1.25) -> 2 tokens", () => { + assert.equal(estimateTokens("abcde"), 2); + }); + + test("rounds up: 1 char -> ceil(0.25) -> 1 token", () => { + assert.equal(estimateTokens("a"), 1); + }); +}); + +describe("estimateMessageTokens", () => { + test("string content is summed via estimateTokens", () => { + // "abcd" -> 1, "abcdefgh" (8) -> 2 => 3 + assert.equal(estimateMessageTokens([{ content: "abcd" }, { content: "abcdefgh" }]), 3); + }); + + test("array content sums estimateTokens of each string .text part", () => { + // [{text:"abcd"}] -> 1, [{text:"abcdefgh"}] -> 2 => 3 + assert.equal( + estimateMessageTokens([{ content: [{ text: "abcd" }, { text: "abcdefgh" }] }]), + 3 + ); + }); + + test("array parts whose .text is not a string contribute 0", () => { + assert.equal( + estimateMessageTokens([{ content: [{ text: 123 }, { notText: "abcd" }] }]), + 0 + ); + }); + + test("non-string, non-array content hits the untouched-sum branch (0)", () => { + assert.equal(estimateMessageTokens([{ content: { foo: 1 } }]), 0); + }); + + test("mixed string + object content only counts the string", () => { + assert.equal(estimateMessageTokens([{ content: "abcd" }, { content: { x: 1 } }]), 1); + }); +}); + +describe("detectCodeComplexity", () => { + test("no code -> 0", () => { + assert.equal(detectCodeComplexity(stringMessages("hello world")), 0); + }); + + test("one language indicator (const decl) -> raw 2", () => { + // langMatches: /const\s+\w+\s*=/ matches once => 1*2 = 2 + assert.equal(detectCodeComplexity(stringMessages("const x = 5")), 2); + }); + + test("one inline code span -> raw 0.5 -> round -> 1", () => { + // inlineCodeCount 1 => 0.5, Math.round(0.5) === 1 + assert.equal(detectCodeComplexity(stringMessages("use `foo` here")), 1); + }); + + test("clamps to a maximum of 25", () => { + // 13 const decls => langMatches 13 => 13*2 = 26 => min(25, 26) = 25 + assert.equal(detectCodeComplexity(stringMessages("const a = ".repeat(13))), 25); + }); +}); + +describe("detectMathComplexity", () => { + test("no math -> 0", () => { + assert.equal(detectMathComplexity(stringMessages("hello world")), 0); + }); + + test("one inline latex span -> 1*4 = 4", () => { + assert.equal(detectMathComplexity(stringMessages("$x$")), 4); + }); + + test("three math word indicators -> 3*1.5 = 4.5 -> round -> 5", () => { + // /\b(sin|cos|tan|...)\b/ matches sin, cos, tan => 3 => 4.5 => Math.round -> 5 + assert.equal(detectMathComplexity(stringMessages("sin cos tan")), 5); + }); + + test("clamps to a maximum of 20", () => { + // 6 block-latex spans => 6*4 = 24 => min(20, 24) = 20 + assert.equal( + detectMathComplexity(stringMessages(Array(6).fill("$$a$$").join(" "))), + 20 + ); + }); +}); + +describe("detectReasoningDepth", () => { + test("no messages -> 0 (no depth bonus)", () => { + assert.equal(detectReasoningDepth({ messages: [] }), 0); + }); + + test("FINDING: a message with no reasoning markers still scores > 0", () => { + // reasonMatches 0, but messageDepthBonus = min(5, 1) = 1 => raw 1 => 1 + assert.equal(detectReasoningDepth(stringMessages("hello")), 1); + }); + + test("one reasoning marker + 1-message bonus -> 2 + 1 = 3", () => { + assert.equal(detectReasoningDepth(stringMessages("therefore")), 3); + }); + + test("message-depth bonus clamps at 5", () => { + // 6 marker-free messages => reasonMatches 0, bonus = min(5, 6) = 5 => 5 + const messages = Array.from({ length: 6 }, () => ({ content: "hello" })); + assert.equal(detectReasoningDepth({ messages }), 5); + }); +}); + +describe("detectContextSize", () => { + // estimateTokens = ceil(len/4); each string of length N yields ceil(N/4) tokens. + test("empty -> 0", () => { + assert.equal(detectContextSize(stringMessages("")), 0); + }); + + test("exactly 1000 tokens is NOT > 1000 -> 0", () => { + assert.equal(detectContextSize(stringMessages(repeat(4000))), 0); + }); + + test("> 1000 tokens -> 2", () => { + assert.equal(detectContextSize(stringMessages(repeat(4004))), 2); + }); + + test("> 4000 tokens -> 4", () => { + assert.equal(detectContextSize(stringMessages(repeat(16004))), 4); + }); + + test("> 8000 tokens -> 6", () => { + assert.equal(detectContextSize(stringMessages(repeat(32004))), 6); + }); + + test("> 16000 tokens -> 9", () => { + assert.equal(detectContextSize(stringMessages(repeat(64004))), 9); + }); + + test("> 32000 tokens -> 12", () => { + assert.equal(detectContextSize(stringMessages(repeat(128004))), 12); + }); + + test("> 64000 tokens -> 15", () => { + assert.equal(detectContextSize(stringMessages(repeat(256004))), 15); + }); +}); + +describe("detectToolCalling", () => { + const tools = (n: number) => + Array.from({ length: n }, (_, i) => ({ function: { name: `t${i}` } })); + + test("undefined tools -> 0", () => { + assert.equal(detectToolCalling({ messages: [] }), 0); + }); + + test("0 tools -> 0", () => { + assert.equal(detectToolCalling({ messages: [], tools: tools(0) }), 0); + }); + + test("1 tool (<=2) -> 2", () => { + assert.equal(detectToolCalling({ messages: [], tools: tools(1) }), 2); + }); + + test("3 tools (>2) -> 4", () => { + assert.equal(detectToolCalling({ messages: [], tools: tools(3) }), 4); + }); + + test("6 tools (>5) -> 6", () => { + assert.equal(detectToolCalling({ messages: [], tools: tools(6) }), 6); + }); + + test("11 tools (>10) -> 8", () => { + assert.equal(detectToolCalling({ messages: [], tools: tools(11) }), 8); + }); + + test("21 tools (>20) -> 10", () => { + assert.equal(detectToolCalling({ messages: [], tools: tools(21) }), 10); + }); +}); + +describe("detectDomainSpecificity", () => { + test("no domain terms -> 0", () => { + assert.equal(detectDomainSpecificity(stringMessages("hello world")), 0); + }); + + test("all 5 medical terms -> 5*2 = 10 (and hits the clamp)", () => { + assert.equal( + detectDomainSpecificity( + stringMessages("diagnosis symptoms treatment patient clinical") + ), + 10 + ); + }); + + test("two legal terms -> 4", () => { + assert.equal(detectDomainSpecificity(stringMessages("pursuant statute")), 4); + }); + + test("scientific has only 4 terms -> max 8", () => { + assert.equal( + detectDomainSpecificity( + stringMessages("hypothesis methodology empirical significant") + ), + 8 + ); + }); + + test("mixed domains return the MAX domain score, not the sum", () => { + // 1 medical term (2) + 1 legal term (2). Sum would be 4; max is 2. + assert.equal(detectDomainSpecificity(stringMessages("diagnosis pursuant")), 2); + }); +}); + +describe("getSpecificityBreakdown", () => { + test("assembles the six detectors, using detectContextSize for contextSize", () => { + // "const x = 5": code 2, math 0, reasoning 1 (depth bonus), context 0, tools 0, domain 0 + assert.deepEqual(getSpecificityBreakdown(stringMessages("const x = 5")), { + codeComplexity: 2, + mathComplexity: 0, + reasoningDepth: 1, + contextSize: 0, + toolCalling: 0, + domainSpecificity: 0, + }); + }); +}); + +describe("detectConversationDepth", () => { + const turns = (n: number) => Array.from({ length: n }, () => ({ role: "user", content: "x" })); + + test("messages without a role are not counted -> 0", () => { + assert.equal(detectConversationDepth(stringMessages("hello")), 0); + }); + + test("<=5 turns -> 0", () => { + assert.equal(detectConversationDepth({ messages: turns(5) }), 0); + }); + + test("6 turns (>5) -> 2", () => { + assert.equal(detectConversationDepth({ messages: turns(6) }), 2); + }); + + test("11 turns (>10) -> 4", () => { + assert.equal(detectConversationDepth({ messages: turns(11) }), 4); + }); + + test("21 turns (>20) -> 6", () => { + assert.equal(detectConversationDepth({ messages: turns(21) }), 6); + }); + + test("31 turns (>30) -> 8", () => { + assert.equal(detectConversationDepth({ messages: turns(31) }), 8); + }); + + test("counts both user and assistant roles", () => { + // 3 user + 3 assistant = 6 turns -> 2 + const messages = [ + { role: "user", content: "a" }, + { role: "assistant", content: "b" }, + { role: "user", content: "c" }, + { role: "assistant", content: "d" }, + { role: "user", content: "e" }, + { role: "assistant", content: "f" }, + ]; + assert.equal(detectConversationDepth({ messages }), 2); + }); +}); + +describe("detectFileReferences", () => { + test("no references -> 0", () => { + assert.equal(detectFileReferences(stringMessages("hello world")), 0); + }); + + test("a multi-segment path counts once", () => { + assert.equal(detectFileReferences(stringMessages("/src/app/main.ts")), 1); + }); + + test("a file:line:col reference counts once", () => { + assert.equal(detectFileReferences(stringMessages("foo:12:5")), 1); + }); + + test("canonical doc names count once", () => { + assert.equal(detectFileReferences(stringMessages("README")), 1); + }); + + test("a unified-diff hunk header counts once", () => { + assert.equal(detectFileReferences(stringMessages("@@ -1,2 @@")), 1); + }); + + test("clamps to a maximum of 5", () => { + // README/CHANGELOG/TODO -> 3, diff/patch/merge -> 3 => 6 matches => min(5, 6) = 5 + assert.equal( + detectFileReferences(stringMessages("README CHANGELOG TODO diff patch merge")), + 5 + ); + }); +}); + +describe("detectErrorContext", () => { + test("no error context -> 0", () => { + assert.equal(detectErrorContext(stringMessages("hello world")), 0); + }); + + test("FINDING: single match yields 0.5 (this detector does not round)", () => { + assert.equal(detectErrorContext(stringMessages("Error")), 0.5); + }); + + test("two matches -> 2*0.5 = 1", () => { + assert.equal(detectErrorContext(stringMessages("Error Exception")), 1); + }); + + test("clamps to a maximum of 5", () => { + // 5 error names + throw/catch/finally (3) + failed/crashed (2) = 10 => 10*0.5 = 5 + assert.equal( + detectErrorContext( + stringMessages( + "Error Exception TypeError ReferenceError SyntaxError throw catch finally failed crashed" + ) + ), + 5 + ); + }); +}); + +describe("detectEnhancedContextSize", () => { + test("no messages, no systemPrompt, no tools -> 0", () => { + assert.equal(detectEnhancedContextSize({ messages: [] }), 0); + }); + + test("systemPrompt tokens count toward the total (> 1000 -> 1)", () => { + // 4004 chars -> 1001 tokens -> > 1000 -> 1 + assert.equal(detectEnhancedContextSize({ messages: [], systemPrompt: repeat(4004) }), 1); + }); + + test("larger systemPrompt climbs the ladder (> 4000 -> 3)", () => { + // 16004 chars -> 4001 tokens -> > 4000 -> 3 + assert.equal(detectEnhancedContextSize({ messages: [], systemPrompt: repeat(16004) }), 3); + }); + + test("tool definitions contribute tokens when present", () => { + // A single tool with a ~4000-char description serializes to ~1000+ tokens -> 1 + const tool = { function: { name: "x", description: repeat(4000) } }; + assert.equal(detectEnhancedContextSize({ messages: [], tools: [tool] }), 1); + }); +}); + +describe("getEnhancedSpecificityBreakdown", () => { + test("differs from getSpecificityBreakdown ONLY in contextSize", () => { + const input: RuleInput = { + messages: [{ content: "const x = 5" }], + systemPrompt: repeat(64004), // 16001 tokens -> enhanced contextSize crosses > 16000 -> 7 + }; + const plain = getSpecificityBreakdown(input); + const enhanced = getEnhancedSpecificityBreakdown(input); + + // Only messages feed detectContextSize, so plain stays at 0. + assert.equal(plain.contextSize, 0); + // systemPrompt tokens push the enhanced context to the > 16000 rung. + assert.equal(enhanced.contextSize, 7); + + // Every other dimension is computed identically. + assert.equal(plain.codeComplexity, enhanced.codeComplexity); + assert.equal(plain.mathComplexity, enhanced.mathComplexity); + assert.equal(plain.reasoningDepth, enhanced.reasoningDepth); + assert.equal(plain.toolCalling, enhanced.toolCalling); + assert.equal(plain.domainSpecificity, enhanced.domainSpecificity); + }); +});