From 651ed6eac0ccb745763c2219f59433f8f9bf162b Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Mon, 2 Mar 2026 18:15:20 -0700 Subject: [PATCH] test: add prompt_score tests + fix scope scoring bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 14 tests for scorePrompt covering all scoring dimensions - Fix bug: long prompts (>100 chars) no longer get full scope score just for being verbose — now requires domain terms (file/function/etc) - Export scorePrompt for testability --- src/tools/prompt-score.ts | 7 ++- tests/tools/prompt-score.test.ts | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 tests/tools/prompt-score.test.ts diff --git a/src/tools/prompt-score.ts b/src/tools/prompt-score.ts index 1cecf01..1795901 100644 --- a/src/tools/prompt-score.ts +++ b/src/tools/prompt-score.ts @@ -40,7 +40,7 @@ interface ScoreResult { feedback: string[]; } -function scorePrompt(text: string): ScoreResult { +export function scorePrompt(text: string): ScoreResult { const feedback: string[] = []; let specificity: number; let scope: number; @@ -59,8 +59,11 @@ function scorePrompt(text: string): ScoreResult { } // Scope: bounded task - if (/\b(only|just|single|one|specific|this)\b/i.test(text) || text.length > 100) { + if (/\b(only|just|single|one|specific|this)\b/i.test(text)) { scope = 25; + } else if (text.length > 100 && /\b(file|function|component|module|test|route|endpoint)\b/i.test(text)) { + scope = 20; + feedback.push("🎯 Scope seems bounded — use 'only/just/specific' to be explicit for +5"); } else if (/\b(all|every|entire|whole)\b/i.test(text)) { scope = 10; feedback.push("🎯 'All/every' is broad — can you narrow the scope?"); diff --git a/tests/tools/prompt-score.test.ts b/tests/tools/prompt-score.test.ts new file mode 100644 index 0000000..9cab1ca --- /dev/null +++ b/tests/tools/prompt-score.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from "vitest"; +import { scorePrompt } from "../../src/tools/prompt-score.js"; + +describe("scorePrompt", () => { + it("gives high score to well-crafted prompt", () => { + const result = scorePrompt( + "Rename the `getUserById` function in `src/lib/users.ts` to `fetchUser` and update only the call sites in this module. It should pass existing tests." + ); + expect(result.total).toBeGreaterThanOrEqual(80); + expect(result.grade).toMatch(/^[AB]/); + expect(result.specificity).toBe(25); + expect(result.actionability).toBe(25); + }); + + it("gives low score to vague prompt", () => { + const result = scorePrompt("make it better"); + expect(result.total).toBeLessThanOrEqual(40); + expect(result.grade).toMatch(/^[DF]/); + expect(result.feedback.length).toBeGreaterThan(0); + }); + + it("detects file paths for specificity", () => { + const result = scorePrompt("fix src/index.ts"); + expect(result.specificity).toBe(25); + }); + + it("detects backtick identifiers for specificity", () => { + const result = scorePrompt("refactor `handleClick` to use async"); + expect(result.specificity).toBe(25); + }); + + it("gives partial specificity for generic type references", () => { + const result = scorePrompt("update the component"); + expect(result.specificity).toBe(15); + }); + + it("penalizes broad scope words", () => { + const result = scorePrompt("fix all the bugs"); + expect(result.scope).toBe(10); + expect(result.feedback.some(f => f.includes("broad"))).toBe(true); + }); + + it("rewards bounded scope words", () => { + const result = scorePrompt("fix only this specific function"); + expect(result.scope).toBe(25); + }); + + it("does NOT give full scope score just for being long", () => { + // Regression: previously any prompt >100 chars got scope=25 + const longVague = "I need you to look at the code and think about what could be improved and then maybe do something about it if you think that makes sense to you right now"; + const result = scorePrompt(longVague); + expect(result.scope).toBeLessThan(25); + }); + + it("rewards specific action verbs", () => { + const result = scorePrompt("extract the validation logic"); + expect(result.actionability).toBe(25); + }); + + it("gives partial score for vague verbs", () => { + const result = scorePrompt("improve the code"); + expect(result.actionability).toBe(15); + }); + + it("detects done conditions", () => { + const result = scorePrompt("it should return null on empty input"); + expect(result.doneCondition).toBe(25); + }); + + it("treats questions as verifiable", () => { + const result = scorePrompt("why does this function throw on empty input?"); + expect(result.doneCondition).toBe(20); + }); + + it("returns correct grade boundaries", () => { + // A+ requires ≥90 + const perfect = scorePrompt( + "Rename only `src/lib/auth.ts` export `validateToken` to `verifyToken`. Tests should pass." + ); + expect(perfect.total).toBeGreaterThanOrEqual(90); + expect(perfect.grade).toBe("A+"); + }); + + it("gives encouraging feedback on perfect score", () => { + const result = scorePrompt( + "Add just one test to `src/utils.test.ts` that asserts `parseDate` returns null for empty string" + ); + if (result.total >= 90) { + expect(result.feedback.some(f => f.includes("Excellent"))).toBe(true); + } + }); +});