@nebutra/agent-runtime 0.2.0
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/.turbo/turbo-build.log +115 -0
- package/.turbo/turbo-test.log +44 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +253 -0
- package/LICENSE +676 -0
- package/README.md +50 -0
- package/dist/adapters/dispatcher-sse.d.ts +68 -0
- package/dist/adapters/dispatcher-sse.js +11 -0
- package/dist/adapters/dispatcher-sse.js.map +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mcp-catalog.d.ts +58 -0
- package/dist/adapters/mcp-catalog.js +9 -0
- package/dist/adapters/mcp-catalog.js.map +1 -0
- package/dist/adapters/prisma-rollout.d.ts +60 -0
- package/dist/adapters/prisma-rollout.js +7 -0
- package/dist/adapters/prisma-rollout.js.map +1 -0
- package/dist/chunk-24ZXP7FI.js +93 -0
- package/dist/chunk-24ZXP7FI.js.map +1 -0
- package/dist/chunk-2DA6Q6TN.js +126 -0
- package/dist/chunk-2DA6Q6TN.js.map +1 -0
- package/dist/chunk-37BBB2P2.js +73 -0
- package/dist/chunk-37BBB2P2.js.map +1 -0
- package/dist/chunk-57W3AR43.js +52 -0
- package/dist/chunk-57W3AR43.js.map +1 -0
- package/dist/chunk-5N4644PB.js +67 -0
- package/dist/chunk-5N4644PB.js.map +1 -0
- package/dist/chunk-5YS7WAPS.js +177 -0
- package/dist/chunk-5YS7WAPS.js.map +1 -0
- package/dist/chunk-6EGG2OZC.js +13 -0
- package/dist/chunk-6EGG2OZC.js.map +1 -0
- package/dist/chunk-7BUOF367.js +126 -0
- package/dist/chunk-7BUOF367.js.map +1 -0
- package/dist/chunk-BJBBR3QA.js +121 -0
- package/dist/chunk-BJBBR3QA.js.map +1 -0
- package/dist/chunk-CGRCUKGT.js +73 -0
- package/dist/chunk-CGRCUKGT.js.map +1 -0
- package/dist/chunk-FUG5DT2C.js +75 -0
- package/dist/chunk-FUG5DT2C.js.map +1 -0
- package/dist/chunk-LO24VOA3.js +199 -0
- package/dist/chunk-LO24VOA3.js.map +1 -0
- package/dist/chunk-MUF7ZZTO.js +57 -0
- package/dist/chunk-MUF7ZZTO.js.map +1 -0
- package/dist/chunk-NN7DATXA.js +46 -0
- package/dist/chunk-NN7DATXA.js.map +1 -0
- package/dist/chunk-PGGWSUTM.js +33 -0
- package/dist/chunk-PGGWSUTM.js.map +1 -0
- package/dist/chunk-RDKYDMXT.js +135 -0
- package/dist/chunk-RDKYDMXT.js.map +1 -0
- package/dist/chunk-YYFPDBJG.js +63 -0
- package/dist/chunk-YYFPDBJG.js.map +1 -0
- package/dist/chunk-ZMYX5VBU.js +135 -0
- package/dist/chunk-ZMYX5VBU.js.map +1 -0
- package/dist/chunk-ZTSKS42I.js +131 -0
- package/dist/chunk-ZTSKS42I.js.map +1 -0
- package/dist/commands.d.ts +74 -0
- package/dist/commands.js +10 -0
- package/dist/commands.js.map +1 -0
- package/dist/definitions.d.ts +94 -0
- package/dist/definitions.js +15 -0
- package/dist/definitions.js.map +1 -0
- package/dist/dispatcher.d.ts +50 -0
- package/dist/dispatcher.js +8 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/durable-turn.d.ts +58 -0
- package/dist/durable-turn.js +9 -0
- package/dist/durable-turn.js.map +1 -0
- package/dist/hook-pipeline.d.ts +114 -0
- package/dist/hook-pipeline.js +13 -0
- package/dist/hook-pipeline.js.map +1 -0
- package/dist/index.d.ts +1874 -0
- package/dist/index.js +3117 -0
- package/dist/index.js.map +1 -0
- package/dist/loop.d.ts +78 -0
- package/dist/loop.js +9 -0
- package/dist/loop.js.map +1 -0
- package/dist/mcp-bridge.d.ts +48 -0
- package/dist/mcp-bridge.js +8 -0
- package/dist/mcp-bridge.js.map +1 -0
- package/dist/model.d.ts +154 -0
- package/dist/model.js +9 -0
- package/dist/model.js.map +1 -0
- package/dist/policy.d.ts +130 -0
- package/dist/policy.js +23 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +170 -0
- package/dist/protocol.js +15 -0
- package/dist/protocol.js.map +1 -0
- package/dist/rollout-store-persistent.d.ts +48 -0
- package/dist/rollout-store-persistent.js +9 -0
- package/dist/rollout-store-persistent.js.map +1 -0
- package/dist/rollout.d.ts +82 -0
- package/dist/rollout.js +15 -0
- package/dist/rollout.js.map +1 -0
- package/dist/sandbox.d.ts +65 -0
- package/dist/sandbox.js +15 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/skills.d.ts +93 -0
- package/dist/skills.js +10 -0
- package/dist/skills.js.map +1 -0
- package/dist/subagents.d.ts +129 -0
- package/dist/subagents.js +21 -0
- package/dist/subagents.js.map +1 -0
- package/dist/tools.d.ts +77 -0
- package/dist/tools.js +9 -0
- package/dist/tools.js.map +1 -0
- package/package.json +74 -0
- package/src/adapters/dispatcher-sse.test.ts +218 -0
- package/src/adapters/dispatcher-sse.ts +222 -0
- package/src/adapters/index.ts +18 -0
- package/src/adapters/mcp-catalog.test.ts +213 -0
- package/src/adapters/mcp-catalog.ts +188 -0
- package/src/adapters/prisma-rollout.test.ts +153 -0
- package/src/adapters/prisma-rollout.ts +104 -0
- package/src/agent-runtime.test.ts +176 -0
- package/src/artifact-stream.test.ts +330 -0
- package/src/artifact-stream.ts +453 -0
- package/src/channel-gateway.test.ts +432 -0
- package/src/channel-gateway.ts +357 -0
- package/src/code-review.test.ts +501 -0
- package/src/code-review.ts +495 -0
- package/src/command-suggestions.test.ts +251 -0
- package/src/command-suggestions.ts +338 -0
- package/src/commands.test.ts +184 -0
- package/src/commands.ts +140 -0
- package/src/commit-message.test.ts +249 -0
- package/src/commit-message.ts +180 -0
- package/src/context-compaction.test.ts +522 -0
- package/src/context-compaction.ts +434 -0
- package/src/definitions.test.ts +78 -0
- package/src/definitions.ts +190 -0
- package/src/deployment-status.test.ts +215 -0
- package/src/deployment-status.ts +227 -0
- package/src/design-context.test.ts +195 -0
- package/src/design-context.ts +198 -0
- package/src/dispatcher.test.ts +234 -0
- package/src/dispatcher.ts +189 -0
- package/src/durable-turn.test.ts +209 -0
- package/src/durable-turn.ts +135 -0
- package/src/edit-planner.test.ts +204 -0
- package/src/edit-planner.ts +325 -0
- package/src/fuzzy-match.test.ts +311 -0
- package/src/fuzzy-match.ts +444 -0
- package/src/hook-pipeline.test.ts +279 -0
- package/src/hook-pipeline.ts +373 -0
- package/src/inbound-admission.test.ts +394 -0
- package/src/inbound-admission.ts +246 -0
- package/src/index.ts +47 -0
- package/src/loop.test.ts +161 -0
- package/src/loop.ts +211 -0
- package/src/mcp-bridge.test.ts +165 -0
- package/src/mcp-bridge.ts +76 -0
- package/src/memory-provider.test.ts +232 -0
- package/src/memory-provider.ts +257 -0
- package/src/model.ts +168 -0
- package/src/permission-ruleset.test.ts +301 -0
- package/src/permission-ruleset.ts +200 -0
- package/src/policy.ts +151 -0
- package/src/project-repo.test.ts +232 -0
- package/src/project-repo.ts +311 -0
- package/src/protocol.ts +159 -0
- package/src/rollout-store-persistent.test.ts +217 -0
- package/src/rollout-store-persistent.ts +166 -0
- package/src/rollout.ts +150 -0
- package/src/sandbox.ts +113 -0
- package/src/session-share.test.ts +360 -0
- package/src/session-share.ts +310 -0
- package/src/skill-distillation.test.ts +177 -0
- package/src/skill-distillation.ts +369 -0
- package/src/skills.test.ts +277 -0
- package/src/skills.ts +255 -0
- package/src/subagents.test.ts +290 -0
- package/src/subagents.ts +332 -0
- package/src/tools.ts +126 -0
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +33 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the fuzzy-match primitive.
|
|
3
|
+
*
|
|
4
|
+
* Pure, deterministic subsequence fuzzy matcher with smart-case behaviour,
|
|
5
|
+
* a case-insensitive variant, an ignore-spaces variant, a glob-style wildcard
|
|
6
|
+
* matcher, and a generic ranking helper. No external deps, no I/O, no state.
|
|
7
|
+
*
|
|
8
|
+
* Scoring is asserted by ORDERING (boundary/contiguous > scattered), never by
|
|
9
|
+
* exact magnitudes — the heuristic is documented in fuzzy-match.ts and only its
|
|
10
|
+
* ordering contract is part of the public surface.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
matchIndices,
|
|
16
|
+
matchIndicesCaseInsensitive,
|
|
17
|
+
matchIndicesCaseInsensitiveIgnoreSpaces,
|
|
18
|
+
matchWildcardPattern,
|
|
19
|
+
matchWildcardPatternCaseInsensitive,
|
|
20
|
+
rankByFuzzy,
|
|
21
|
+
} from "./fuzzy-match.js";
|
|
22
|
+
|
|
23
|
+
describe("matchIndices — subsequence fuzzy match", () => {
|
|
24
|
+
it("returns ordered indices for a subsequence hit", () => {
|
|
25
|
+
// "foobar": f=0 o=1 o=2 b=3 a=4 r=5 → f@0, b@3, r@5
|
|
26
|
+
const result = matchIndices("foobar", "fbr");
|
|
27
|
+
expect(result).not.toBeNull();
|
|
28
|
+
expect(result?.indices).toEqual([0, 3, 5]);
|
|
29
|
+
// indices must be strictly increasing and point at the matched chars
|
|
30
|
+
expect(result?.indices).toEqual([...(result?.indices ?? [])].sort((a, b) => a - b));
|
|
31
|
+
for (const i of result?.indices ?? []) {
|
|
32
|
+
expect(i).toBeGreaterThanOrEqual(0);
|
|
33
|
+
expect(i).toBeLessThan("foobar".length);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns null when query is not a subsequence", () => {
|
|
38
|
+
expect(matchIndices("foobar", "xyz")).toBeNull();
|
|
39
|
+
expect(matchIndices("abc", "abcd")).toBeNull();
|
|
40
|
+
// out-of-order chars are not a subsequence
|
|
41
|
+
expect(matchIndices("abc", "ba")).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("matches a full exact string", () => {
|
|
45
|
+
const result = matchIndices("hello", "hello");
|
|
46
|
+
expect(result?.indices).toEqual([0, 1, 2, 3, 4]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("treats empty query as matching everything with score 0 and empty indices", () => {
|
|
50
|
+
const result = matchIndices("anything", "");
|
|
51
|
+
expect(result).not.toBeNull();
|
|
52
|
+
expect(result?.indices).toEqual([]);
|
|
53
|
+
expect(result?.score).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("empty query matches even empty text", () => {
|
|
57
|
+
const result = matchIndices("", "");
|
|
58
|
+
expect(result?.indices).toEqual([]);
|
|
59
|
+
expect(result?.score).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("non-empty query against empty text is null", () => {
|
|
63
|
+
expect(matchIndices("", "a")).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("smart-case", () => {
|
|
67
|
+
it("all-lowercase query matches case-insensitively", () => {
|
|
68
|
+
const result = matchIndices("FooBar", "fb");
|
|
69
|
+
expect(result).not.toBeNull();
|
|
70
|
+
expect(result?.indices).toEqual([0, 3]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("query with an uppercase char matches case-sensitively", () => {
|
|
74
|
+
// 'B' must match an uppercase B; 'foobar' has none → null
|
|
75
|
+
expect(matchIndices("foobar", "B")).toBeNull();
|
|
76
|
+
// but it matches against a real uppercase B
|
|
77
|
+
const result = matchIndices("fooBar", "B");
|
|
78
|
+
expect(result?.indices).toEqual([3]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("mixed-case query: lowercase letters still match insensitively per char? (case-sensitive whole query)", () => {
|
|
82
|
+
// Spec: presence of ANY uppercase makes the WHOLE query case-sensitive.
|
|
83
|
+
// 'Fb' → 'F' needs uppercase F, 'b' needs lowercase b (case-sensitive).
|
|
84
|
+
expect(matchIndices("foobar", "Fb")).toBeNull();
|
|
85
|
+
const result = matchIndices("Foobar", "Fb");
|
|
86
|
+
expect(result?.indices).toEqual([0, 3]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("boundary / contiguity scoring (ordering only)", () => {
|
|
91
|
+
it("contiguous run outranks a scattered match", () => {
|
|
92
|
+
const contiguous = matchIndices("foobar", "foo");
|
|
93
|
+
const scattered = matchIndices("fxoxox foo", "foo");
|
|
94
|
+
expect(contiguous).not.toBeNull();
|
|
95
|
+
expect(scattered).not.toBeNull();
|
|
96
|
+
expect((contiguous?.score ?? 0) > (scattered?.score ?? 0)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("word-boundary match outranks a mid-word match", () => {
|
|
100
|
+
// "ab" right after a separator vs "ab" buried mid-token
|
|
101
|
+
const boundary = matchIndices("foo/abc", "abc");
|
|
102
|
+
const midword = matchIndices("xabcyyyabc", "abc");
|
|
103
|
+
expect(boundary).not.toBeNull();
|
|
104
|
+
expect(midword).not.toBeNull();
|
|
105
|
+
expect((boundary?.score ?? 0) > (midword?.score ?? 0)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("match at string start outranks a later contiguous match", () => {
|
|
109
|
+
const atStart = matchIndices("abcxxxx", "abc");
|
|
110
|
+
const later = matchIndices("zzzzabc", "abc");
|
|
111
|
+
expect((atStart?.score ?? 0) > (later?.score ?? 0)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("prefers the boundary subsequence when several exist", () => {
|
|
115
|
+
// "fb" can match index 0/1 (mid 'foo bar' f..) or the boundary 'f' of
|
|
116
|
+
// 'foo' + boundary 'b' of 'bar'. Boundary-preferring heuristic should pick
|
|
117
|
+
// the word-start 'f' (0) and word-start 'b' (4).
|
|
118
|
+
const result = matchIndices("foo bar", "fb");
|
|
119
|
+
expect(result?.indices).toEqual([0, 4]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("matchIndicesCaseInsensitive", () => {
|
|
125
|
+
it("always ignores case even with uppercase in query", () => {
|
|
126
|
+
// "foobar": f@0, b@3, r@5 — uppercase query, but ci variant folds it.
|
|
127
|
+
const result = matchIndicesCaseInsensitive("foobar", "FBR");
|
|
128
|
+
expect(result?.indices).toEqual([0, 3, 5]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("still returns null for non-subsequence", () => {
|
|
132
|
+
expect(matchIndicesCaseInsensitive("foobar", "zzz")).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("empty query → score 0, empty indices", () => {
|
|
136
|
+
const result = matchIndicesCaseInsensitive("abc", "");
|
|
137
|
+
expect(result?.score).toBe(0);
|
|
138
|
+
expect(result?.indices).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("matchIndicesCaseInsensitiveIgnoreSpaces", () => {
|
|
143
|
+
it("ignores spaces in query and text but returns original-text indices", () => {
|
|
144
|
+
// text "foo bar" with query "f b" → spaces ignored on both sides.
|
|
145
|
+
const result = matchIndicesCaseInsensitiveIgnoreSpaces("foo bar", "f b");
|
|
146
|
+
expect(result).not.toBeNull();
|
|
147
|
+
// indices must point at original positions of 'f' (0) and 'b' (4),
|
|
148
|
+
// never at the space at index 3.
|
|
149
|
+
expect(result?.indices).toEqual([0, 4]);
|
|
150
|
+
expect(result?.indices).not.toContain(3);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("is case-insensitive", () => {
|
|
154
|
+
const result = matchIndicesCaseInsensitiveIgnoreSpaces("Foo Bar", "FB");
|
|
155
|
+
expect(result?.indices).toEqual([0, 4]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("query that is only spaces behaves like empty query", () => {
|
|
159
|
+
const result = matchIndicesCaseInsensitiveIgnoreSpaces("foo bar", " ");
|
|
160
|
+
expect(result?.score).toBe(0);
|
|
161
|
+
expect(result?.indices).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns null when non-space chars are not a subsequence", () => {
|
|
165
|
+
expect(matchIndicesCaseInsensitiveIgnoreSpaces("foo bar", "zq")).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("matched index never lands on a skipped space", () => {
|
|
169
|
+
const result = matchIndicesCaseInsensitiveIgnoreSpaces("a b c d", "abcd");
|
|
170
|
+
expect(result?.indices).toEqual([0, 2, 4, 6]);
|
|
171
|
+
for (const i of result?.indices ?? []) {
|
|
172
|
+
expect("a b c d"[i]).not.toBe(" ");
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("matchWildcardPattern — glob-style, anchored full-string", () => {
|
|
178
|
+
it("matches '*' as any run including empty", () => {
|
|
179
|
+
expect(matchWildcardPattern("anything", "*")).toBe(true);
|
|
180
|
+
expect(matchWildcardPattern("", "*")).toBe(true);
|
|
181
|
+
expect(matchWildcardPattern("src/foo.ts", "src/*")).toBe(true);
|
|
182
|
+
expect(matchWildcardPattern("src/", "src/*")).toBe(true);
|
|
183
|
+
expect(matchWildcardPattern("abc", "a*c")).toBe(true);
|
|
184
|
+
expect(matchWildcardPattern("ac", "a*c")).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("'?' matches exactly one char", () => {
|
|
188
|
+
expect(matchWildcardPattern("abc", "a?c")).toBe(true);
|
|
189
|
+
expect(matchWildcardPattern("ac", "a?c")).toBe(false);
|
|
190
|
+
expect(matchWildcardPattern("abbc", "a?c")).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("char classes [abc] and ranges [a-z]", () => {
|
|
194
|
+
expect(matchWildcardPattern("cat", "[cb]at")).toBe(true);
|
|
195
|
+
expect(matchWildcardPattern("bat", "[cb]at")).toBe(true);
|
|
196
|
+
expect(matchWildcardPattern("rat", "[cb]at")).toBe(false);
|
|
197
|
+
expect(matchWildcardPattern("file9.txt", "file[0-9].txt")).toBe(true);
|
|
198
|
+
expect(matchWildcardPattern("fileA.txt", "file[0-9].txt")).toBe(false);
|
|
199
|
+
expect(matchWildcardPattern("x", "[a-z]")).toBe(true);
|
|
200
|
+
expect(matchWildcardPattern("X", "[a-z]")).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("escape with backslash treats metachars literally", () => {
|
|
204
|
+
expect(matchWildcardPattern("a*b", "a\\*b")).toBe(true);
|
|
205
|
+
expect(matchWildcardPattern("axb", "a\\*b")).toBe(false);
|
|
206
|
+
expect(matchWildcardPattern("a?b", "a\\?b")).toBe(true);
|
|
207
|
+
expect(matchWildcardPattern("a[b", "a\\[b")).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("is anchored (full-string) — partial does not match", () => {
|
|
211
|
+
expect(matchWildcardPattern("foobar", "foo")).toBe(false);
|
|
212
|
+
expect(matchWildcardPattern("foobar", "foo*")).toBe(true);
|
|
213
|
+
expect(matchWildcardPattern("foobar", "*bar")).toBe(true);
|
|
214
|
+
expect(matchWildcardPattern("foobar", "bar")).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("treats '/' as an ordinary char (path-like)", () => {
|
|
218
|
+
expect(matchWildcardPattern("a/b/c", "a/*/c")).toBe(true);
|
|
219
|
+
expect(matchWildcardPattern("a/b/c", "a*c")).toBe(true);
|
|
220
|
+
expect(matchWildcardPattern("a/b/c", "*/*/*")).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("is case-sensitive by default; case-insensitive variant ignores case", () => {
|
|
224
|
+
expect(matchWildcardPattern("FOO", "foo")).toBe(false);
|
|
225
|
+
expect(matchWildcardPatternCaseInsensitive("FOO", "foo")).toBe(true);
|
|
226
|
+
expect(matchWildcardPatternCaseInsensitive("FoO.TS", "*.ts")).toBe(true);
|
|
227
|
+
expect(matchWildcardPatternCaseInsensitive("Bat", "[CB]at")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does not catastrophically backtrack on adversarial '*' input", () => {
|
|
231
|
+
const text = "a".repeat(50);
|
|
232
|
+
const pattern = `${"*".repeat(30)}b`;
|
|
233
|
+
const start = Date.now();
|
|
234
|
+
const matched = matchWildcardPattern(text, pattern);
|
|
235
|
+
expect(matched).toBe(false);
|
|
236
|
+
expect(Date.now() - start).toBeLessThan(1000);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("rankByFuzzy", () => {
|
|
241
|
+
interface Row {
|
|
242
|
+
id: number;
|
|
243
|
+
label: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const rows: Row[] = [
|
|
247
|
+
{ id: 1, label: "foobar" },
|
|
248
|
+
{ id: 2, label: "no match here" },
|
|
249
|
+
{ id: 3, label: "fxoxoxbar" },
|
|
250
|
+
{ id: 4, label: "foo/bar" },
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
it("filters out non-matches", () => {
|
|
254
|
+
const ranked = rankByFuzzy(rows, "fbr", (r) => r.label);
|
|
255
|
+
expect(ranked.every((r) => r.item.id !== 2)).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("sorts by score descending", () => {
|
|
259
|
+
const ranked = rankByFuzzy(rows, "fbr", (r) => r.label);
|
|
260
|
+
for (let i = 1; i < ranked.length; i++) {
|
|
261
|
+
expect(ranked[i - 1]!.score).toBeGreaterThanOrEqual(ranked[i]!.score);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns score + indices alongside the item", () => {
|
|
266
|
+
const ranked = rankByFuzzy(rows, "foo", (r) => r.label);
|
|
267
|
+
const first = ranked[0]!;
|
|
268
|
+
expect(typeof first.score).toBe("number");
|
|
269
|
+
expect(Array.isArray(first.indices)).toBe(true);
|
|
270
|
+
expect(first.indices.length).toBe(3);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("is stable for equal scores (preserves input order)", () => {
|
|
274
|
+
const tied: Row[] = [
|
|
275
|
+
{ id: 10, label: "abc" },
|
|
276
|
+
{ id: 11, label: "abc" },
|
|
277
|
+
{ id: 12, label: "abc" },
|
|
278
|
+
];
|
|
279
|
+
const ranked = rankByFuzzy(tied, "abc", (r) => r.label);
|
|
280
|
+
expect(ranked.map((r) => r.item.id)).toEqual([10, 11, 12]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("does not mutate the input array or its items", () => {
|
|
284
|
+
const input: Row[] = [
|
|
285
|
+
{ id: 1, label: "foobar" },
|
|
286
|
+
{ id: 2, label: "barbaz" },
|
|
287
|
+
];
|
|
288
|
+
const snapshot = JSON.parse(JSON.stringify(input));
|
|
289
|
+
rankByFuzzy(input, "bar", (r) => r.label);
|
|
290
|
+
expect(input).toEqual(snapshot);
|
|
291
|
+
expect(input.length).toBe(2);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("respects caseInsensitive option", () => {
|
|
295
|
+
const list = [{ id: 1, label: "foobar" }];
|
|
296
|
+
// Query 'fB' has an uppercase → smart-case makes it case-sensitive, so
|
|
297
|
+
// 'B' cannot match lowercase 'b' in 'foobar' → filtered out.
|
|
298
|
+
expect(rankByFuzzy(list, "fB", (r) => r.label).length).toBe(0);
|
|
299
|
+
// With caseInsensitive forced, 'B' folds to 'b' and it matches.
|
|
300
|
+
expect(rankByFuzzy(list, "fB", (r) => r.label, { caseInsensitive: true }).length).toBe(1);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("empty query matches all items with score 0", () => {
|
|
304
|
+
const ranked = rankByFuzzy(rows, "", (r) => r.label);
|
|
305
|
+
expect(ranked.length).toBe(rows.length);
|
|
306
|
+
expect(ranked.every((r) => r.score === 0)).toBe(true);
|
|
307
|
+
expect(ranked.every((r) => r.indices.length === 0)).toBe(true);
|
|
308
|
+
// stable order preserved
|
|
309
|
+
expect(ranked.map((r) => r.item.id)).toEqual([1, 2, 3, 4]);
|
|
310
|
+
});
|
|
311
|
+
});
|