/**
* Self-check for the palette scorer. Run with:
* node_modules/.bin/tsx src/components/command-palette/fuzzyScore.check.ts
* (tsx lives in the repo root node_modules; no test framework needed.)
*/
import assert from "node:assert/strict";
import { fuzzyScore, scoreCommand } from "./fuzzyScore";
import {
CONTEXT_WEIGHT,
FLAT_OPTION_WEIGHT,
titleRuns,
type Command,
} from "./paletteTypes";
const NO_MATCH = -1;
// Ranking order is the whole point: prefix > word-boundary > mid-word > scattered.
assert.ok(
fuzzyScore("Change status", "change") > fuzzyScore("Change status", "status"),
"prefix must outrank a later word"
);
assert.ok(
fuzzyScore("Change status", "status") > fuzzyScore("Assign to", "sgn"),
"substring must outrank a scattered subsequence"
);
assert.equal(fuzzyScore("Change status", "zzz"), NO_MATCH);
assert.equal(fuzzyScore("Change status", ""), 0, "empty query is neutral");
// A missing letter must fail even when every other letter is present in order.
assert.equal(fuzzyScore("Reply to ticket", "replyx"), NO_MATCH);
// Subsequence matching still finds acronym-ish input.
assert.ok(fuzzyScore("Change priority", "cp") > NO_MATCH);
// Word-boundary bonus: "status" after a space beats the same distance mid-word.
assert.ok(
fuzzyScore("Change status", "status") > fuzzyScore("Changestatus", "status"),
"word-boundary hit must beat a mid-word hit at the same offset"
);
// Guards against undefined titles reaching the scorer.
assert.equal(fuzzyScore(undefined as unknown as string, "a"), NO_MATCH);
// --- flat option rows vs their drill-down parent --------------------------
// The whole point of the flat rows: "urgent" must SET the priority, while
// "priority" must still lead with the picker. If these invert, Enter silently
// does the wrong one of the two.
const changePriority: Command = {
id: "ticket-priority",
title: "Change priority",
group: "Ticket",
weight: CONTEXT_WEIGHT,
keywords: "level",
};
const setUrgent: Command = {
id: "priority-Urgent",
title: "Set priority: Urgent",
group: "Ticket",
weight: FLAT_OPTION_WEIGHT,
keywords: "Urgent",
hideWhenEmpty: true,
};
const changeStatus: Command = {
id: "ticket-status",
title: "Change status",
group: "Ticket",
weight: CONTEXT_WEIGHT,
keywords: "state",
};
const setOpen: Command = {
id: "status-Open",
title: "Set status: Open",
group: "Ticket",
weight: FLAT_OPTION_WEIGHT,
keywords: "Open",
hideWhenEmpty: true,
};
assert.ok(
scoreCommand(setUrgent, "urgent") > scoreCommand(changePriority, "urgent"),
"typing an option name must lead with the row that sets it"
);
assert.ok(
scoreCommand(setOpen, "open") > scoreCommand(changeStatus, "open"),
"same for status names"
);
assert.ok(
scoreCommand(changePriority, "priority") > scoreCommand(setUrgent, "priority"),
"typing the field name must still lead with the picker"
);
assert.ok(
scoreCommand(changeStatus, "status") > scoreCommand(setOpen, "status"),
"same for status"
);
// Flat rows must clear the fixed rank server search hits carry (~950), or a
// ticket whose subject contains "urgent" buries the row that sets it.
const TOP_SEARCH_RANK = 950;
assert.ok(
scoreCommand(setUrgent, "urgent") > TOP_SEARCH_RANK,
"flat option rows must outrank server search hits"
);
// Hidden flat rows need a deliberate (substring-or-better) match: "sett" is a
// scattered subsequence of "Set status: …"/"Set priority: …", so it would
// un-hide every option row at once and bury Settings' exact prefix match.
assert.equal(scoreCommand(setUrgent, "sett"), NO_MATCH);
assert.equal(scoreCommand(setOpen, "sett"), NO_MATCH);
assert.ok(
scoreCommand(setOpen, "set status") > NO_MATCH,
"a deliberate prefix still surfaces the flat rows"
);
assert.ok(
scoreCommand(changeStatus, "chst") > NO_MATCH,
"normal rows keep scattered abbreviation matching"
);
// An empty query scores 0 everywhere, which is exactly why hideWhenEmpty exists
// rather than a low weight — weight cannot separate rows that all tie at 0.
assert.equal(scoreCommand(setUrgent, ""), 0);
assert.equal(scoreCommand(changePriority, ""), 0);
// Fixed ranks bypass the scorer entirely.
assert.equal(
scoreCommand({ ...setUrgent, rank: 42 }, "nomatchwhatsoever"),
42,
"rank must win over the fuzzy path"
);
// --- a root row must not outrank the category it belongs to ----------------
// "Saved Replies" leads with the word both rows share, which is what keeps the
// category on top. The rule this pins: a category title must not bury its own
// distinguishing word mid-title, because prefix (1000) vs word-boundary (~950)
// is a wider gap than CONTEXT_WEIGHT 1.2 vs FLAT_OPTION_WEIGHT 1.15 can close.
const savedReplies: Command = {
id: "ticket-saved-reply",
title: "Saved Replies",
group: "Ticket",
weight: CONTEXT_WEIGHT,
keywords: "reply template canned macro snippet",
};
const oneSavedReply: Command = {
id: "saved-reply-flat-x",
title: "Refund policy",
subtitle: "Saved Reply",
group: "Ticket",
weight: FLAT_OPTION_WEIGHT,
hideWhenEmpty: true,
};
for (const term of ["saved", "saved repl", "reply", "canned"]) {
assert.ok(
scoreCommand(savedReplies, term) > scoreCommand(oneSavedReply, term),
`"${term}" must lead with the category, not with one saved reply`
);
}
// "reply" is not a substring of "replies", so a plural title cannot be found by
// the singular the agent actually types — it has to be carried in keywords.
assert.equal(
scoreCommand({ ...savedReplies, keywords: undefined }, "reply"),
-1,
"documents why 'reply' is in the keywords and not left to the title"
);
// ...while the reply's own words still reach it directly, which is its whole job.
for (const term of ["refund", "polic"]) {
assert.ok(
scoreCommand(oneSavedReply, term) > scoreCommand(savedReplies, term),
`"${term}" must reach the reply itself`
);
}
// The hazard this arrangement avoids, worth pinning because it is not obvious:
// a category row that carries its distinguishing word *mid-title* loses to a
// root row that leads with it. Prefix (1000) vs word-boundary (~950) is a bigger
// gap than CONTEXT_WEIGHT 1.2 vs FLAT_OPTION_WEIGHT 1.15 can close.
const midTitleCategory: Command = { ...savedReplies, title: "Insert saved reply" };
const titlePrefixedRow: Command = {
...oneSavedReply,
title: "Saved Reply: Refund policy",
};
assert.ok(
scoreCommand(titlePrefixedRow, "saved") > scoreCommand(midTitleCategory, "saved"),
"a mid-title category is buried by a root row that leads with the same word"
);
// Leading with that word is what makes the category safe — both score a prefix
// match, so the weights decide and the category wins. Note this means the
// subtitle is chosen for readability, not to escape the hazard above: a
// "Saved reply: X" title would rank correctly under "Saved replies" too.
assert.ok(
scoreCommand(savedReplies, "saved") > scoreCommand(titlePrefixedRow, "saved"),
"a category leading with the shared word outranks a root row on weight alone"
);
// --- server highlighting -------------------------------------------------
// Lives here rather than in its own file: same shape of pure, import-free helper.
assert.deepEqual(
titleRuns("Cannot login to portal"),
[
{ text: "Cannot ", match: false },
{ text: "login", match: true },
{ text: " to portal", match: false },
],
"runs alternate around the marked text"
);
assert.deepEqual(
titleRuns("Refund for order"),
[
{ text: "Refund", match: true },
{ text: " for ", match: false },
{ text: "order", match: true },
],
"several marks in one title"
);
assert.deepEqual(
titleRuns("Nothing highlighted"),
[{ text: "Nothing highlighted", match: false }],
"an unhighlighted title is one plain run"
);
// Empty runs would render as stray nodes between adjacent marks.
assert.deepEqual(
titleRuns("ab"),
[
{ text: "a", match: true },
{ text: "b", match: true },
],
"adjacent marks leave no empty run between them"
);
assert.deepEqual(titleRuns(""), [], "empty in, empty out");
// The whole reason for rendering runs as text nodes: markup in ticket content
// must come out inert, never as elements.
assert.deepEqual(
titleRuns(" and hi"),
[
{ text: "alert(1) and ", match: false },
{ text: "hi", match: true },
],
"other tags are stripped, not rendered"
);
console.log("fuzzyScore: all checks passed");