@vltpkg/query 1.5.0 → 1.6.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.
Files changed (211) hide show
  1. package/dist/id.js +12 -5
  2. package/package.json +10 -12
  3. package/skills/dss-query/REFERENCE.md +0 -125
  4. package/skills/dss-query/SKILL.md +0 -190
  5. package/skills/dss-query/evals/README.md +0 -81
  6. package/skills/dss-query/evals/evals.json +0 -89
  7. package/skills/dss-query/evals/grade.mjs +0 -481
  8. package/src/attribute.ts +0 -190
  9. package/src/combinator.ts +0 -135
  10. package/src/error.ts +0 -78
  11. package/src/id.ts +0 -46
  12. package/src/index.ts +0 -670
  13. package/src/parser.ts +0 -122
  14. package/src/pseudo/abandoned.ts +0 -9
  15. package/src/pseudo/attr.ts +0 -92
  16. package/src/pseudo/built.ts +0 -19
  17. package/src/pseudo/confused.ts +0 -29
  18. package/src/pseudo/cve.ts +0 -93
  19. package/src/pseudo/cwe.ts +0 -97
  20. package/src/pseudo/debug.ts +0 -9
  21. package/src/pseudo/deprecated.ts +0 -9
  22. package/src/pseudo/dev.ts +0 -18
  23. package/src/pseudo/diff.ts +0 -90
  24. package/src/pseudo/dist.ts +0 -148
  25. package/src/pseudo/dynamic.ts +0 -9
  26. package/src/pseudo/empty.ts +0 -15
  27. package/src/pseudo/entropic.ts +0 -9
  28. package/src/pseudo/env.ts +0 -6
  29. package/src/pseudo/eval.ts +0 -9
  30. package/src/pseudo/fs.ts +0 -9
  31. package/src/pseudo/helpers.ts +0 -106
  32. package/src/pseudo/host.ts +0 -111
  33. package/src/pseudo/hostname.ts +0 -156
  34. package/src/pseudo/license.ts +0 -134
  35. package/src/pseudo/link.ts +0 -30
  36. package/src/pseudo/malware.ts +0 -41
  37. package/src/pseudo/minified.ts +0 -9
  38. package/src/pseudo/missing.ts +0 -16
  39. package/src/pseudo/native.ts +0 -9
  40. package/src/pseudo/network.ts +0 -9
  41. package/src/pseudo/obfuscated.ts +0 -9
  42. package/src/pseudo/optional.ts +0 -18
  43. package/src/pseudo/outdated.ts +0 -345
  44. package/src/pseudo/overridden.ts +0 -20
  45. package/src/pseudo/path.ts +0 -146
  46. package/src/pseudo/peer.ts +0 -18
  47. package/src/pseudo/prerelease.ts +0 -47
  48. package/src/pseudo/private.ts +0 -19
  49. package/src/pseudo/prod.ts +0 -18
  50. package/src/pseudo/published.ts +0 -250
  51. package/src/pseudo/registry.ts +0 -29
  52. package/src/pseudo/root.ts +0 -19
  53. package/src/pseudo/scanned.ts +0 -20
  54. package/src/pseudo/score.ts +0 -186
  55. package/src/pseudo/scripts.ts +0 -61
  56. package/src/pseudo/semver.ts +0 -320
  57. package/src/pseudo/severity.ts +0 -231
  58. package/src/pseudo/shell.ts +0 -9
  59. package/src/pseudo/shrinkwrap.ts +0 -9
  60. package/src/pseudo/skill.ts +0 -59
  61. package/src/pseudo/skills.ts +0 -29
  62. package/src/pseudo/spec.ts +0 -131
  63. package/src/pseudo/squat.ts +0 -225
  64. package/src/pseudo/suspicious.ts +0 -9
  65. package/src/pseudo/tracker.ts +0 -9
  66. package/src/pseudo/trivial.ts +0 -9
  67. package/src/pseudo/type.ts +0 -26
  68. package/src/pseudo/undesirable.ts +0 -9
  69. package/src/pseudo/unknown.ts +0 -9
  70. package/src/pseudo/unmaintained.ts +0 -9
  71. package/src/pseudo/unpopular.ts +0 -9
  72. package/src/pseudo/unstable.ts +0 -9
  73. package/src/pseudo/vuln.ts +0 -273
  74. package/src/pseudo/workspace.ts +0 -22
  75. package/src/pseudo.ts +0 -440
  76. package/src/types.ts +0 -206
  77. package/tap-snapshots/test/attribute.ts.test.cjs +0 -304
  78. package/tap-snapshots/test/combinator.ts.test.cjs +0 -333
  79. package/tap-snapshots/test/id.ts.test.cjs +0 -118
  80. package/tap-snapshots/test/parser.ts.test.cjs +0 -344
  81. package/tap-snapshots/test/pseudo/abandoned.ts.test.cjs +0 -18
  82. package/tap-snapshots/test/pseudo/attr.ts.test.cjs +0 -161
  83. package/tap-snapshots/test/pseudo/built.ts.test.cjs +0 -70
  84. package/tap-snapshots/test/pseudo/confused.ts.test.cjs +0 -32
  85. package/tap-snapshots/test/pseudo/cve.ts.test.cjs +0 -49
  86. package/tap-snapshots/test/pseudo/cwe.ts.test.cjs +0 -49
  87. package/tap-snapshots/test/pseudo/debug.ts.test.cjs +0 -18
  88. package/tap-snapshots/test/pseudo/deprecated.ts.test.cjs +0 -18
  89. package/tap-snapshots/test/pseudo/dist.ts.test.cjs +0 -30
  90. package/tap-snapshots/test/pseudo/dynamic.ts.test.cjs +0 -18
  91. package/tap-snapshots/test/pseudo/empty.ts.test.cjs +0 -50
  92. package/tap-snapshots/test/pseudo/entropic.ts.test.cjs +0 -18
  93. package/tap-snapshots/test/pseudo/env.ts.test.cjs +0 -18
  94. package/tap-snapshots/test/pseudo/eval.ts.test.cjs +0 -18
  95. package/tap-snapshots/test/pseudo/fs.ts.test.cjs +0 -18
  96. package/tap-snapshots/test/pseudo/helpers.ts.test.cjs +0 -18
  97. package/tap-snapshots/test/pseudo/hostname.ts.test.cjs +0 -148
  98. package/tap-snapshots/test/pseudo/license.ts.test.cjs +0 -41
  99. package/tap-snapshots/test/pseudo/link.ts.test.cjs +0 -36
  100. package/tap-snapshots/test/pseudo/malware.ts.test.cjs +0 -22
  101. package/tap-snapshots/test/pseudo/minified.ts.test.cjs +0 -18
  102. package/tap-snapshots/test/pseudo/missing.ts.test.cjs +0 -30
  103. package/tap-snapshots/test/pseudo/native.ts.test.cjs +0 -18
  104. package/tap-snapshots/test/pseudo/network.ts.test.cjs +0 -18
  105. package/tap-snapshots/test/pseudo/obfuscated.ts.test.cjs +0 -18
  106. package/tap-snapshots/test/pseudo/outdated.ts.test.cjs +0 -132
  107. package/tap-snapshots/test/pseudo/overridden.ts.test.cjs +0 -75
  108. package/tap-snapshots/test/pseudo/prerelease.ts.test.cjs +0 -101
  109. package/tap-snapshots/test/pseudo/private.ts.test.cjs +0 -38
  110. package/tap-snapshots/test/pseudo/published.ts.test.cjs +0 -171
  111. package/tap-snapshots/test/pseudo/registry.ts.test.cjs +0 -68
  112. package/tap-snapshots/test/pseudo/root.ts.test.cjs +0 -24
  113. package/tap-snapshots/test/pseudo/scanned.ts.test.cjs +0 -18
  114. package/tap-snapshots/test/pseudo/score.ts.test.cjs +0 -239
  115. package/tap-snapshots/test/pseudo/scripts.ts.test.cjs +0 -62
  116. package/tap-snapshots/test/pseudo/semver.ts.test.cjs +0 -575
  117. package/tap-snapshots/test/pseudo/severity.ts.test.cjs +0 -122
  118. package/tap-snapshots/test/pseudo/shell.ts.test.cjs +0 -18
  119. package/tap-snapshots/test/pseudo/shrinkwrap.ts.test.cjs +0 -18
  120. package/tap-snapshots/test/pseudo/skills.ts.test.cjs +0 -19
  121. package/tap-snapshots/test/pseudo/spec.ts.test.cjs +0 -148
  122. package/tap-snapshots/test/pseudo/squat.ts.test.cjs +0 -129
  123. package/tap-snapshots/test/pseudo/suspicious.ts.test.cjs +0 -18
  124. package/tap-snapshots/test/pseudo/tracker.ts.test.cjs +0 -18
  125. package/tap-snapshots/test/pseudo/trivial.ts.test.cjs +0 -18
  126. package/tap-snapshots/test/pseudo/type.ts.test.cjs +0 -51
  127. package/tap-snapshots/test/pseudo/undesirable.ts.test.cjs +0 -18
  128. package/tap-snapshots/test/pseudo/unknown.ts.test.cjs +0 -18
  129. package/tap-snapshots/test/pseudo/unmaintained.ts.test.cjs +0 -18
  130. package/tap-snapshots/test/pseudo/unpopular.ts.test.cjs +0 -18
  131. package/tap-snapshots/test/pseudo/unstable.ts.test.cjs +0 -18
  132. package/tap-snapshots/test/pseudo/vuln.ts.test.cjs +0 -208
  133. package/tap-snapshots/test/pseudo/vulnerable.ts.test.cjs +0 -20
  134. package/tap-snapshots/test/pseudo/workspace.ts.test.cjs +0 -19
  135. package/tap-snapshots/test/pseudo.ts.test.cjs +0 -1247
  136. package/test/attribute.ts +0 -273
  137. package/test/combinator.ts +0 -175
  138. package/test/error.ts +0 -60
  139. package/test/fixtures/graph.ts +0 -999
  140. package/test/fixtures/selector.ts +0 -103
  141. package/test/fixtures/types.ts +0 -7
  142. package/test/id.ts +0 -102
  143. package/test/index.ts +0 -1148
  144. package/test/parser.ts +0 -117
  145. package/test/pseudo/abandoned.ts +0 -98
  146. package/test/pseudo/attr.ts +0 -298
  147. package/test/pseudo/built.ts +0 -214
  148. package/test/pseudo/confused.ts +0 -160
  149. package/test/pseudo/cve.ts +0 -249
  150. package/test/pseudo/cwe.ts +0 -255
  151. package/test/pseudo/debug.ts +0 -98
  152. package/test/pseudo/deprecated.ts +0 -98
  153. package/test/pseudo/dev.ts +0 -61
  154. package/test/pseudo/diff.ts +0 -417
  155. package/test/pseudo/dist.ts +0 -215
  156. package/test/pseudo/dynamic.ts +0 -98
  157. package/test/pseudo/empty.ts +0 -108
  158. package/test/pseudo/entropic.ts +0 -101
  159. package/test/pseudo/env.ts +0 -98
  160. package/test/pseudo/eval.ts +0 -98
  161. package/test/pseudo/fs.ts +0 -98
  162. package/test/pseudo/helpers.ts +0 -370
  163. package/test/pseudo/host-context.ts +0 -276
  164. package/test/pseudo/hostname.ts +0 -314
  165. package/test/pseudo/license.ts +0 -265
  166. package/test/pseudo/link.ts +0 -94
  167. package/test/pseudo/malware.ts +0 -184
  168. package/test/pseudo/minified.ts +0 -98
  169. package/test/pseudo/missing.ts +0 -111
  170. package/test/pseudo/native.ts +0 -98
  171. package/test/pseudo/network.ts +0 -98
  172. package/test/pseudo/obfuscated.ts +0 -98
  173. package/test/pseudo/optional.ts +0 -73
  174. package/test/pseudo/outdated.ts +0 -461
  175. package/test/pseudo/overridden.ts +0 -317
  176. package/test/pseudo/path.ts +0 -680
  177. package/test/pseudo/peer.ts +0 -101
  178. package/test/pseudo/prerelease.ts +0 -279
  179. package/test/pseudo/private.ts +0 -108
  180. package/test/pseudo/prod.ts +0 -61
  181. package/test/pseudo/published.ts +0 -557
  182. package/test/pseudo/registry.ts +0 -122
  183. package/test/pseudo/root.ts +0 -66
  184. package/test/pseudo/scanned.ts +0 -144
  185. package/test/pseudo/score.ts +0 -591
  186. package/test/pseudo/scripts.ts +0 -324
  187. package/test/pseudo/semver.ts +0 -822
  188. package/test/pseudo/severity.ts +0 -342
  189. package/test/pseudo/shell.ts +0 -98
  190. package/test/pseudo/shrinkwrap.ts +0 -98
  191. package/test/pseudo/skill.ts +0 -189
  192. package/test/pseudo/skills.ts +0 -90
  193. package/test/pseudo/spec.ts +0 -525
  194. package/test/pseudo/squat.ts +0 -565
  195. package/test/pseudo/suspicious.ts +0 -98
  196. package/test/pseudo/tracker.ts +0 -98
  197. package/test/pseudo/trivial.ts +0 -98
  198. package/test/pseudo/type.ts +0 -105
  199. package/test/pseudo/undesirable.ts +0 -98
  200. package/test/pseudo/unknown.ts +0 -98
  201. package/test/pseudo/unmaintained.ts +0 -98
  202. package/test/pseudo/unpopular.ts +0 -98
  203. package/test/pseudo/unstable.ts +0 -98
  204. package/test/pseudo/vuln.ts +0 -625
  205. package/test/pseudo/vulnerable.ts +0 -137
  206. package/test/pseudo/workspace.ts +0 -111
  207. package/test/pseudo.ts +0 -584
  208. package/tsconfig.json +0 -3
  209. package/tsconfig.publish.json +0 -19
  210. package/tsconfig.publish.tsbuildinfo +0 -1
  211. package/typedoc.mjs +0 -2
