@pi-unipi/command-enchantment 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -24,7 +24,9 @@
24
24
  ],
25
25
  "files": [
26
26
  "src/**/*.ts",
27
- "README.md"
27
+ "README.md",
28
+ "!src/__tests__/**",
29
+ "!src/**/*.test.ts"
28
30
  ],
29
31
  "peerDependencies": {
30
32
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,120 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { existsSync, globSync, readFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
-
5
- function findRepoRoot(start: string): string {
6
- let dir = start;
7
- while (dir !== dirname(dir)) {
8
- if (existsSync(join(dir, "packages", "autocomplete", "src", "constants.ts"))) return dir;
9
- dir = dirname(dir);
10
- }
11
- return start;
12
- }
13
-
14
- const root = findRepoRoot(process.cwd());
15
-
16
- function read(path: string): string {
17
- return readFileSync(join(root, path), "utf-8");
18
- }
19
-
20
- function collectConstants(): Map<string, string> {
21
- const constants = new Map<string, string>([["UNIPI_PREFIX", "unipi:"]]);
22
-
23
- for (const path of globSync("packages/**/*.ts", { cwd: root })) {
24
- const text = read(path);
25
- for (const obj of text.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\{([\s\S]*?)\}\s*as\s+const/g)) {
26
- const [, name, body] = obj;
27
- for (const item of body.matchAll(/(\w+):\s*"([^"]+)"/g)) {
28
- constants.set(`${name}.${item[1]}`, item[2]);
29
- }
30
- }
31
- }
32
-
33
- return constants;
34
- }
35
-
36
- function evaluateCommandExpression(expr: string, constants: Map<string, string>): string | null {
37
- const trimmed = expr.trim();
38
-
39
- if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
40
- const quote = trimmed[0];
41
- const end = trimmed.indexOf(quote, 1);
42
- return end > 0 ? trimmed.slice(1, end) : null;
43
- }
44
-
45
- if (trimmed.startsWith("`")) {
46
- const end = trimmed.lastIndexOf("`");
47
- if (end <= 0) return null;
48
- return trimmed.slice(1, end).replace(/\$\{([^}]+)\}/g, (_match, key: string) => {
49
- const resolved = constants.get(key.trim());
50
- return resolved ?? `\${${key}}`;
51
- });
52
- }
53
-
54
- return constants.get(trimmed) ?? null;
55
- }
56
-
57
- function registeredCommands(): { commands: Set<string>; nonUnipi: string[]; unresolved: string[] } {
58
- const constants = collectConstants();
59
- const commands = new Set<string>();
60
- const nonUnipi: string[] = [];
61
- const unresolved: string[] = [];
62
-
63
- for (const path of globSync("packages/**/*.ts", { cwd: root }).sort()) {
64
- const text = read(path);
65
- if (!text.includes("registerCommand")) continue;
66
-
67
- for (const match of text.matchAll(/\.registerCommand\(\s*([^,\n]+)/g)) {
68
- const expr = match[1].trim();
69
-
70
- // Workflow registers a loop over WORKFLOW_COMMANDS via local `fullCommand`.
71
- if (path === "packages/workflow/commands.ts" && expr === "fullCommand") {
72
- for (const [key, value] of constants) {
73
- if (key.startsWith("WORKFLOW_COMMANDS.")) commands.add(`unipi:${value}`);
74
- }
75
- continue;
76
- }
77
-
78
- const command = evaluateCommandExpression(expr, constants);
79
- if (!command || command.includes("${")) {
80
- unresolved.push(`${path}: ${expr}`);
81
- continue;
82
- }
83
-
84
- if (command.startsWith("unipi:")) {
85
- commands.add(command);
86
- } else {
87
- nonUnipi.push(`${command} (${path})`);
88
- }
89
- }
90
- }
91
-
92
- return { commands, nonUnipi, unresolved };
93
- }
94
-
95
- function autocompleteRegistry(): { registry: Set<string>; descriptions: Set<string>; registryPackages: Set<string>; labels: Set<string> } {
96
- const text = read("packages/autocomplete/src/constants.ts");
97
- const registryBody = text.match(/export const COMMAND_REGISTRY[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
98
- const descriptionsBody = text.match(/export const COMMAND_DESCRIPTIONS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
99
- const labelsBody = text.match(/export const PACKAGE_LABELS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
100
-
101
- const registry = new Set([...registryBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
102
- const descriptions = new Set([...descriptionsBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
103
- const registryPackages = new Set([...registryBody.matchAll(/"unipi:[^"]+"\s*:\s*"([^"]+)"/g)].map((m) => m[1]));
104
- const labels = new Set([...labelsBody.matchAll(/^\s*"?([a-z][a-z0-9-]*)"?:\s*"/gm)].map((m) => m[1]));
105
-
106
- return { registry, descriptions, registryPackages, labels };
107
- }
108
-
109
- describe("autocomplete command registry audit", () => {
110
- it("mirrors every registered /unipi:* command and has no non-unipi package commands", () => {
111
- const registered = registeredCommands();
112
- const autocomplete = autocompleteRegistry();
113
-
114
- expect(registered.unresolved, "unresolved registerCommand expressions").toEqual([]);
115
- expect(registered.nonUnipi, "package commands must use the unipi: prefix").toEqual([]);
116
- expect([...registered.commands].sort()).toEqual([...autocomplete.registry].sort());
117
- expect([...autocomplete.registry].sort()).toEqual([...autocomplete.descriptions].sort());
118
- expect([...autocomplete.registryPackages].filter((pkg) => !autocomplete.labels.has(pkg)).sort()).toEqual([]);
119
- });
120
- });
@@ -1,423 +0,0 @@
1
- /**
2
- * Tests for the 4-tier autocomplete command sorting model.
3
- *
4
- * Exercises crossItemPriority() and sortTaggedItems() from sorting.ts.
5
- * These are pure functions extracted from the provider closure for testability.
6
- */
7
-
8
- import { describe, it, expect } from "vitest";
9
- import {
10
- crossItemPriority,
11
- sortTaggedItems,
12
- type TaggedItem,
13
- } from "../sorting.js";
14
- import type { AutocompleteItem } from "@earendil-works/pi-tui";
15
-
16
- // ─── Helpers ─────────────────────────────────────────────────────────
17
-
18
- /** Shorthand to create an AutocompleteItem. */
19
- function item(value: string, label?: string): AutocompleteItem {
20
- return { value, label: label ?? value, description: "" };
21
- }
22
-
23
- /** Shorthand to create a TaggedItem. */
24
- function tagged(value: string, isUnipi: boolean, label?: string): TaggedItem {
25
- return { item: item(value, label), isUnipi };
26
- }
27
-
28
- /** Extract just the values from a sorted TaggedItem[] for easy assertions. */
29
- function values(sorted: TaggedItem[]): string[] {
30
- return sorted.map((t) => t.item.value);
31
- }
32
-
33
- // ═══════════════════════════════════════════════════════════════════════
34
- // crossItemPriority — tier assignment
35
- // ═══════════════════════════════════════════════════════════════════════
36
-
37
- describe("crossItemPriority", () => {
38
- // ── Tier 0: exact full-value match ──────────────────────────────────
39
-
40
- it("assigns tier 0 for exact base command match", () => {
41
- // query "new" matches item value "new" exactly
42
- expect(crossItemPriority(item("new"), false, "new")).toBe(0);
43
- });
44
-
45
- it("assigns tier 0 for exact unipi full-value match", () => {
46
- // query "unipi:brainstorm" matches item value "unipi:brainstorm"
47
- expect(crossItemPriority(item("unipi:brainstorm"), true, "unipi:brainstorm")).toBe(0);
48
- });
49
-
50
- it("does NOT assign tier 0 for short-name-only match on unipi item", () => {
51
- // query "brainstorm" is not the full value "unipi:brainstorm"
52
- expect(crossItemPriority(item("unipi:brainstorm"), true, "brainstorm")).not.toBe(0);
53
- });
54
-
55
- // ── Tier 1: unipi short-name exact match ────────────────────────────
56
-
57
- it("assigns tier 1 when unipi short name matches query exactly", () => {
58
- expect(crossItemPriority(item("unipi:brainstorm"), true, "brainstorm")).toBe(1);
59
- });
60
-
61
- it("does NOT assign tier 1 for non-unipi items", () => {
62
- // "new" is not a unipi item, so short === full === "new", which is tier 0 (exact)
63
- expect(crossItemPriority(item("new"), false, "new")).toBe(0);
64
- });
65
-
66
- it("does NOT assign tier 1 when unipi short name doesn't match", () => {
67
- expect(crossItemPriority(item("unipi:brainstorm"), true, "brain")).not.toBeLessThanOrEqual(1);
68
- });
69
-
70
- // ── Tier 2: prefix match ────────────────────────────────────────────
71
-
72
- it("assigns tier 2 for base prefix match", () => {
73
- expect(crossItemPriority(item("work"), false, "wor")).toBe(2);
74
- });
75
-
76
- it("assigns tier 2 for unipi short-name prefix match", () => {
77
- // short = "work", query = "wor" → startsWith → tier 2
78
- expect(crossItemPriority(item("unipi:work"), true, "wor")).toBe(2);
79
- });
80
-
81
- it("assigns tier 2 when full value has prefix (unipi: prefix)", () => {
82
- // full = "unipi:compact", query = "unipi:co" → startsWith → tier 2
83
- expect(crossItemPriority(item("unipi:compact"), true, "unipi:co")).toBe(2);
84
- });
85
-
86
- // ── Tier 3: fuzzy fallback ──────────────────────────────────────────
87
-
88
- it("assigns tier 3 for fuzzy-only match", () => {
89
- // "review-work" doesn't start with "wor" but contains characters
90
- expect(crossItemPriority(item("unipi:review-work"), true, "wor")).toBe(3);
91
- });
92
-
93
- it("assigns tier 3 for character subsequence match", () => {
94
- // "fix" is a subsequence of... no wait, let's use "fx" vs "fix"
95
- // "fix" contains f, i, x. query "fx" → f...x → subsequence → tier 3
96
- expect(crossItemPriority(item("fix"), false, "fx")).toBe(3);
97
- });
98
-
99
- it("assigns tier 3 for non-matching query (all items get tier 3)", () => {
100
- expect(crossItemPriority(item("brainstorm"), false, "xyz")).toBe(3);
101
- });
102
-
103
- // ── Case insensitivity ──────────────────────────────────────────────
104
-
105
- it("matches case-insensitively (tier 0)", () => {
106
- expect(crossItemPriority(item("new"), false, "New")).toBe(0);
107
- expect(crossItemPriority(item("New"), false, "new")).toBe(0);
108
- });
109
-
110
- it("matches case-insensitively (tier 2)", () => {
111
- expect(crossItemPriority(item("Compact"), false, "co")).toBe(2);
112
- });
113
-
114
- it("matches case-insensitively (tier 1)", () => {
115
- expect(crossItemPriority(item("unipi:Brainstorm"), true, "brainstorm")).toBe(1);
116
- });
117
- });
118
-
119
- // ═══════════════════════════════════════════════════════════════════════
120
- // sortTaggedItems — cross-group merge sort
121
- // ═══════════════════════════════════════════════════════════════════════
122
-
123
- describe("sortTaggedItems", () => {
124
- // ── Test 1: Exact base command before fuzzy unipi ───────────────────
125
-
126
- it("ranks exact base command (tier 0) before fuzzy unipi (tier 3)", () => {
127
- const items = [
128
- tagged("unipi:review-work", true), // tier 3 (r→e→w fuzzy)
129
- tagged("new", false), // tier 0 (exact)
130
- ];
131
-
132
- const sorted = sortTaggedItems(items, "new");
133
- expect(values(sorted)).toEqual(["new", "unipi:review-work"]);
134
- });
135
-
136
- // ── Test 2: Unipi short-name exact (tier 1) ────────────────────────
137
-
138
- it("ranks unipi short-name exact (tier 1) above prefix and fuzzy", () => {
139
- const items = [
140
- tagged("unipi:brainstorm", true), // tier 1
141
- tagged("unipi:plan", true), // tier 3 (no b-r-a-i-n)
142
- ];
143
-
144
- const sorted = sortTaggedItems(items, "brainstorm");
145
- expect(sorted[0].item.value).toBe("unipi:brainstorm");
146
- expect(crossItemPriority(items[0].item, true, "brainstorm")).toBe(1);
147
- });
148
-
149
- // ── Test 3: Full unipi exact (tier 0) ──────────────────────────────
150
-
151
- it("ranks full unipi: value exact match as tier 0", () => {
152
- const items = [
153
- tagged("unipi:brainstorm", true),
154
- tagged("brainstorm", false), // also tier 0 exact on value
155
- ];
156
-
157
- const sorted = sortTaggedItems(items, "unipi:brainstorm");
158
- // Both could be tier 0 or fuzzy. unipi:brainstorm is tier 0 (exact full).
159
- // "brainstorm" as base: full "brainstorm" !== "unipi:brainstorm" → not tier 0
160
- // full.startsWith query? "brainstorm".startsWith("unipi:brainstorm") → no
161
- // So "brainstorm" gets tier 3
162
- expect(sorted[0].item.value).toBe("unipi:brainstorm");
163
- });
164
-
165
- // ── Test 4: Prefix match ordering (non-unipi before unipi) ─────────
166
-
167
- it("within same tier, sorts non-unipi before unipi", () => {
168
- const items = [
169
- tagged("unipi:work", true), // tier 2 (prefix "wor" → "work")
170
- tagged("work", false), // tier 2 (prefix "wor" → "work")
171
- ];
172
-
173
- const sorted = sortTaggedItems(items, "wor");
174
- expect(values(sorted)).toEqual(["work", "unipi:work"]);
175
- });
176
-
177
- // ── Test 5: Fuzzy sorted by similarity (shorter name first) ────────
178
-
179
- it("within tier 3 fuzzy, shorter names sort first", () => {
180
- const items = [
181
- tagged("unipi:review-work", true), // longer
182
- tagged("fix", false), // shorter
183
- tagged("unipi:fix", true), // medium
184
- ];
185
-
186
- const sorted = sortTaggedItems(items, "fx");
187
- // All tier 3 fuzzy. Sorted by: non-unipi first, then by length.
188
- // "fix" (len 3, non-unipi) → "unipi:fix" (len 9, unipi) → "unipi:review-work" (len 17, unipi)
189
- expect(values(sorted)).toEqual(["fix", "unipi:fix", "unipi:review-work"]);
190
- });
191
-
192
- // ── Test 6: Mixed tiers ────────────────────────────────────────────
193
-
194
- it("sorts across mixed tiers: exact → prefix → fuzzy", () => {
195
- const items = [
196
- tagged("unipi:compact", true), // tier 2 (prefix "co")
197
- tagged("compact-xyz", false), // tier 2 (prefix "co")
198
- tagged("co", false), // tier 0 (exact "co")
199
- tagged("unipi:consultant", true), // tier 2 (prefix "co")
200
- tagged("config", false), // tier 2 (prefix "co")
201
- tagged("unipi:chore-create", true), // tier 3 (fuzzy c→o)
202
- ];
203
-
204
- const sorted = sortTaggedItems(items, "co");
205
- // Tier 0: co
206
- // Tier 2: compact-xyz, config (non-unipi) then unipi:compact, unipi:consultant (unipi)
207
- // Tier 3: unipi:chore-create
208
- const result = values(sorted);
209
- expect(result[0]).toBe("co");
210
- // Tier 2 non-unipi
211
- expect(result.slice(1, 3)).toEqual(expect.arrayContaining(["compact-xyz", "config"]));
212
- // Tier 2 unipi
213
- expect(result.slice(3, 5)).toEqual(expect.arrayContaining(["unipi:compact", "unipi:consultant"]));
214
- // Tier 3
215
- expect(result[5]).toBe("unipi:chore-create");
216
- });
217
-
218
- // ── Test 7: No base command match, only unipi ──────────────────────
219
-
220
- it("handles unipi-only matches (tier 1 short-name exact)", () => {
221
- const items = [
222
- tagged("unipi:ralph", true), // tier 1 (short "ralph" === "ralph")
223
- tagged("unipi:ralph-stop", true), // tier 2 (prefix "ralph")
224
- ];
225
-
226
- const sorted = sortTaggedItems(items, "ralph");
227
- expect(values(sorted)).toEqual(["unipi:ralph", "unipi:ralph-stop"]);
228
- });
229
-
230
- // ── Test 8: Skill commands filtered (simulated: skill: items present) ─
231
-
232
- it("sortItems does not filter skill commands — that's the provider's job", () => {
233
- // The sorting function itself is pure — it just sorts whatever it receives.
234
- // Skill filtering happens at the provider level. Verify sorting handles them.
235
- // Note: "skill:animate" does NOT start with "a", so it gets tier 3 (fuzzy).
236
- // "unipi:auto" short name "auto" starts with "a" → tier 2 (prefix).
237
- const items = [
238
- tagged("skill:animate", false),
239
- tagged("skill:audit", false),
240
- tagged("unipi:auto", true),
241
- ];
242
-
243
- const sorted = sortTaggedItems(items, "a");
244
- // unipi:auto (tier 2) before skill: items (tier 3)
245
- // Within tier 3: non-unipi, sorted by length: audit(11) < animate(13)
246
- expect(values(sorted)).toEqual(["unipi:auto", "skill:audit", "skill:animate"]);
247
- });
248
-
249
- // ── Test 9: Skill query scenario ───────────────────────────────────
250
-
251
- it("when skill: prefix items are present, they sort by tier like any other", () => {
252
- // "skill:work" does NOT start with "work" → tier 3 (fuzzy)
253
- // "unipi:work" short name "work" starts with "work" → tier 2 (prefix)
254
- const items = [
255
- tagged("skill:work", false), // tier 3 (fuzzy)
256
- tagged("unipi:work", true), // tier 2 (prefix)
257
- tagged("skill:workshop", false), // tier 3 (fuzzy)
258
- ];
259
-
260
- const sorted = sortTaggedItems(items, "work");
261
- // unipi:work (tier 2) before skill items (tier 3)
262
- // Within tier 3: non-unipi, sorted by length: skill:work(11) < skill:workshop(15)
263
- expect(values(sorted)).toEqual(["unipi:work", "skill:work", "skill:workshop"]);
264
- });
265
-
266
- // ── Test 10: Empty query ───────────────────────────────────────────
267
-
268
- it("empty query gives all items tier 0 (exact on empty string)", () => {
269
- // When query is "", every value matches exactly: "".startsWith("") → true
270
- // Actually: full === "" is false for non-empty values. But "".startsWith("") is true → tier 2
271
- // Wait: full === q where q = "". If value is "new", full="new" !== "" → not tier 0.
272
- // short.startsWith("") → true → tier 2.
273
- // So all items get tier 2 when query is "".
274
- // Within tier 2: non-unipi first, then stable.
275
- const items = [
276
- tagged("unipi:brain", true),
277
- tagged("new", false),
278
- tagged("unipi:work", true),
279
- tagged("fix", false),
280
- ];
281
-
282
- const sorted = sortTaggedItems(items, "");
283
- // All tier 2. Non-unipi first (preserving original order), then unipi.
284
- expect(values(sorted)).toEqual(["new", "fix", "unipi:brain", "unipi:work"]);
285
- });
286
-
287
- // ── Test 11: Case insensitive ──────────────────────────────────────
288
-
289
- it("matches case-insensitively across tiers", () => {
290
- const items = [
291
- tagged("unipi:Brainstorm", true),
292
- tagged("New", false),
293
- ];
294
-
295
- // query "/New" stripped → "new"
296
- const sorted = sortTaggedItems(items, "new");
297
- expect(sorted[0].item.value).toBe("New"); // tier 0 exact (case-insensitive)
298
-
299
- // query "BRAINSTORM" → unipi short-name "brainstorm" exact (case-insensitive)
300
- const sorted2 = sortTaggedItems(items, "BRAINSTORM");
301
- expect(sorted2[0].item.value).toBe("unipi:Brainstorm"); // tier 1
302
- });
303
-
304
- // ── Test 12: Stable sort ───────────────────────────────────────────
305
-
306
- it("preserves original order for items at same tier and same source", () => {
307
- const items = [
308
- tagged("alpha", false), // tier 2 (prefix "a")
309
- tagged("unipi:a-first", true), // tier 2
310
- tagged("beta", false), // tier 2
311
- tagged("unipi:a-second", true), // tier 2
312
- tagged("gamma", false), // tier 2
313
- ];
314
-
315
- const sorted = sortTaggedItems(items, "a");
316
- const result = values(sorted);
317
-
318
- // Non-unipi items preserve their original relative order: alpha, beta, gamma
319
- const nonUnipi = result.filter((v) => !v.startsWith("unipi:"));
320
- expect(nonUnipi).toEqual(["alpha", "beta", "gamma"]);
321
-
322
- // Unipi items preserve their original relative order: a-first, a-second
323
- const unipiItems = result.filter((v) => v.startsWith("unipi:"));
324
- expect(unipiItems).toEqual(["unipi:a-first", "unipi:a-second"]);
325
- });
326
-
327
- // ── Test 13: btw:new scenario ──────────────────────────────────────
328
-
329
- it("btw:new gets tier 3 fuzzy for query 'new' (not prefix match)", () => {
330
- // "btw:new" does NOT start with "new" → not tier 2
331
- // "btw:new" !== "new" → not tier 0
332
- // fuzzy match: b...t...w...:...n...e...w → "new" is subsequence → tier 3
333
- const items = [
334
- tagged("btw:new", false), // tier 3
335
- tagged("new", false), // tier 0
336
- tagged("unipi:new-thing", true), // tier 2 (prefix "new")
337
- ];
338
-
339
- const sorted = sortTaggedItems(items, "new");
340
- expect(values(sorted)).toEqual(["new", "unipi:new-thing", "btw:new"]);
341
- });
342
-
343
- it("verifies btw:new is tier 3 for query 'new'", () => {
344
- expect(crossItemPriority(item("btw:new"), false, "new")).toBe(3);
345
- });
346
-
347
- it("verifies 'new' is tier 0 for query 'new'", () => {
348
- expect(crossItemPriority(item("new"), false, "new")).toBe(0);
349
- });
350
- });
351
-
352
- // ═══════════════════════════════════════════════════════════════════════
353
- // Additional edge cases
354
- // ═══════════════════════════════════════════════════════════════════════
355
-
356
- describe("edge cases", () => {
357
- it("handles single item", () => {
358
- const items = [tagged("new", false)];
359
- const sorted = sortTaggedItems(items, "new");
360
- expect(values(sorted)).toEqual(["new"]);
361
- });
362
-
363
- it("handles empty input array", () => {
364
- const sorted = sortTaggedItems([], "new");
365
- expect(sorted).toEqual([]);
366
- });
367
-
368
- it("does not mutate the original array", () => {
369
- const original = [tagged("b", false), tagged("a", false)];
370
- const copy = [...original];
371
- sortTaggedItems(original, "a");
372
- expect(original).toEqual(copy);
373
- });
374
-
375
- it("sorts multiple tier 0 exact matches (non-unipi before unipi)", () => {
376
- const items = [
377
- tagged("unipi:abc", true), // tier 0 (full value "unipi:abc" === query)
378
- tagged("unipi:abc", false), // tier 0 (but isUnipi=false, value doesn't start with "unipi:")
379
- ];
380
-
381
- // Both have value "unipi:abc", but one is tagged isUnipi=true
382
- // Wait: the non-unipi one has value "unipi:abc" — that's unusual but possible
383
- // crossItemPriority: full="unipi:abc" === "unipi:abc" → tier 0 for both
384
- // Same tier → non-unipi first
385
- const sorted = sortTaggedItems(items, "unipi:abc");
386
- expect(sorted[0].isUnipi).toBe(false);
387
- expect(sorted[1].isUnipi).toBe(true);
388
- });
389
-
390
- it("tier 3 sorts by length, non-unipi first within same length", () => {
391
- const items = [
392
- tagged("unipi:ab", true), // len 9, tier 3
393
- tagged("cd", false), // len 2, tier 3
394
- tagged("ef", false), // len 2, tier 3
395
- tagged("unipi:gh", true), // len 9, tier 3
396
- ];
397
-
398
- const sorted = sortTaggedItems(items, "z");
399
- // All tier 3. Non-unipi first (by length within non-unipi): cd(2), ef(2)
400
- // Then unipi (by length): unipi:ab(9), unipi:gh(9)
401
- const result = values(sorted);
402
- expect(result.slice(0, 2)).toEqual(["cd", "ef"]);
403
- expect(result.slice(2, 4)).toEqual(["unipi:ab", "unipi:gh"]);
404
- });
405
-
406
- it("handles query matching full unipi prefix + partial short name", () => {
407
- // query "unipi:bra" → full "unipi:brainstorm".startsWith("unipi:bra") → tier 2
408
- expect(crossItemPriority(item("unipi:brainstorm"), true, "unipi:bra")).toBe(2);
409
- });
410
-
411
- it("unipi item with tier 1 beats base item with tier 2", () => {
412
- const items = [
413
- tagged("work", false), // tier 2 (prefix "brain"... no)
414
- tagged("unipi:brainstorm", true), // tier 1 (short exact)
415
- ];
416
-
417
- // query "brainstorm":
418
- // "work" → full "work" !== "brainstorm", !startsWith → tier 3
419
- // "unipi:brainstorm" → short "brainstorm" === "brainstorm" → tier 1
420
- const sorted = sortTaggedItems(items, "brainstorm");
421
- expect(values(sorted)).toEqual(["unipi:brainstorm", "work"]);
422
- });
423
- });