@adea-ai/plugins 1.0.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/LICENSE +201 -0
- package/README.md +74 -0
- package/dist/agent-plugins-catalog.d.ts +26 -0
- package/dist/agent-plugins-catalog.js +699 -0
- package/dist/agent-plugins-catalog.js.map +1 -0
- package/dist/agent-plugins-harness.d.ts +54 -0
- package/dist/agent-plugins-harness.js +212 -0
- package/dist/agent-plugins-harness.js.map +1 -0
- package/dist/agent-plugins-schema.d.ts +402 -0
- package/dist/agent-plugins-schema.js +200 -0
- package/dist/agent-plugins-schema.js.map +1 -0
- package/dist/consumer-index.d.ts +217 -0
- package/dist/consumer-index.js +358 -0
- package/dist/consumer-index.js.map +1 -0
- package/dist/display-name.d.ts +8 -0
- package/dist/display-name.js +160 -0
- package/dist/display-name.js.map +1 -0
- package/dist/harness.d.ts +13 -0
- package/dist/harness.js +169 -0
- package/dist/harness.js.map +1 -0
- package/dist/icon-mirror.d.ts +69 -0
- package/dist/icon-mirror.js +147 -0
- package/dist/icon-mirror.js.map +1 -0
- package/dist/icons.d.ts +65 -0
- package/dist/icons.js +268 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +304 -0
- package/dist/index.js +1877 -0
- package/dist/index.js.map +1 -0
- package/dist/portable-catalog.d.ts +9 -0
- package/dist/portable-catalog.js +175 -0
- package/dist/portable-catalog.js.map +1 -0
- package/dist/publication.d.ts +98 -0
- package/dist/publication.js +121 -0
- package/dist/publication.js.map +1 -0
- package/dist/schema.d.ts +705 -0
- package/dist/schema.js +267 -0
- package/dist/schema.js.map +1 -0
- package/dist/site-icons.d.ts +55 -0
- package/dist/site-icons.js +155 -0
- package/dist/site-icons.js.map +1 -0
- package/dist/sources.d.ts +54 -0
- package/dist/sources.js +306 -0
- package/dist/sources.js.map +1 -0
- package/package.json +102 -0
- package/schemas/agent-package.v1.json +387 -0
- package/schemas/catalog.v1.schema.json +407 -0
- package/schemas/compatibility.v1.schema.json +23 -0
- package/schemas/harness-profile.v1.json +75 -0
- package/schemas/installation-plan.v2.json +695 -0
- package/schemas/sources-lock.v1.schema.json +61 -0
- package/src/agent-plugins-catalog.test.ts +402 -0
- package/src/agent-plugins-catalog.ts +800 -0
- package/src/agent-plugins-harness.test.ts +272 -0
- package/src/agent-plugins-harness.ts +281 -0
- package/src/agent-plugins-schema.test.ts +27 -0
- package/src/agent-plugins-schema.ts +219 -0
- package/src/artifacts.test.ts +545 -0
- package/src/catalog-installation.test.ts +42 -0
- package/src/consumer-index.test.ts +465 -0
- package/src/consumer-index.ts +592 -0
- package/src/display-name.test.ts +56 -0
- package/src/display-name.ts +157 -0
- package/src/harness.ts +202 -0
- package/src/icon-mirror.ts +196 -0
- package/src/icons.test.ts +185 -0
- package/src/icons.ts +297 -0
- package/src/index.test.ts +888 -0
- package/src/index.ts +2538 -0
- package/src/portable-catalog.test.ts +186 -0
- package/src/portable-catalog.ts +203 -0
- package/src/publication.test.ts +117 -0
- package/src/publication.ts +171 -0
- package/src/schema.ts +309 -0
- package/src/site-icon-fallback.test.ts +267 -0
- package/src/site-icons.test.ts +171 -0
- package/src/site-icons.ts +161 -0
- package/src/sources.ts +360 -0
- package/src/upstream-headers.test.ts +148 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { Catalog, Plugin } from './schema.js'
|
|
3
|
+
import {
|
|
4
|
+
buildConsumerIndex,
|
|
5
|
+
DEFAULT_SOURCE_PREFERENCE,
|
|
6
|
+
parseProductPreference,
|
|
7
|
+
type ProductPreference,
|
|
8
|
+
} from './consumer-index.js'
|
|
9
|
+
import { createArtifacts, verifyConsumerIndex } from './index.js'
|
|
10
|
+
|
|
11
|
+
const preference: ProductPreference = {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
sourcePreference: [...DEFAULT_SOURCE_PREFERENCE],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PluginInput {
|
|
17
|
+
sourceId: string
|
|
18
|
+
name: string
|
|
19
|
+
categories: string[]
|
|
20
|
+
displayName?: string
|
|
21
|
+
releaseRepositoryUrl?: string
|
|
22
|
+
productGroupingKey?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function plugin(input: PluginInput): Plugin {
|
|
26
|
+
return {
|
|
27
|
+
pluginId: `plugin:${input.sourceId}:${input.name}`,
|
|
28
|
+
displayName: input.displayName ?? input.name,
|
|
29
|
+
description: `${input.name} description`,
|
|
30
|
+
productGroupingKey: input.productGroupingKey ?? input.name,
|
|
31
|
+
categories: input.categories,
|
|
32
|
+
keywords: [input.name],
|
|
33
|
+
authors: ['Test'],
|
|
34
|
+
icons: [],
|
|
35
|
+
sourceId: input.sourceId,
|
|
36
|
+
upstreamPluginName: input.name,
|
|
37
|
+
currentReleaseId: `release:${input.sourceId}-${input.name}`,
|
|
38
|
+
// Defaults to the marketplace's own repository, i.e. a wrapper package.
|
|
39
|
+
availableReleases: [
|
|
40
|
+
{
|
|
41
|
+
releaseId: `release:${input.sourceId}-${input.name}`,
|
|
42
|
+
resolvedRepositoryUrl:
|
|
43
|
+
input.releaseRepositoryUrl ?? `https://github.com/${input.sourceId}/marketplace`,
|
|
44
|
+
resolvedCommitSha: 'a'.repeat(40),
|
|
45
|
+
canonicalContentDigest: `sha256:${'d'.repeat(64)}`,
|
|
46
|
+
contentResolution: 'complete',
|
|
47
|
+
capabilities: [],
|
|
48
|
+
requiredConnectors: [],
|
|
49
|
+
requiredCredentials: [],
|
|
50
|
+
releaseMetadata: {},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
capabilitySummary: { 'mcp-server': 1 },
|
|
54
|
+
harnessCompatibility: {
|
|
55
|
+
cursor: { status: 'native', reasons: [], responsibleCapabilities: [] },
|
|
56
|
+
},
|
|
57
|
+
license: { name: 'MIT', source: 'plugin-manifest' },
|
|
58
|
+
provenance: {
|
|
59
|
+
manifestPath: 'marketplace.json',
|
|
60
|
+
pluginSubdirectory: '.',
|
|
61
|
+
repositoryUrl: `https://github.com/${input.sourceId}/marketplace`,
|
|
62
|
+
resolvedCommitSha: 'a'.repeat(40),
|
|
63
|
+
sourceManifestDigest: `sha256:${'d'.repeat(64)}`,
|
|
64
|
+
upstreamEntryDigest: `sha256:${'d'.repeat(64)}`,
|
|
65
|
+
},
|
|
66
|
+
securityClassification: {
|
|
67
|
+
level: 'low',
|
|
68
|
+
reasons: [],
|
|
69
|
+
permissionSensitiveChanges: [],
|
|
70
|
+
contentResolution: 'complete',
|
|
71
|
+
},
|
|
72
|
+
} as unknown as Plugin
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function catalog(plugins: Plugin[]): Catalog {
|
|
76
|
+
const sourceIds = [...new Set(plugins.map((item) => item.sourceId))].toSorted()
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
catalogId: `catalog:${'a'.repeat(64)}`,
|
|
80
|
+
generatedAt: '2026-01-01T00:00:00.000Z',
|
|
81
|
+
sources: sourceIds.map((sourceId) => ({
|
|
82
|
+
sourceId,
|
|
83
|
+
repositoryUrl: `https://github.com/${sourceId}/marketplace`,
|
|
84
|
+
})),
|
|
85
|
+
plugins,
|
|
86
|
+
} as unknown as Catalog
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('product placement', () => {
|
|
90
|
+
test('collapses one product published by several sources into a single entry', () => {
|
|
91
|
+
const index = buildConsumerIndex({
|
|
92
|
+
catalog: catalog([
|
|
93
|
+
plugin({
|
|
94
|
+
sourceId: 'cursor-official',
|
|
95
|
+
name: 'google-calendar',
|
|
96
|
+
categories: ['productivity'],
|
|
97
|
+
}),
|
|
98
|
+
plugin({
|
|
99
|
+
sourceId: 'openai-official',
|
|
100
|
+
name: 'google-calendar',
|
|
101
|
+
categories: ['productivity'],
|
|
102
|
+
}),
|
|
103
|
+
]),
|
|
104
|
+
preference,
|
|
105
|
+
})
|
|
106
|
+
expect(index.counts).toEqual({ plugins: 2, products: 1, redundantPlugins: 1 })
|
|
107
|
+
expect(index.products['google-calendar']).toMatchObject({
|
|
108
|
+
pluginId: 'plugin:cursor-official:google-calendar',
|
|
109
|
+
variantPluginIds: [
|
|
110
|
+
'plugin:cursor-official:google-calendar',
|
|
111
|
+
'plugin:openai-official:google-calendar',
|
|
112
|
+
],
|
|
113
|
+
variantsIdentical: false,
|
|
114
|
+
})
|
|
115
|
+
expect(index.categories[0]!.productKeys).toEqual(['google-calendar'])
|
|
116
|
+
expect(index.categories[0]!.topProductKeys).toEqual(['google-calendar'])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('files a product under every category its variants belong to', () => {
|
|
120
|
+
const base = catalog([
|
|
121
|
+
plugin({ sourceId: 'claude-official', name: 'slack', categories: ['productivity'] }),
|
|
122
|
+
plugin({ sourceId: 'openai-official', name: 'slack', categories: ['communication'] }),
|
|
123
|
+
])
|
|
124
|
+
const index = buildConsumerIndex({ catalog: base, preference })
|
|
125
|
+
// Sources disagree about where Slack belongs; the product belongs in both,
|
|
126
|
+
// while the canonical variant still decides identity.
|
|
127
|
+
expect(index.products.slack!.categories).toEqual(['communication', 'productivity'])
|
|
128
|
+
expect(index.products.slack!.primaryCategory).toBe('communication')
|
|
129
|
+
expect(index.products.slack!.pluginId).toBe('plugin:claude-official:slack')
|
|
130
|
+
expect(index.categories.map((category) => category.category)).toEqual([
|
|
131
|
+
'communication',
|
|
132
|
+
'productivity',
|
|
133
|
+
])
|
|
134
|
+
for (const category of index.categories)
|
|
135
|
+
expect(category.productKeys, category.category).toEqual(['slack'])
|
|
136
|
+
// The published index must satisfy its own membership check.
|
|
137
|
+
expect(() => verifyConsumerIndex(JSON.stringify(index), base)).not.toThrow()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('marks variants that ship byte-identical content', () => {
|
|
141
|
+
const shared = { releaseRepositoryUrl: 'https://github.com/vendor/plugin' }
|
|
142
|
+
const first = plugin({
|
|
143
|
+
sourceId: 'claude-official',
|
|
144
|
+
name: 'atlan',
|
|
145
|
+
categories: ['data-analytics'],
|
|
146
|
+
...shared,
|
|
147
|
+
})
|
|
148
|
+
const second = plugin({
|
|
149
|
+
sourceId: 'knowledge-work-official',
|
|
150
|
+
name: 'atlan',
|
|
151
|
+
categories: ['data-analytics'],
|
|
152
|
+
...shared,
|
|
153
|
+
})
|
|
154
|
+
const identical = buildConsumerIndex({
|
|
155
|
+
catalog: catalog([
|
|
156
|
+
{ ...first, currentReleaseId: 'release:same' },
|
|
157
|
+
{ ...second, currentReleaseId: 'release:same' },
|
|
158
|
+
]),
|
|
159
|
+
preference,
|
|
160
|
+
})
|
|
161
|
+
expect(identical.products.atlan!.variantsIdentical).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('prefers vendor-published content over a marketplace wrapper package', () => {
|
|
165
|
+
const index = buildConsumerIndex({
|
|
166
|
+
catalog: catalog([
|
|
167
|
+
plugin({ sourceId: 'claude-official', name: 'slack', categories: ['communication'] }),
|
|
168
|
+
plugin({
|
|
169
|
+
sourceId: 'openai-official',
|
|
170
|
+
name: 'slack',
|
|
171
|
+
categories: ['communication'],
|
|
172
|
+
releaseRepositoryUrl: 'https://github.com/slackapi/slack-mcp-plugin',
|
|
173
|
+
}),
|
|
174
|
+
]),
|
|
175
|
+
preference,
|
|
176
|
+
})
|
|
177
|
+
expect(index.products.slack!.pluginId).toBe('plugin:openai-official:slack')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('falls back to the configured source order when every variant is a wrapper', () => {
|
|
181
|
+
const index = buildConsumerIndex({
|
|
182
|
+
catalog: catalog([
|
|
183
|
+
plugin({ sourceId: 'openai-official', name: 'github', categories: ['developer-tools'] }),
|
|
184
|
+
plugin({ sourceId: 'claude-official', name: 'github', categories: ['developer-tools'] }),
|
|
185
|
+
]),
|
|
186
|
+
preference,
|
|
187
|
+
})
|
|
188
|
+
expect(index.products.github!.pluginId).toBe('plugin:claude-official:github')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('applies an explicit override ahead of every heuristic', () => {
|
|
192
|
+
const index = buildConsumerIndex({
|
|
193
|
+
catalog: catalog([
|
|
194
|
+
plugin({ sourceId: 'claude-official', name: 'github', categories: ['developer-tools'] }),
|
|
195
|
+
plugin({ sourceId: 'cursor-official', name: 'github', categories: ['developer-tools'] }),
|
|
196
|
+
]),
|
|
197
|
+
preference: {
|
|
198
|
+
...preference,
|
|
199
|
+
canonicalOverrides: { github: 'plugin:cursor-official:github' },
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
expect(index.products.github!.pluginId).toBe('plugin:cursor-official:github')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('prefers a human-formatted label among variants', () => {
|
|
206
|
+
const index = buildConsumerIndex({
|
|
207
|
+
catalog: catalog([
|
|
208
|
+
plugin({ sourceId: 'claude-official', name: 'figma', categories: ['creativity'] }),
|
|
209
|
+
plugin({
|
|
210
|
+
sourceId: 'openai-official',
|
|
211
|
+
name: 'figma',
|
|
212
|
+
categories: ['creativity'],
|
|
213
|
+
displayName: 'Figma',
|
|
214
|
+
}),
|
|
215
|
+
]),
|
|
216
|
+
preference,
|
|
217
|
+
})
|
|
218
|
+
expect(index.products.figma!.displayName).toBe('Figma')
|
|
219
|
+
expect(index.products.figma!.pluginId).toBe('plugin:claude-official:figma')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('places a product from a source the preference list does not name', () => {
|
|
223
|
+
const index = buildConsumerIndex({
|
|
224
|
+
catalog: catalog([
|
|
225
|
+
plugin({ sourceId: 'unlisted-source', name: 'tool', categories: ['developer-tools'] }),
|
|
226
|
+
]),
|
|
227
|
+
preference,
|
|
228
|
+
})
|
|
229
|
+
expect(index.products.tool!.pluginId).toBe('plugin:unlisted-source:tool')
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('category shelves', () => {
|
|
234
|
+
const names = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta']
|
|
235
|
+
|
|
236
|
+
test('orders curated products first and fills the rest deterministically', () => {
|
|
237
|
+
const index = buildConsumerIndex({
|
|
238
|
+
catalog: catalog(
|
|
239
|
+
names.map((name) =>
|
|
240
|
+
plugin({ sourceId: 'claude-official', name, categories: ['productivity'] })
|
|
241
|
+
)
|
|
242
|
+
),
|
|
243
|
+
leading: { productivity: ['zeta', 'gamma'] },
|
|
244
|
+
topCount: 4,
|
|
245
|
+
preference,
|
|
246
|
+
})
|
|
247
|
+
const category = index.categories[0]!
|
|
248
|
+
expect(category.productKeys).toEqual([
|
|
249
|
+
'zeta',
|
|
250
|
+
'gamma',
|
|
251
|
+
'alpha',
|
|
252
|
+
'beta',
|
|
253
|
+
'delta',
|
|
254
|
+
'epsilon',
|
|
255
|
+
'eta',
|
|
256
|
+
])
|
|
257
|
+
expect(category.topProductKeys).toEqual(['zeta', 'gamma', 'alpha', 'beta'])
|
|
258
|
+
expect(index.diagnostics).toEqual([])
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('a curated product is never taken by another category that only fills', () => {
|
|
262
|
+
// 'teaching' sorts first under developer-tools, but education-research
|
|
263
|
+
// curates it, so developer-tools must fill from nothing instead.
|
|
264
|
+
const index = buildConsumerIndex({
|
|
265
|
+
catalog: catalog([
|
|
266
|
+
plugin({
|
|
267
|
+
sourceId: 'cursor-official',
|
|
268
|
+
name: 'teaching',
|
|
269
|
+
categories: ['developer-tools', 'education-research'],
|
|
270
|
+
}),
|
|
271
|
+
]),
|
|
272
|
+
leading: { 'education-research': ['teaching'] },
|
|
273
|
+
preference,
|
|
274
|
+
})
|
|
275
|
+
expect(
|
|
276
|
+
index.categories.find((item) => item.category === 'developer-tools')!.topProductKeys
|
|
277
|
+
).toEqual([])
|
|
278
|
+
expect(
|
|
279
|
+
index.categories.find((item) => item.category === 'education-research')!.topProductKeys
|
|
280
|
+
).toEqual(['teaching'])
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('reports every curated name that no longer resolves and skips it', () => {
|
|
284
|
+
const index = buildConsumerIndex({
|
|
285
|
+
catalog: catalog([
|
|
286
|
+
plugin({ sourceId: 'claude-official', name: 'sentry', categories: ['developer-tools'] }),
|
|
287
|
+
plugin({
|
|
288
|
+
sourceId: 'claude-official',
|
|
289
|
+
name: 'nightvision',
|
|
290
|
+
categories: ['developer-tools'],
|
|
291
|
+
}),
|
|
292
|
+
plugin({ sourceId: 'claude-official', name: 'zscaler', categories: ['security'] }),
|
|
293
|
+
]),
|
|
294
|
+
leading: {
|
|
295
|
+
'developer-tools': ['getsentry', 'sentry'],
|
|
296
|
+
security: ['nightvision', 'no-such-plugin'],
|
|
297
|
+
'unknown-category': ['anything'],
|
|
298
|
+
},
|
|
299
|
+
preference,
|
|
300
|
+
})
|
|
301
|
+
expect(index.diagnostics).toEqual([
|
|
302
|
+
{ category: 'developer-tools', name: 'getsentry', reason: 'PRODUCT_NOT_FOUND' },
|
|
303
|
+
{ category: 'security', name: 'nightvision', reason: 'PRODUCT_NOT_IN_CATEGORY' },
|
|
304
|
+
{ category: 'security', name: 'no-such-plugin', reason: 'PRODUCT_NOT_FOUND' },
|
|
305
|
+
{ category: 'unknown-category', name: 'anything', reason: 'UNKNOWN_CATEGORY' },
|
|
306
|
+
])
|
|
307
|
+
const developer = index.categories.find((item) => item.category === 'developer-tools')!
|
|
308
|
+
// The stale name leaves no slot behind; only the real product is shelved.
|
|
309
|
+
expect(developer.topProductKeys).toEqual(['sentry', 'nightvision'])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('is deterministic for the same catalog in any input order', () => {
|
|
313
|
+
const forward = names
|
|
314
|
+
.slice(0, 3)
|
|
315
|
+
.map((name) => plugin({ sourceId: 'claude-official', name, categories: ['productivity'] }))
|
|
316
|
+
const first = buildConsumerIndex({
|
|
317
|
+
catalog: catalog(forward),
|
|
318
|
+
leading: { productivity: ['gamma'] },
|
|
319
|
+
preference,
|
|
320
|
+
})
|
|
321
|
+
const second = buildConsumerIndex({
|
|
322
|
+
catalog: catalog(forward.toReversed()),
|
|
323
|
+
leading: { productivity: ['gamma'] },
|
|
324
|
+
preference,
|
|
325
|
+
})
|
|
326
|
+
expect(JSON.stringify(first)).toBe(JSON.stringify(second))
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
describe('product preference configuration', () => {
|
|
331
|
+
test('rejects malformed placement configuration', () => {
|
|
332
|
+
expect(() => parseProductPreference(null)).toThrow('PRODUCT_PREFERENCE_INVALID')
|
|
333
|
+
expect(() => parseProductPreference({ schemaVersion: 2, sourcePreference: ['a'] })).toThrow(
|
|
334
|
+
'PRODUCT_PREFERENCE_INVALID'
|
|
335
|
+
)
|
|
336
|
+
expect(() => parseProductPreference({ schemaVersion: 1, sourcePreference: [] })).toThrow(
|
|
337
|
+
'PRODUCT_PREFERENCE_INVALID'
|
|
338
|
+
)
|
|
339
|
+
expect(() =>
|
|
340
|
+
parseProductPreference({ schemaVersion: 1, sourcePreference: ['a', 'a'] })
|
|
341
|
+
).toThrow('PRODUCT_PREFERENCE_DUPLICATE_SOURCE')
|
|
342
|
+
expect(() =>
|
|
343
|
+
parseProductPreference({
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
sourcePreference: ['a'],
|
|
346
|
+
canonicalOverrides: { tool: 'tool' },
|
|
347
|
+
})
|
|
348
|
+
).toThrow('PRODUCT_PREFERENCE_OVERRIDE_INVALID: tool')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('accepts and normalizes the checked-in configuration shape', () => {
|
|
352
|
+
expect(
|
|
353
|
+
parseProductPreference({
|
|
354
|
+
schemaVersion: 1,
|
|
355
|
+
sourcePreference: ['claude-official'],
|
|
356
|
+
canonicalOverrides: { github: 'plugin:claude-official:github' },
|
|
357
|
+
})
|
|
358
|
+
).toEqual({
|
|
359
|
+
schemaVersion: 1,
|
|
360
|
+
sourcePreference: ['claude-official'],
|
|
361
|
+
canonicalOverrides: { github: 'plugin:claude-official:github' },
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
describe('artifact publication', () => {
|
|
367
|
+
const lock = {
|
|
368
|
+
schemaVersion: 1 as const,
|
|
369
|
+
lockId: `lock:${'b'.repeat(64)}`,
|
|
370
|
+
sources: [],
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function publishedDiagnostics(curationDiagnostics?: boolean) {
|
|
374
|
+
const base = catalog([
|
|
375
|
+
plugin({ sourceId: 'claude-official', name: 'sentry', categories: ['developer-tools'] }),
|
|
376
|
+
])
|
|
377
|
+
const artifacts = createArtifacts(base, lock as never, {
|
|
378
|
+
leading: { 'developer-tools': ['getsentry'] },
|
|
379
|
+
...(curationDiagnostics === undefined ? {} : { curationDiagnostics }),
|
|
380
|
+
})
|
|
381
|
+
return JSON.parse(artifacts['categories.v1.json']).diagnostics
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
test('publishes curated-name drift for a live catalog', () => {
|
|
385
|
+
expect(publishedDiagnostics()).toHaveLength(1)
|
|
386
|
+
expect(publishedDiagnostics(true)).toHaveLength(1)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('omits curated-name drift for an offline fixture build', () => {
|
|
390
|
+
expect(publishedDiagnostics(false)).toEqual([])
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe('consumer index verification', () => {
|
|
395
|
+
const plugins = [
|
|
396
|
+
plugin({ sourceId: 'claude-official', name: 'a', categories: ['productivity'] }),
|
|
397
|
+
plugin({ sourceId: 'claude-official', name: 'b', categories: ['productivity'] }),
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
test('accepts an index that agrees with its catalog', () => {
|
|
401
|
+
const base = catalog(plugins)
|
|
402
|
+
const index = buildConsumerIndex({
|
|
403
|
+
catalog: base,
|
|
404
|
+
leading: { productivity: ['b'] },
|
|
405
|
+
preference,
|
|
406
|
+
})
|
|
407
|
+
const result = verifyConsumerIndex(JSON.stringify(index), base)
|
|
408
|
+
expect(result.hasConsumerIndex).toBe(true)
|
|
409
|
+
expect(result.counts?.products).toBe(2)
|
|
410
|
+
expect(result.diagnostics).toEqual([])
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('accepts a snapshot published before the index existed', () => {
|
|
414
|
+
const base = catalog(plugins)
|
|
415
|
+
const legacy = JSON.stringify({
|
|
416
|
+
schemaVersion: 1,
|
|
417
|
+
catalogId: base.catalogId,
|
|
418
|
+
categories: [
|
|
419
|
+
{
|
|
420
|
+
category: 'productivity',
|
|
421
|
+
pluginIds: ['plugin:claude-official:a', 'plugin:claude-official:b'],
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
})
|
|
425
|
+
const result = verifyConsumerIndex(legacy, base)
|
|
426
|
+
expect(result.hasConsumerIndex).toBe(false)
|
|
427
|
+
expect(result.counts).toBeUndefined()
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('rejects a category list that names a plugin outside the catalog', () => {
|
|
431
|
+
const base = catalog(plugins)
|
|
432
|
+
const index = buildConsumerIndex({ catalog: base, preference })
|
|
433
|
+
const tampered = {
|
|
434
|
+
...index,
|
|
435
|
+
categories: index.categories.map((category) => ({
|
|
436
|
+
...category,
|
|
437
|
+
pluginIds: ['plugin:claude-official:a', 'plugin:claude-official:missing'],
|
|
438
|
+
})),
|
|
439
|
+
}
|
|
440
|
+
expect(() => verifyConsumerIndex(JSON.stringify(tampered), base)).toThrow(
|
|
441
|
+
'CONSUMER_INDEX_FOREIGN_PLUGIN'
|
|
442
|
+
)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('rejects a shelf that repeats a product across categories', () => {
|
|
446
|
+
const base = catalog([
|
|
447
|
+
plugin({
|
|
448
|
+
sourceId: 'claude-official',
|
|
449
|
+
name: 'a',
|
|
450
|
+
categories: ['productivity', 'communication'],
|
|
451
|
+
}),
|
|
452
|
+
])
|
|
453
|
+
const index = buildConsumerIndex({ catalog: base, preference })
|
|
454
|
+
const tampered = {
|
|
455
|
+
...index,
|
|
456
|
+
categories: index.categories.map((category) => ({
|
|
457
|
+
...category,
|
|
458
|
+
topProductKeys: ['a'],
|
|
459
|
+
})),
|
|
460
|
+
}
|
|
461
|
+
expect(() => verifyConsumerIndex(JSON.stringify(tampered), base)).toThrow(
|
|
462
|
+
'CONSUMER_INDEX_TOP_CROSS_CATEGORY'
|
|
463
|
+
)
|
|
464
|
+
})
|
|
465
|
+
})
|