1/**
2 * Self-check for saved-reply ordering and promotion. Run with:
3 * node_modules/.bin/tsx src/components/command-palette/savedReplyRanking.check.ts
4 * (tsx lives in the repo root node_modules; no test framework needed.)
5 */
6import assert from "node:assert/strict";
7import {
8 mostUsedFirst,
9 topSavedReplies,
10 withUse,
11 withoutReply,
12 type SavedReplyUsage,
13} from "./savedReplyRanking";
14
15const usage: SavedReplyUsage = {
16 refund: { title: "Refund policy", count: 12 },
17 reset: { title: "Password reset", count: 6 },
18 escalate: { title: "Escalation notice", count: 5 },
19 shipping: { title: "Shipping delay", count: 4 },
20};
21
22// --- ordering ------------------------------------------------------------
23
24const list = [
25 { name: "shipping" },
26 { name: "refund" },
27 { name: "never-used" },
28 { name: "reset" },
29];
30
31assert.deepEqual(
32 mostUsedFirst(list, usage).map((reply) => reply.name),
33 ["refund", "reset", "shipping", "never-used"],
34 "most-used first"
35);
36
37// Stability is what makes cold start sane: with no history at all the list must
38// come back in the order the server sent it (modified desc), not reshuffled.
39assert.deepEqual(
40 mostUsedFirst(list, {}).map((reply) => reply.name),
41 ["shipping", "refund", "never-used", "reset"],
42 "no usage must preserve source order"
43);
44
45// Ties must not reorder either, or the drill-down would shuffle between opens.
46assert.deepEqual(
47 mostUsedFirst(list, {
48 shipping: { title: "s", count: 3 },
49 reset: { title: "r", count: 3 },
50 }).map((reply) => reply.name),
51 ["shipping", "reset", "refund", "never-used"],
52 "equal counts must preserve source order"
53);
54
55// mostUsedFirst must not mutate its input: the caller holds the resource's data.
56const original = [{ name: "shipping" }, { name: "refund" }];
57mostUsedFirst(original, usage);
58assert.deepEqual(
59 original.map((reply) => reply.name),
60 ["shipping", "refund"],
61 "must not sort the caller's array in place"
62);
63
64// --- promotion to a root row ---------------------------------------------
65
66assert.deepEqual(
67 topSavedReplies(usage, 5, 3).map((reply) => reply.name),
68 ["refund", "reset", "escalate"],
69 "top rows are the highest counts at or above the threshold"
70);
71
72// The threshold is inclusive at exactly minUses, and excludes just below it.
73assert.deepEqual(
74 topSavedReplies(usage, 5, 99).map((reply) => reply.name),
75 ["refund", "reset", "escalate"],
76 "count === minUses qualifies, count < minUses does not"
77);
78
79// The whole point of earning the row: long-tail usage yields no rows at all,
80// rather than promoting whatever happens to be least-rare.
81assert.deepEqual(
82 topSavedReplies({ a: { title: "A", count: 2 }, b: { title: "B", count: 1 } }, 5, 3),
83 [],
84 "long-tail usage must promote nothing"
85);
86
87assert.deepEqual(topSavedReplies({}, 5, 3), [], "no history promotes nothing");
88
89assert.equal(
90 topSavedReplies(usage, 1, 2).length,
91 2,
92 "maxRows caps the row count"
93);
94
95// Titles ride along so a root row needs no list fetch to render.
96assert.equal(
97 topSavedReplies(usage, 5, 1)[0]?.title,
98 "Refund policy",
99 "the stored title comes back with the row"
100);
101
102// --- recording -----------------------------------------------------------
103
104assert.equal(withUse({}, "refund", "Refund policy").refund?.count, 1);
105assert.equal(withUse(usage, "refund", "Refund policy").refund?.count, 13);
106
107// A rename is picked up on next use rather than needing a migration.
108assert.equal(
109 withUse(usage, "refund", "Refund policy v2").refund?.title,
110 "Refund policy v2",
111 "the latest title wins"
112);
113
114assert.equal(usage.refund?.count, 12, "withUse must not mutate");
115
116assert.ok(
117 !("refund" in withoutReply(usage, "refund")),
118 "a deleted reply is dropped"
119);
120assert.equal(usage.refund?.count, 12, "withoutReply must not mutate");
121
122// A reply that just fell below the threshold must lose its row immediately.
123assert.deepEqual(
124 topSavedReplies(withoutReply(usage, "refund"), 5, 3).map((r) => r.name),
125 ["reset", "escalate"],
126 "dropping a reply removes its root row"
127);
128
129console.log("savedReplyRanking: all checks passed");
130