package/test/index.ts DELETED
@@ -1,1148 +0,0 @@
1
- import t from 'tap'
2
- import { Query, walk } from '../src/index.ts'
3
- import {
4
- getCycleGraph,
5
- getSimpleGraph,
6
- getSingleWorkspaceGraph,
7
- } from './fixtures/graph.ts'
8
- import { copyGraphSelectionState } from './fixtures/selector.ts'
9
- import { joinDepIDTuple } from '@vltpkg/dep-id'
10
- import {
11
- asPackageReportData,
12
- asSecurityArchiveLike,
13
- } from '@vltpkg/security-archive'
14
- import type {
15
- ParsedSelectorToken,
16
- ParserState,
17
- QueryResponse,
18
- } from '../src/types.ts'
19
- import type { PostcssNode } from '@vltpkg/dss-parser'
20
-
21
- type TestCase = [string, string[]]
22
-
23
- // Create a mock search options for tests
24
- const mockSearchOptions = {
25
- signal: { throwIfAborted: () => {} } as AbortSignal,
26
- scopeIDs: [joinDepIDTuple(['file', '.'])],
27
- }
28
-
29
- const testBrokenState = (): ParserState => {
30
- const graph = getSimpleGraph()
31
- const initial = {
32
- nodes: new Set(graph.nodes.values()),
33
- edges: new Set(graph.edges.values()),
34
- }
35
- const state: ParserState = {
36
- cancellable: async () => {},
37
- collect: {
38
- nodes: new Set(),
39
- edges: new Set(),
40
- },
41
- comment: '',
42
- current: { type: 'bork' } as unknown as PostcssNode,
43
- initial: copyGraphSelectionState(initial),
44
- partial: copyGraphSelectionState(initial),
45
- importers: new Set(graph.importers),
46
- hostContexts: undefined,
47
- walk,
48
- retries: 0,
49
- scopeIDs: mockSearchOptions.scopeIDs,
50
- securityArchive: undefined,
51
- signal: mockSearchOptions.signal,
52
- specificity: { idCounter: 0, commonCounter: 0 },
53
- }
54
- return state
55
- }
56
-
57
- t.test('simple graph', async t => {
58
- const graph = getSimpleGraph()
59
- const queryToExpected = new Set<TestCase>([
60
- ['', []], // should match no deps on missing query
61
- ['*', ['my-project', 'a', 'b', 'c', 'd', 'e', 'f', '@x/y']], // universal
62
- ['*', ['my-project', 'a', 'b', 'c', 'd', 'e', 'f', '@x/y']], // repeat, cache-hit
63
- ['* >*', ['a', 'b', 'e', '@x/y', 'c', 'd', 'f']], // dependencies
64
- [':root', ['my-project']], // select :root
65
- [':root > *', ['a', 'b', 'e', '@x/y']], // direct deps of :root
66
- [':root > :root', []], // :root pseudo-selector can't depend on itself
67
- [':dev', ['b', '@x/y']], // retrieve deps of dev type
68
- [':root > :dev', ['b', '@x/y']], // mixed with a combinator
69
- [':root > :dev[name=b]', ['b']], // specific node
70
- [':root > [name=b]:dev', ['b']], // specific node backwards
71
- [':root > *[name=b]:dev', ['b']], // specific node backwards with universal
72
- [':root > :dev[name=b] ~ *', ['a', 'e', '@x/y']], // retrieves its siblings
73
- [':root > :optional', []], // no direct optional dep
74
- [':root :optional', ['f']], // optional descendent found
75
- [':root *', ['a', 'b', 'e', '@x/y', 'c', 'd', 'f']], // all descendents from root
76
- ['[name=a]', ['a']], // select by name
77
- [':root :has([name])', ['b', 'd']], // combined has example, filtering any descendent that has direct deps with a name
78
- [':root > [name=a]', ['a']], // select children by name
79
- [':root > [name=d] [name=b]', []], // no direct descendent with that name
80
- [':root > [name=d] [name=e]', []], // descendent found by name
81
- [':root [name=d] ~ [name=c]', ['c']], // fully qualified
82
- [':root [name^=@x]', ['@x/y']], // attribute starts with value
83
- ['/* do something */ :root [name^=@x]', ['@x/y']], // support comments
84
- [':root > *{&[name=a]}', ['a']], // support nesting
85
- [':root > * { &[name=a] }', ['a']], // support nesting with spaces
86
- [':root > * { &[name=a], &[name=b] }', ['a', 'b']], // support multiple nesting selectors
87
- ['[name=b], [name=c], [name=f]', ['b', 'c', 'f']], // select by name
88
- ['[name=d], [name=d] > *', ['d', 'e', 'f']], // select a package and its dependencies
89
- [
90
- ':root > *, :prod, [name=a], :has(:dev, :optional)',
91
- ['a', 'b', 'e', '@x/y', 'c', 'd', 'my-project'],
92
- ],
93
- ['#a', ['a']], // identifier
94
- ['#a:v(1)', ['a']], // matches identifier + semver
95
- ['#a:v(2)', []], // fails to match identifier + semver
96
- [':path("*")', ['my-project', '@x/y']], // path selector for all path-based nodes
97
- [':path(".")', ['my-project']], // path selector for root
98
- [':path("**/a/**")', []], // should not match since 'a' is not workspace or file type
99
- [':root, #a, :foo', ['my-project', 'a']], // should be loose on multiple selectors
100
- ])
101
-
102
- const query = new Query({
103
- nodes: new Set(graph.nodes.values()),
104
- edges: graph.edges,
105
- importers: graph.importers,
106
- securityArchive: undefined,
107
- })
108
- for (const [q, expected] of queryToExpected) {
109
- t.strictSame(
110
- (await query.search(q, mockSearchOptions)).nodes.map(
111
- i => i.name,
112
- ),
113
- expected,
114
- `query > "${q}"`,
115
- )
116
- }
117
-
118
- await t.test('comments extraction', async t => {
119
- const result = await query.search(
120
- '/* comment one */ :root /* comment two */ > * /* comment three */',
121
- mockSearchOptions,
122
- )
123
- t.strictSame(
124
- result.comment,
125
- 'comment one',
126
- 'should extract the first comment from query',
127
- )
128
- })
129
-
130
- await t.test('comments extraction with no comments', async t => {
131
- const result = await query.search(':root > *', mockSearchOptions)
132
- t.strictSame(
133
- result.comment,
134
- '',
135
- 'should return empty string for query without comments',
136
- )
137
- })
138
-
139
- await t.test('comments cache hit', async t => {
140
- const q = '/* cached comment */ :root'
141
- await query.search(q, mockSearchOptions) // First call to populate cache
142
- const result = await query.search(q, mockSearchOptions) // Second call should hit cache
143
- t.strictSame(
144
- result.comment,
145
- 'cached comment',
146
- 'should return comment for cached query results',
147
- )
148
- })
149
- })
150
-
151
- t.test('workspace', async t => {
152
- const graph = getSingleWorkspaceGraph()
153
- const queryToExpected = new Map<string, string[]>([
154
- ['', []], // should match no deps on missing query
155
- ['*', ['ws', 'w']], // universal
156
- [':root', ['ws']], // select :root
157
- [':root > *', []], // direct deps of :root
158
- [':root > :root', []], // :root can't depend on itself
159
- [':project#w', ['w']], // select by project + workspace name
160
- ['#w:project', ['w']], // should be interchangeable
161
- ['/* do something */ [name^=w]', ['ws', 'w']], // support comments
162
- ])
163
- const query = new Query({
164
- nodes: new Set(graph.nodes.values()),
165
- edges: graph.edges,
166
- importers: graph.importers,
167
- securityArchive: undefined,
168
- })
169
- for (const [q, expected] of queryToExpected) {
170
- t.strictSame(
171
- (await query.search(q, mockSearchOptions)).nodes.map(
172
- i => i.name,
173
- ),
174
- expected,
175
- `query > "${q}"`,
176
- )
177
- }
178
-
179
- await t.test('comments handling', async t => {
180
- const result = await query.search(
181
- '/* workspace comment */ :root',
182
- mockSearchOptions,
183
- )
184
- t.strictSame(
185
- result.comment,
186
- 'workspace comment',
187
- 'should extract comment from workspace query',
188
- )
189
- })
190
- })
191
-
192
- t.test('cycle', async t => {
193
- const graph = getCycleGraph()
194
- const queryToExpected = new Set<TestCase>([
195
- ['', []], // should match no deps on missing query
196
- ['*', ['cycle-project', 'a', 'b']], // universal
197
- [':root', ['cycle-project']], // select :root
198
- [':root > *', ['a']], // direct deps of :root
199
- [':root > :root', []], // :root can depend on itself
200
- ['/* do something */ [name^=a]', ['a']], // support comments
201
- [':root > :prod > *', ['b']], // mixed selectors
202
- ])
203
- const query = new Query({
204
- nodes: new Set(graph.nodes.values()),
205
- edges: graph.edges,
206
- importers: graph.importers,
207
- securityArchive: undefined,
208
- })
209
- for (const [q, expected] of queryToExpected) {
210
- t.strictSame(
211
- (await query.search(q, mockSearchOptions)).nodes.map(
212
- i => i.name,
213
- ),
214
- expected,
215
- `query > "${q}"`,
216
- )
217
- }
218
- })
219
-
220
- t.test('insights', async t => {
221
- const graph = getSimpleGraph()
222
- const queryToExpected = new Set<TestCase>([
223
- ['*', ['my-project', 'a', 'b', 'c', 'd', 'e', 'f', '@x/y']], // universal
224
- ['*:severity(high)', ['e']], // universal with severity filter
225
- ['*:cve(CVE-2023-1234)', ['e']], // match cve by id
226
- ])
227
- const query = new Query({
228
- nodes: new Set(graph.nodes.values()),
229
- edges: graph.edges,
230
- importers: graph.importers,
231
- securityArchive: asSecurityArchiveLike(
232
- new Map([
233
- [
234
- joinDepIDTuple(['registry', '', 'e@1.0.0']),
235
- asPackageReportData({
236
- id: 'e@1.0.0',
237
- author: [],
238
- size: 0,
239
- type: 'npm',
240
- name: 'e',
241
- version: '1.0.0',
242
- license: 'MIT',
243
- score: {
244
- overall: 0.5,
245
- security: 0.3,
246
- maintenance: 0.7,
247
- popularity: 0.6,
248
- },
249
- alerts: [
250
- {
251
- key: '12314320948130',
252
- type: 'cve',
253
- severity: 'high',
254
- category: 'security',
255
- props: {
256
- cveId: 'CVE-2023-1234',
257
- lastPublish: '2023-01-01',
258
- cwes: [{ id: 'CWE-1234' }],
259
- },
260
- },
261
- ],
262
- }),
263
- ],
264
- ]),
265
- ),
266
- })
267
- for (const [q, expected] of queryToExpected) {
268
- const result = await query.search(q, mockSearchOptions)
269
- t.strictSame(
270
- result.nodes.map(i => i.name),
271
- expected,
272
- `query > "${q}"`,
273
- )
274
-
275
- // Verify score values for node 'e'
276
- const nodeE = result.nodes.find(n => n.name === 'e')
277
- if (nodeE) {
278
- t.strictSame(
279
- nodeE.insights.score,
280
- {
281
- overall: 0.5,
282
- security: 0.3,
283
- maintenance: 0.7,
284
- popularity: 0.6,
285
- },
286
- 'node e should have correct score values',
287
- )
288
- }
289
- }
290
- })
291
-
292
- t.test('parts', async t => {
293
- const graph = getSimpleGraph()
294
- const opts = {
295
- nodes: new Set(graph.nodes.values()),
296
- edges: graph.edges,
297
- importers: graph.importers,
298
- securityArchive: undefined,
299
- }
300
- const query = new Query({ ...opts, partSelectors: ['skill'] })
301
- const search = (q: string) => query.search(q, mockSearchOptions)
302
- const parts = async (q: string) =>
303
- (await search(q)).parts?.map(({ nodes, ...p }) => ({
304
- ...p,
305
- nodes: nodes.map(n => n.name).sort(),
306
- }))
307
-
308
- t.strictSame(await parts(':root > *, #b:skill(x)'), [
309
- { nodes: ['@x/y', 'a', 'b', 'e'] },
310
- { name: 'skill', patterns: ['x'], nodes: ['b'] },
311
- ])
312
- const res = await search(':root > *, #b:skill(x)')
313
- const [plain, b] = res.parts ?? []
314
- t.notOk(plain && 'name' in plain, 'plain part has no name')
315
- t.notOk(plain && 'patterns' in plain, 'plain part has no patterns')
316
- t.equal(
317
- b?.nodes[0],
318
- res.nodes.find(n => n.name === 'b'),
319
- 'part nodes are response nodes',
320
- )
321
- t.ok(b?.nodes[0]?.insights, 'part nodes are hydrated')
322
- t.equal(
323
- await search(':root > *, #b:skill(x)'),
324
- res,
325
- 'cache hit keeps parts',
326
- )
327
-
328
- t.strictSame(await parts(':root > *:not(#b), #b:skill(x)'), [
329
- { nodes: ['@x/y', 'a', 'e'] },
330
- { name: 'skill', patterns: ['x'], nodes: ['b'] },
331
- ])
332
- t.strictSame(await parts('#a, #b:skill(x), #c:skill(y)'), [
333
- { nodes: ['a'] },
334
- { name: 'skill', patterns: ['x'], nodes: ['b'] },
335
- { name: 'skill', patterns: ['y'], nodes: ['c'] },
336
- ])
337
- t.strictSame(await parts('#b:skill(x), #a'), [
338
- { name: 'skill', patterns: ['x'], nodes: ['b'] },
339
- { nodes: ['a'] },
340
- ])
341
- t.strictSame(await parts('#nope:skill(x)'), [
342
- { name: 'skill', patterns: ['x'], nodes: [] },
343
- ])
344
- t.strictSame(await parts('#a, #b:skills(x)'), [
345
- { nodes: ['a'] },
346
- { nodes: [] },
347
- ])
348
- t.notOk('parts' in (await search('')), 'empty query')
349
-
350
- const plainQuery = new Query(opts)
351
- t.notOk(
352
- 'parts' in
353
- (await plainQuery.search(':root > *', mockSearchOptions)),
354
- 'no parts without partSelectors',
355
- )
356
- })
357
-
358
- t.test(':skills', async t => {
359
- const graph = getSimpleGraph()
360
- const query = new Query({
361
- nodes: new Set(graph.nodes.values()),
362
- edges: graph.edges,
363
- importers: graph.importers,
364
- securityArchive: undefined,
365
- hasSkills: async node => node.name === 'c' || node.name === 'e',
366
- })
367
- const names = async (q: string) =>
368
- (await query.search(q, mockSearchOptions)).nodes
369
- .map(n => n.name)
370
- .sort()
371
- t.strictSame(await names(':skills'), ['c', 'e'])
372
- t.strictSame(await names(':root > :skills'), ['e'])
373
- t.strictSame(await names(':not(:skills)'), [
374
- '@x/y',
375
- 'a',
376
- 'b',
377
- 'd',
378
- 'f',
379
- 'my-project',
380
- ])
381
- t.strictSame(await names(':is(:skills)'), ['c', 'e'])
382
- t.strictSame(await names(':has(:skills)'), ['b', 'd', 'my-project'])
383
- })
384
-
385
- t.test('bad selector type', async t => {
386
- await t.rejects(
387
- walk(testBrokenState()),
388
- {
389
- message: 'Missing parser for query node: bork',
390
- cause: { code: 'EQUERY' },
391
- },
392
- 'should throw a parser error',
393
- )
394
- })
395
-
396
- t.test('bad selector type [loose mode]', async t => {
397
- await t.resolves(
398
- walk({ ...testBrokenState(), loose: true }),
399
- 'should resolve with no error',
400
- )
401
- })
402
-
403
- t.test('trying to use tag selectors', async t => {
404
- const graph = getSimpleGraph()
405
- const search = (query: string) =>
406
- new Query({
407
- nodes: new Set(graph.nodes.values()),
408
- edges: graph.edges,
409
- importers: graph.importers,
410
- securityArchive: undefined,
411
- }).search(query, mockSearchOptions)
412
-
413
- await t.rejects(
414
- search('foo'),
415
- {
416
- message: 'Unsupported selector',
417
- // the parsed node is circular, only its text may be reported
418
- cause: { code: 'EQUERY', found: 'foo', wanted: '#foo' },
419
- },
420
- 'should throw an unsupported selector error',
421
- )
422
-
423
- await t.rejects(
424
- search('.dev'),
425
- { cause: { code: 'EQUERY', found: '.dev', wanted: ':dev' } },
426
- 'should suggest the pseudo-class of the same name',
427
- )
428
-
429
- await t.rejects(
430
- search('skill'),
431
- { cause: { code: 'EQUERY', found: 'skill', wanted: '#skill' } },
432
- 'should not suggest a part selector',
433
- )
434
-
435
- await t.rejects(
436
- search('.foo'),
437
- { cause: { code: 'EQUERY', found: '.foo', wanted: '#foo' } },
438
- 'should suggest an id selector when there is no such pseudo-class',
439
- )
440
-
441
- await t.rejects(
442
- search('.'),
443
- { cause: { code: 'EQUERY', found: '.', wanted: undefined } },
444
- 'should not guess when there is no name to guess from',
445
- )
446
- })
447
-
448
- t.test('trying to use string selectors', async t => {
449
- const graph = getSimpleGraph()
450
- await t.rejects(
451
- new Query({
452
- nodes: new Set(graph.nodes.values()),
453
- edges: graph.edges,
454
- importers: graph.importers,
455
- securityArchive: undefined,
456
- }).search('"foo"', mockSearchOptions),
457
- {
458
- message: 'Unsupported selector',
459
- cause: { code: 'EQUERY', found: '"foo"' },
460
- },
461
- 'should throw an unsupported selector error',
462
- )
463
- })
464
-
465
- t.test('malformed query syntax', async t => {
466
- const graph = getSimpleGraph()
467
- await t.rejects(
468
- new Query({
469
- nodes: new Set(graph.nodes.values()),
470
- edges: graph.edges,
471
- importers: graph.importers,
472
- securityArchive: undefined,
473
- }).search(':outdated(', mockSearchOptions),
474
- {
475
- name: 'SyntaxError',
476
- message: 'Invalid query syntax: unexpected end of input',
477
- cause: { code: 'EQUERY', found: ':outdated(' },
478
- },
479
- 'should throw a query syntax error',
480
- )
481
- })
482
-
483
- t.test('cancellable search', async t => {
484
- const graph = getSingleWorkspaceGraph()
485
- const query = new Query({
486
- nodes: new Set(graph.nodes.values()),
487
- edges: graph.edges,
488
- importers: graph.importers,
489
- securityArchive: undefined,
490
- })
491
- const ac = new AbortController()
492
- const q = ':root > * > *'
493
- await t.rejects(
494
- new Promise((_, reject) => {
495
- void query
496
- .search(q, { signal: ac.signal })
497
- .catch((err: unknown) => {
498
- reject(err)
499
- })
500
- ac.abort(new Error('query aborted'))
501
- // the set timeout bellow should never be called since
502
- // the search should throw the abort error before
503
- setTimeout(() => {
504
- reject(new Error('ERR'))
505
- }, 1000)
506
- }),
507
- /query aborted/,
508
- 'should reject with abort error',
509
- )
510
- })
511
-
512
- t.test('Query.hasSecuritySelectors', async t => {
513
- t.ok(
514
- Query.hasSecuritySelectors(':root > *:unmaintained'),
515
- 'should return true',
516
- )
517
- t.ok(
518
- Query.hasSecuritySelectors(':root > *:has(:unmaintained)'),
519
- 'should return true',
520
- )
521
- t.ok(
522
- Query.hasSecuritySelectors(':unmaintained'),
523
- 'should return true',
524
- )
525
- t.ok(
526
- Query.hasSecuritySelectors(':vuln'),
527
- 'should return true for :vuln',
528
- )
529
- t.ok(
530
- Query.hasSecuritySelectors(':vulnerable'),
531
- 'should return true for :vulnerable',
532
- )
533
- t.notOk(Query.hasSecuritySelectors(':foo'), 'should return false')
534
- t.notOk(Query.hasSecuritySelectors(':has'), 'should return false')
535
- })
536
-
537
- t.test('getQueryTokens', t => {
538
- const normalizeParseOutput = (nodes: ParsedSelectorToken[]) => {
539
- return nodes.map(node => {
540
- return {
541
- type: node.type,
542
- token: node.token,
543
- }
544
- })
545
- }
546
-
547
- t.test('should getQueryTokens empty query', t => {
548
- t.strictSame(normalizeParseOutput(Query.getQueryTokens('')), [])
549
- t.end()
550
- })
551
-
552
- t.test('should getQueryTokens pseudo selector', t => {
553
- t.strictSame(
554
- normalizeParseOutput(Query.getQueryTokens(':root')),
555
- [
556
- {
557
- type: 'pseudo',
558
- token: ':root',
559
- },
560
- ],
561
- )
562
- t.end()
563
- })
564
-
565
- t.test('should getQueryTokens attribute selector', t => {
566
- t.test('with value', t => {
567
- t.strictSame(
568
- normalizeParseOutput(Query.getQueryTokens('[name="react"]')),
569
- [
570
- {
571
- type: 'attribute',
572
- token: '[name="react"]',
573
- },
574
- ],
575
- )
576
- t.end()
577
- })
578
-
579
- t.test('with operator', t => {
580
- t.strictSame(
581
- normalizeParseOutput(Query.getQueryTokens('[name^="react"]')),
582
- [
583
- {
584
- type: 'attribute',
585
- token: '[name^="react"]',
586
- },
587
- ],
588
- )
589
- t.end()
590
- })
591
-
592
- t.test('without value', t => {
593
- t.strictSame(
594
- normalizeParseOutput(Query.getQueryTokens('[name]')),
595
- [
596
- {
597
- type: 'attribute',
598
- token: '[name]',
599
- },
600
- ],
601
- )
602
- t.end()
603
- })
604
- t.end()
605
- })
606
-
607
- t.test('should getQueryTokens class selector', t => {
608
- t.strictSame(
609
- normalizeParseOutput(Query.getQueryTokens('.container')),
610
- [
611
- {
612
- type: 'tag',
613
- token: '.container',
614
- },
615
- ],
616
- )
617
- t.end()
618
- })
619
-
620
- t.test('should getQueryTokens id selector', t => {
621
- t.strictSame(
622
- normalizeParseOutput(Query.getQueryTokens('#main')),
623
- [
624
- {
625
- type: 'id',
626
- token: '#main',
627
- },
628
- ],
629
- )
630
-
631
- t.test('should getQueryTokens scoped-name id selector', t => {
632
- t.strictSame(
633
- normalizeParseOutput(Query.getQueryTokens('#@scoped/name')),
634
- [
635
- {
636
- type: 'id',
637
- token: '#@scoped/name',
638
- },
639
- ],
640
- )
641
- t.end()
642
- })
643
-
644
- t.test(
645
- 'should getQueryTokens dash-separated-scoped-name id selector',
646
- t => {
647
- t.strictSame(
648
- normalizeParseOutput(
649
- Query.getQueryTokens('#@scoped-org/name'),
650
- ),
651
- [
652
- {
653
- type: 'id',
654
- token: '#@scoped-org/name',
655
- },
656
- ],
657
- )
658
- t.end()
659
- },
660
- )
661
-
662
- t.test(
663
- 'should getQueryTokens dot-separated-scoped-name id selector',
664
- t => {
665
- t.strictSame(
666
- normalizeParseOutput(
667
- Query.getQueryTokens('#@scoped.org/name'),
668
- ),
669
- [
670
- {
671
- type: 'id',
672
- token: '#@scoped.org/name',
673
- },
674
- ],
675
- )
676
- t.end()
677
- },
678
- )
679
- t.end()
680
- })
681
-
682
- t.test(
683
- 'should getQueryTokens simple comma separated selector',
684
- t => {
685
- t.strictSame(
686
- normalizeParseOutput(Query.getQueryTokens('#a, #b')),
687
- [
688
- {
689
- type: 'id',
690
- token: '#a',
691
- },
692
- {
693
- type: 'selector',
694
- token: ',',
695
- },
696
- {
697
- type: 'id',
698
- token: ' #b',
699
- },
700
- ],
701
- )
702
- t.end()
703
- },
704
- )
705
-
706
- t.test('should getQueryTokens tags', t => {
707
- t.strictSame(
708
- normalizeParseOutput(Query.getQueryTokens('name=test')),
709
- [
710
- {
711
- type: 'tag',
712
- token: 'name=test',
713
- },
714
- ],
715
- )
716
- t.end()
717
- })
718
-
719
- t.test('should getQueryTokens strings', t => {
720
- t.strictSame(
721
- normalizeParseOutput(Query.getQueryTokens('name="test"')),
722
- [
723
- {
724
- type: 'tag',
725
- token: 'name=',
726
- },
727
- {
728
- type: 'string',
729
- token: '"test"',
730
- },
731
- ],
732
- )
733
- t.end()
734
- })
735
-
736
- t.test('should getQueryTokens combinator', t => {
737
- t.test('non-whitespace', t => {
738
- t.strictSame(normalizeParseOutput(Query.getQueryTokens('>')), [
739
- {
740
- type: 'combinator',
741
- token: '>',
742
- },
743
- ])
744
- t.end()
745
- })
746
-
747
- t.test('whitespace', t => {
748
- t.strictSame(Query.getQueryTokens(' '), [])
749
- t.end()
750
- })
751
- t.end()
752
- })
753
-
754
- t.test('should getQueryTokens complex selector', t => {
755
- t.test('with nested selectors', t => {
756
- t.strictSame(
757
- normalizeParseOutput(
758
- Query.getQueryTokens(':root > [name="react"] .container'),
759
- ),
760
- [
761
- {
762
- type: 'pseudo',
763
- token: ':root',
764
- },
765
- {
766
- type: 'combinator',
767
- token: ' > ',
768
- },
769
- {
770
- type: 'attribute',
771
- token: '[name="react"]',
772
- },
773
- {
774
- type: 'combinator',
775
- token: ' ',
776
- },
777
- {
778
- type: 'tag',
779
- token: '.container',
780
- },
781
- ],
782
- )
783
- t.end()
784
- })
785
-
786
- t.test('with nested pseudo selectors', t => {
787
- t.strictSame(
788
- normalizeParseOutput(Query.getQueryTokens(':not(#react)')),
789
- [
790
- {
791
- type: 'pseudo',
792
- token: ':not(',
793
- },
794
- {
795
- type: 'id',
796
- token: '#react',
797
- },
798
- {
799
- type: 'pseudo',
800
- token: ')',
801
- },
802
- ],
803
- )
804
- t.test('with nested comma separated selectors', t => {
805
- t.strictSame(
806
- normalizeParseOutput(
807
- Query.getQueryTokens(':not(#a, #b), :is(#c)'),
808
- ),
809
- [
810
- {
811
- type: 'pseudo',
812
- token: ':not(',
813
- },
814
- {
815
- type: 'id',
816
- token: '#a',
817
- },
818
- {
819
- type: 'selector',
820
- token: ',',
821
- },
822
- {
823
- type: 'id',
824
- token: ' #b',
825
- },
826
- {
827
- type: 'pseudo',
828
- token: ')',
829
- },
830
- {
831
- type: 'selector',
832
- token: ',',
833
- },
834
- {
835
- type: 'pseudo',
836
- token: ' :is(',
837
- },
838
- {
839
- type: 'id',
840
- token: '#c',
841
- },
842
- {
843
- type: 'pseudo',
844
- token: ')',
845
- },
846
- ],
847
- )
848
- t.end()
849
- })
850
- t.end()
851
- })
852
-
853
- t.test('with malformed queries', t => {
854
- t.test('should handle unclosed brackets', t => {
855
- t.strictSame(
856
- normalizeParseOutput(Query.getQueryTokens('[name="react"')),
857
- [],
858
- )
859
- t.end()
860
- })
861
-
862
- t.test('should handle unclosed pseudo selectors', t => {
863
- t.strictSame(
864
- normalizeParseOutput(Query.getQueryTokens(':not(#react')),
865
- [
866
- {
867
- type: 'pseudo',
868
- token: ':not',
869
- },
870
- ],
871
- )
872
- t.end()
873
- })
874
- t.end()
875
- })
876
-
877
- t.test('with multiple attributes', t => {
878
- t.strictSame(
879
- normalizeParseOutput(
880
- Query.getQueryTokens('[name="react"][version="18"]'),
881
- ),
882
- [
883
- {
884
- type: 'attribute',
885
- token: '[name="react"]',
886
- },
887
- {
888
- type: 'attribute',
889
- token: '[version="18"]',
890
- },
891
- ],
892
- )
893
- t.end()
894
- })
895
- t.end()
896
- })
897
-
898
- t.end()
899
- })
900
-
901
- t.test(':scope with custom scopeIDs', async t => {
902
- const graph = getSimpleGraph()
903
- const query = new Query({
904
- nodes: new Set(graph.nodes.values()),
905
- edges: graph.edges,
906
- importers: graph.importers,
907
- securityArchive: undefined,
908
- })
909
- const customScopeID = joinDepIDTuple(['registry', '', 'a@1.0.0'])
910
- t.strictSame(
911
- (
912
- await query.search(':scope', {
913
- ...mockSearchOptions,
914
- scopeIDs: [customScopeID],
915
- })
916
- ).nodes.map(n => n.name),
917
- ['a'],
918
- 'should select only the node specified by scopeIDs',
919
- )
920
- })
921
-
922
- t.test('specificity calculation', async t => {
923
- const graph = getSimpleGraph()
924
- const query = new Query({
925
- nodes: new Set(graph.nodes.values()),
926
- edges: graph.edges,
927
- importers: graph.importers,
928
- securityArchive: undefined,
929
- })
930
-
931
- const specificityTestCases = [
932
- {
933
- query: ':root',
934
- idCounter: 0,
935
- commonCounter: 1,
936
- description: 'single pseudo-element',
937
- },
938
- {
939
- query: ':root > #b',
940
- idCounter: 1,
941
- commonCounter: 1,
942
- description: 'child combinator with id',
943
- },
944
- {
945
- query: ':root #b',
946
- idCounter: 1,
947
- commonCounter: 1,
948
- description: 'descendant combinator with id',
949
- },
950
- {
951
- query: ':root ~ #b',
952
- idCounter: 1,
953
- commonCounter: 1,
954
- description: 'sibling combinator with id',
955
- },
956
- {
957
- query: ':root > #b > #c',
958
- idCounter: 2,
959
- commonCounter: 1,
960
- description: 'multiple combinators with ids',
961
- },
962
- {
963
- query: ':root > :prod',
964
- idCounter: 0,
965
- commonCounter: 2,
966
- description: 'only pseudo classes',
967
- },
968
- {
969
- query: ':root > :prod:dev',
970
- idCounter: 0,
971
- commonCounter: 3,
972
- description: 'multiple pseudo classes',
973
- },
974
- {
975
- query: ':root > :prod:dev:optional',
976
- idCounter: 0,
977
- commonCounter: 4,
978
- description: 'multiple pseudo classes',
979
- },
980
- {
981
- query: ':root > #missing',
982
- idCounter: 1,
983
- commonCounter: 1,
984
- description: 'missing id still counts',
985
- },
986
- {
987
- query: ':root > [name=b]',
988
- idCounter: 0,
989
- commonCounter: 2,
990
- description: 'attribute selector',
991
- },
992
- {
993
- query: ':root > [name=b][version=2.0.0]',
994
- idCounter: 0,
995
- commonCounter: 3,
996
- description: 'multiple attribute selectors',
997
- },
998
- {
999
- query: ':root > :not(:prod)',
1000
- idCounter: 0,
1001
- commonCounter: 1,
1002
- description: ':not does not affect specificity',
1003
- },
1004
- {
1005
- query: ':root > :is(:prod, :dev)',
1006
- idCounter: 0,
1007
- commonCounter: 1,
1008
- description: ':is does not affect specificity',
1009
- },
1010
- {
1011
- query: ':root > :has(:prod)',
1012
- idCounter: 0,
1013
- commonCounter: 1,
1014
- description: ':has does not affect specificity',
1015
- },
1016
- {
1017
- query: '#b:prod',
1018
- idCounter: 1,
1019
- commonCounter: 1,
1020
- description: 'id with pseudo class',
1021
- },
1022
- {
1023
- query: '#b:prod[name=b]',
1024
- idCounter: 1,
1025
- commonCounter: 2,
1026
- description: 'id with pseudo class and attribute',
1027
- },
1028
- ]
1029
-
1030
- for (const {
1031
- query: queryStr,
1032
- idCounter,
1033
- commonCounter,
1034
- description,
1035
- } of specificityTestCases) {
1036
- const result = await query.search(queryStr, mockSearchOptions)
1037
- t.strictSame(
1038
- {
1039
- idCounter: result.specificity.idCounter,
1040
- commonCounter: result.specificity.commonCounter,
1041
- },
1042
- { idCounter, commonCounter },
1043
- `specificity for "${queryStr}" - ${description}`,
1044
- )
1045
- }
1046
- })
1047
-
1048
- t.test('specificitySort', async t => {
1049
- // Create test QueryResponse objects with different specificity values
1050
- const responses: QueryResponse[] = [
1051
- {
1052
- edges: [],
1053
- nodes: [],
1054
- importers: [],
1055
- comment: 'First response (id:0, common:1)',
1056
- specificity: { idCounter: 0, commonCounter: 1 },
1057
- },
1058
- {
1059
- edges: [],
1060
- nodes: [],
1061
- importers: [],
1062
- comment: 'Second response (id:2, common:0)',
1063
- specificity: { idCounter: 2, commonCounter: 0 },
1064
- },
1065
- {
1066
- edges: [],
1067
- nodes: [],
1068
- importers: [],
1069
- comment: 'Third response (id:1, common:3)',
1070
- specificity: { idCounter: 1, commonCounter: 3 },
1071
- },
1072
- {
1073
- edges: [],
1074
- nodes: [],
1075
- importers: [],
1076
- comment: 'Fourth response (id:1, common:2)',
1077
- specificity: { idCounter: 1, commonCounter: 2 },
1078
- },
1079
- {
1080
- edges: [],
1081
- nodes: [],
1082
- importers: [],
1083
- comment: 'Fifth response (id:0, common:0)',
1084
- specificity: { idCounter: 0, commonCounter: 0 },
1085
- },
1086
- ]
1087
-
1088
- const sortedResponses = Query.specificitySort(responses)
1089
-
1090
- // Check that the order is correct based on specificity rules
1091
- t.strictSame(
1092
- sortedResponses.map(r => r.comment),
1093
- [
1094
- 'Second response (id:2, common:0)', // Highest idCounter (2)
1095
- 'Third response (id:1, common:3)', // Next idCounter (1) with higher commonCounter (3)
1096
- 'Fourth response (id:1, common:2)', // Same idCounter (1) with lower commonCounter (2)
1097
- 'First response (id:0, common:1)', // Lowest idCounter (0) with higher commonCounter (1)
1098
- 'Fifth response (id:0, common:0)', // Lowest idCounter (0) with lowest commonCounter (0)
1099
- ],
1100
- 'should sort responses by specificity with higher values first',
1101
- )
1102
-
1103
- // Test that the original array is not modified
1104
- t.notSame(
1105
- responses.map(r => r.comment),
1106
- sortedResponses.map(r => r.comment),
1107
- 'should not modify the original array',
1108
- )
1109
-
1110
- // Test that the sort is stable (preserves original order for equal specificity)
1111
- const equalSpecificityResponses: QueryResponse[] = [
1112
- {
1113
- edges: [],
1114
- nodes: [],
1115
- importers: [],
1116
- comment: 'First equal response',
1117
- specificity: { idCounter: 1, commonCounter: 1 },
1118
- },
1119
- {
1120
- edges: [],
1121
- nodes: [],
1122
- importers: [],
1123
- comment: 'Second equal response',
1124
- specificity: { idCounter: 1, commonCounter: 1 },
1125
- },
1126
- {
1127
- edges: [],
1128
- nodes: [],
1129
- importers: [],
1130
- comment: 'Third equal response',
1131
- specificity: { idCounter: 1, commonCounter: 1 },
1132
- },
1133
- ]
1134
-
1135
- const sortedEqualResponses = Query.specificitySort(
1136
- equalSpecificityResponses,
1137
- )
1138
-
1139
- t.strictSame(
1140
- sortedEqualResponses.map(r => r.comment),
1141
- [
1142
- 'First equal response',
1143
- 'Second equal response',
1144
- 'Third equal response',
1145
- ],
1146
- 'should preserve original order for equal specificity values',
1147
- )
1148
- })