@ggui-ai/registry-core 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +76 -0
- package/dist/impls/memory-bundle-storage.d.ts +12 -0
- package/dist/impls/memory-bundle-storage.d.ts.map +1 -0
- package/dist/impls/memory-bundle-storage.js +40 -0
- package/dist/impls/memory-registry-storage.d.ts +3 -0
- package/dist/impls/memory-registry-storage.d.ts.map +1 -0
- package/dist/impls/memory-registry-storage.js +154 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/interfaces/authn.d.ts +51 -0
- package/dist/interfaces/authn.d.ts.map +1 -0
- package/dist/interfaces/authn.js +1 -0
- package/dist/interfaces/bundle-storage.d.ts +79 -0
- package/dist/interfaces/bundle-storage.d.ts.map +1 -0
- package/dist/interfaces/bundle-storage.js +1 -0
- package/dist/interfaces/registry-storage.d.ts +204 -0
- package/dist/interfaces/registry-storage.d.ts.map +1 -0
- package/dist/interfaces/registry-storage.js +19 -0
- package/dist/ops/compile.d.ts +48 -0
- package/dist/ops/compile.d.ts.map +1 -0
- package/dist/ops/compile.js +163 -0
- package/dist/ops/conformance.d.ts +154 -0
- package/dist/ops/conformance.d.ts.map +1 -0
- package/dist/ops/conformance.js +487 -0
- package/dist/ops/list-versions.d.ts +58 -0
- package/dist/ops/list-versions.d.ts.map +1 -0
- package/dist/ops/list-versions.js +60 -0
- package/dist/ops/publish.d.ts +70 -0
- package/dist/ops/publish.d.ts.map +1 -0
- package/dist/ops/publish.js +417 -0
- package/dist/ops/read.d.ts +52 -0
- package/dist/ops/read.d.ts.map +1 -0
- package/dist/ops/read.js +60 -0
- package/dist/ops/register-author-key.d.ts +22 -0
- package/dist/ops/register-author-key.d.ts.map +1 -0
- package/dist/ops/register-author-key.js +171 -0
- package/dist/ops/search.d.ts +50 -0
- package/dist/ops/search.d.ts.map +1 -0
- package/dist/ops/search.js +106 -0
- package/dist/testing/bundle-storage-contract.d.ts +3 -0
- package/dist/testing/bundle-storage-contract.d.ts.map +1 -0
- package/dist/testing/bundle-storage-contract.js +176 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/registry-storage-contract.d.ts +3 -0
- package/dist/testing/registry-storage-contract.d.ts.map +1 -0
- package/dist/testing/registry-storage-contract.js +392 -0
- package/dist/types.d.ts +482 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +110 -0
- package/dist/utils/base64.d.ts +15 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +35 -0
- package/dist/utils/semver.d.ts +13 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +69 -0
- package/package.json +85 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test for {@link RegistryStorage} — every impl runs these.
|
|
3
|
+
*
|
|
4
|
+
* Consumed via the package's `./testing` subpath:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { registryStorageContract } from '@ggui-ai/registry-core/testing';
|
|
8
|
+
* import { inMemoryRegistryStorage } from '@ggui-ai/registry-core';
|
|
9
|
+
*
|
|
10
|
+
* describe('memory impl', () => {
|
|
11
|
+
* registryStorageContract(() => inMemoryRegistryStorage());
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The factory MUST return a fresh storage every call — the contract
|
|
16
|
+
* tests rely on isolation between cases.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
import { ARTIFACTS_METADATA_SK } from '../types.js';
|
|
20
|
+
function makeMetadata(overrides = {}) {
|
|
21
|
+
return {
|
|
22
|
+
artifactId: '@test/foo',
|
|
23
|
+
sk: ARTIFACTS_METADATA_SK,
|
|
24
|
+
kind: 'gadget',
|
|
25
|
+
latestVersion: '0.1.0',
|
|
26
|
+
description: 'a test gadget',
|
|
27
|
+
tags: ['test'],
|
|
28
|
+
visibility: 'public',
|
|
29
|
+
hook: 'useFoo',
|
|
30
|
+
authorName: 'Alice',
|
|
31
|
+
publishedAt: '2026-05-17T00:00:00.000Z',
|
|
32
|
+
publishedBy: 'user-1',
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// `overrides` is typed `Partial<GadgetManifest>` (not
|
|
37
|
+
// `Partial<ArtifactManifest>`) so the spread cannot widen `kind` to
|
|
38
|
+
// the full `'gadget' | 'blueprint'` union — the literal stays
|
|
39
|
+
// `'gadget'` and the object satisfies the gadget union member.
|
|
40
|
+
function makeStubManifest(overrides = {}) {
|
|
41
|
+
return {
|
|
42
|
+
kind: 'gadget',
|
|
43
|
+
scope: '@test',
|
|
44
|
+
name: 'foo',
|
|
45
|
+
version: '0.1.0',
|
|
46
|
+
description: 'a test gadget',
|
|
47
|
+
visibility: 'public',
|
|
48
|
+
bundle: './dist/index.js',
|
|
49
|
+
exports: [
|
|
50
|
+
{
|
|
51
|
+
hook: 'useFoo',
|
|
52
|
+
description: 'the test gadget export',
|
|
53
|
+
usage: 'A test gadget for registry-core contract tests',
|
|
54
|
+
example: { props: {} },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Blueprint-kind stub manifest. Separate from {@link makeStubManifest}
|
|
62
|
+
* (a gadget builder) because a blueprint and a gadget manifest are
|
|
63
|
+
* disjoint union members — one carries `source`, the other `bundle` +
|
|
64
|
+
* `exports`. `overrides` is `Partial<BlueprintManifest>` so the spread
|
|
65
|
+
* cannot widen `kind`.
|
|
66
|
+
*/
|
|
67
|
+
function makeStubBlueprintManifest(overrides = {}) {
|
|
68
|
+
return {
|
|
69
|
+
kind: 'blueprint',
|
|
70
|
+
scope: '@test',
|
|
71
|
+
name: 'bp',
|
|
72
|
+
version: '0.1.0',
|
|
73
|
+
description: 'a test blueprint',
|
|
74
|
+
visibility: 'public',
|
|
75
|
+
source: 'export default () => null;',
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function makeVersion(overrides = {}) {
|
|
80
|
+
return {
|
|
81
|
+
artifactId: '@test/foo',
|
|
82
|
+
version: '0.1.0',
|
|
83
|
+
manifest: makeStubManifest(),
|
|
84
|
+
kind: 'gadget',
|
|
85
|
+
visibility: 'public',
|
|
86
|
+
bundleUrl: 'https://example.invalid/bundle.js',
|
|
87
|
+
bundleSri: 'sha384-AAAA',
|
|
88
|
+
signatureUrl: 'https://example.invalid/bundle.js.sig',
|
|
89
|
+
authorPublicKey: 'BBBB',
|
|
90
|
+
publishedAt: '2026-05-17T00:00:00.000Z',
|
|
91
|
+
publishedBy: 'user-1',
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function makeAuthorKey(overrides = {}) {
|
|
96
|
+
return {
|
|
97
|
+
subject: 'user-1',
|
|
98
|
+
keyId: 'key-1',
|
|
99
|
+
publicKeyBase64: 'BBBB',
|
|
100
|
+
...overrides,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function makeCompiledBlob(overrides = {}) {
|
|
104
|
+
return {
|
|
105
|
+
compiledDigest: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
106
|
+
compiledBytes: 'ZXhwb3J0IGRlZmF1bHQgKCkgPT4gbnVsbA==',
|
|
107
|
+
compiledSize: 24,
|
|
108
|
+
refCount: 1,
|
|
109
|
+
createdAt: '2026-05-19T00:00:00.000Z',
|
|
110
|
+
...overrides,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function registryStorageContract(makeStorage) {
|
|
114
|
+
describe('RegistryStorage contract', () => {
|
|
115
|
+
describe('plugin metadata', () => {
|
|
116
|
+
it('returns null for missing artifactId', async () => {
|
|
117
|
+
const storage = makeStorage();
|
|
118
|
+
expect(await storage.getArtifactMetadata('@nope/missing')).toBe(null);
|
|
119
|
+
});
|
|
120
|
+
it('round-trips a metadata row', async () => {
|
|
121
|
+
const storage = makeStorage();
|
|
122
|
+
const row = makeMetadata();
|
|
123
|
+
await storage.putArtifactMetadata(row);
|
|
124
|
+
const fetched = await storage.getArtifactMetadata(row.artifactId);
|
|
125
|
+
expect(fetched).toEqual(row);
|
|
126
|
+
});
|
|
127
|
+
it('overwrites on second put (latest-version semantics)', async () => {
|
|
128
|
+
const storage = makeStorage();
|
|
129
|
+
await storage.putArtifactMetadata(makeMetadata({ latestVersion: '0.1.0' }));
|
|
130
|
+
await storage.putArtifactMetadata(makeMetadata({ latestVersion: '0.2.0' }));
|
|
131
|
+
const fetched = await storage.getArtifactMetadata('@test/foo');
|
|
132
|
+
expect(fetched?.latestVersion).toBe('0.2.0');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('scanArtifacts', () => {
|
|
136
|
+
it('returns empty page on empty store', async () => {
|
|
137
|
+
const storage = makeStorage();
|
|
138
|
+
const page = await storage.scanArtifacts({});
|
|
139
|
+
expect(page.rows).toEqual([]);
|
|
140
|
+
expect(page.nextCursor).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
it('returns all public rows when no filter', async () => {
|
|
143
|
+
const storage = makeStorage();
|
|
144
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@a/1' }));
|
|
145
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@b/2' }));
|
|
146
|
+
const page = await storage.scanArtifacts({});
|
|
147
|
+
const ids = page.rows.map((r) => r.artifactId).sort();
|
|
148
|
+
expect(ids).toEqual(['@a/1', '@b/2']);
|
|
149
|
+
});
|
|
150
|
+
it('filters by kind', async () => {
|
|
151
|
+
const storage = makeStorage();
|
|
152
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@a/g', kind: 'gadget' }));
|
|
153
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@b/bp', kind: 'blueprint', hook: undefined }));
|
|
154
|
+
const page = await storage.scanArtifacts({ kind: 'blueprint' });
|
|
155
|
+
expect(page.rows.map((r) => r.artifactId)).toEqual(['@b/bp']);
|
|
156
|
+
});
|
|
157
|
+
it('filters by tag', async () => {
|
|
158
|
+
const storage = makeStorage();
|
|
159
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@a/x', tags: ['map', 'leaflet'] }));
|
|
160
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@b/y', tags: ['form'] }));
|
|
161
|
+
const page = await storage.scanArtifacts({ tag: 'map' });
|
|
162
|
+
expect(page.rows.map((r) => r.artifactId)).toEqual(['@a/x']);
|
|
163
|
+
});
|
|
164
|
+
it('filters by q (substring of name / description / tags)', async () => {
|
|
165
|
+
const storage = makeStorage();
|
|
166
|
+
await storage.putArtifactMetadata(makeMetadata({
|
|
167
|
+
artifactId: '@a/leaflet',
|
|
168
|
+
description: 'Map widget',
|
|
169
|
+
tags: ['geo'],
|
|
170
|
+
}));
|
|
171
|
+
await storage.putArtifactMetadata(makeMetadata({ artifactId: '@b/form', description: 'Submission form' }));
|
|
172
|
+
const byName = await storage.scanArtifacts({ q: 'leaf' });
|
|
173
|
+
expect(byName.rows.map((r) => r.artifactId)).toEqual(['@a/leaflet']);
|
|
174
|
+
const byDesc = await storage.scanArtifacts({ q: 'widget' });
|
|
175
|
+
expect(byDesc.rows.map((r) => r.artifactId)).toEqual(['@a/leaflet']);
|
|
176
|
+
const byTag = await storage.scanArtifacts({ q: 'geo' });
|
|
177
|
+
expect(byTag.rows.map((r) => r.artifactId)).toEqual(['@a/leaflet']);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('plugin versions', () => {
|
|
181
|
+
it('returns null for missing version', async () => {
|
|
182
|
+
const storage = makeStorage();
|
|
183
|
+
expect(await storage.getArtifactVersion('@nope/x', '0.0.0')).toBe(null);
|
|
184
|
+
});
|
|
185
|
+
it('round-trips a version row', async () => {
|
|
186
|
+
const storage = makeStorage();
|
|
187
|
+
const row = makeVersion();
|
|
188
|
+
const result = await storage.putArtifactVersionIfAbsent(row);
|
|
189
|
+
expect(result).toEqual({ ok: true });
|
|
190
|
+
const fetched = await storage.getArtifactVersion(row.artifactId, row.version);
|
|
191
|
+
expect(fetched).toEqual(row);
|
|
192
|
+
});
|
|
193
|
+
it('rejects on (artifactId, version) collision', async () => {
|
|
194
|
+
const storage = makeStorage();
|
|
195
|
+
await storage.putArtifactVersionIfAbsent(makeVersion());
|
|
196
|
+
const second = await storage.putArtifactVersionIfAbsent(makeVersion());
|
|
197
|
+
expect(second).toEqual({ ok: false, reason: 'version_exists' });
|
|
198
|
+
});
|
|
199
|
+
it('allows the same artifactId at different versions', async () => {
|
|
200
|
+
const storage = makeStorage();
|
|
201
|
+
const r1 = await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.1.0' }));
|
|
202
|
+
const r2 = await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.2.0' }));
|
|
203
|
+
expect(r1).toEqual({ ok: true });
|
|
204
|
+
expect(r2).toEqual({ ok: true });
|
|
205
|
+
});
|
|
206
|
+
it('yanks a version (sets yanked: true)', async () => {
|
|
207
|
+
const storage = makeStorage();
|
|
208
|
+
await storage.putArtifactVersionIfAbsent(makeVersion());
|
|
209
|
+
await storage.yankArtifactVersion('@test/foo', '0.1.0');
|
|
210
|
+
const fetched = await storage.getArtifactVersion('@test/foo', '0.1.0');
|
|
211
|
+
expect(fetched?.yanked).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('listArtifactVersions', () => {
|
|
215
|
+
it('returns empty array on miss', async () => {
|
|
216
|
+
const storage = makeStorage();
|
|
217
|
+
expect(await storage.listArtifactVersions('@nope/missing')).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
it('returns every version for a single artifactId', async () => {
|
|
220
|
+
const storage = makeStorage();
|
|
221
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.1.0' }));
|
|
222
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.2.0' }));
|
|
223
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.1.5' }));
|
|
224
|
+
const rows = await storage.listArtifactVersions('@test/foo');
|
|
225
|
+
expect(rows.map((r) => r.version).sort()).toEqual(['0.1.0', '0.1.5', '0.2.0']);
|
|
226
|
+
});
|
|
227
|
+
it('scopes to the requested artifactId (does NOT leak siblings)', async () => {
|
|
228
|
+
const storage = makeStorage();
|
|
229
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ artifactId: '@test/foo', version: '0.1.0' }));
|
|
230
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ artifactId: '@test/bar', version: '0.1.0' }));
|
|
231
|
+
const fooRows = await storage.listArtifactVersions('@test/foo');
|
|
232
|
+
expect(fooRows.map((r) => r.artifactId)).toEqual(['@test/foo']);
|
|
233
|
+
const barRows = await storage.listArtifactVersions('@test/bar');
|
|
234
|
+
expect(barRows.map((r) => r.artifactId)).toEqual(['@test/bar']);
|
|
235
|
+
});
|
|
236
|
+
it('surfaces yanked: true on yanked rows', async () => {
|
|
237
|
+
const storage = makeStorage();
|
|
238
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.1.0' }));
|
|
239
|
+
await storage.putArtifactVersionIfAbsent(makeVersion({ version: '0.2.0' }));
|
|
240
|
+
await storage.yankArtifactVersion('@test/foo', '0.1.0');
|
|
241
|
+
const rows = await storage.listArtifactVersions('@test/foo');
|
|
242
|
+
const sorted = [...rows].sort((a, b) => a.version.localeCompare(b.version));
|
|
243
|
+
expect(sorted.map((r) => ({ v: r.version, y: r.yanked === true }))).toEqual([
|
|
244
|
+
{ v: '0.1.0', y: true },
|
|
245
|
+
{ v: '0.2.0', y: false },
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('compiled blobs', () => {
|
|
250
|
+
it('returns null on miss', async () => {
|
|
251
|
+
const storage = makeStorage();
|
|
252
|
+
expect(await storage.getCompiledBlob('deadbeef')).toBe(null);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
describe('commitVersionAndBlob (atomic two-row write)', () => {
|
|
256
|
+
// New-blob path: both rows INSERT under one transaction.
|
|
257
|
+
it('new-blob path — persists both rows, refCount=1, returns mode=new-blob', async () => {
|
|
258
|
+
const storage = makeStorage();
|
|
259
|
+
const versionRow = makeVersion({
|
|
260
|
+
artifactId: '@test/bp',
|
|
261
|
+
version: '0.1.0',
|
|
262
|
+
kind: 'blueprint',
|
|
263
|
+
manifest: makeStubBlueprintManifest({ name: 'bp', source: 'export default () => null;' }),
|
|
264
|
+
compiledDigest: 'a'.repeat(64),
|
|
265
|
+
bundleUrl: undefined,
|
|
266
|
+
bundleSri: undefined,
|
|
267
|
+
signatureUrl: undefined,
|
|
268
|
+
});
|
|
269
|
+
const blobRow = makeCompiledBlob({
|
|
270
|
+
compiledDigest: 'a'.repeat(64),
|
|
271
|
+
refCount: 1,
|
|
272
|
+
});
|
|
273
|
+
const result = await storage.commitVersionAndBlob(versionRow, blobRow);
|
|
274
|
+
expect(result).toEqual({ ok: true, mode: 'new-blob' });
|
|
275
|
+
const fetchedVersion = await storage.getArtifactVersion('@test/bp', '0.1.0');
|
|
276
|
+
expect(fetchedVersion?.compiledDigest).toBe('a'.repeat(64));
|
|
277
|
+
const fetchedBlob = await storage.getCompiledBlob('a'.repeat(64));
|
|
278
|
+
expect(fetchedBlob?.refCount).toBe(1);
|
|
279
|
+
// Full-row faithfulness — every field round-trips through the
|
|
280
|
+
// new-blob path (compiledBytes, compiledSize, createdAt).
|
|
281
|
+
expect(fetchedBlob).toEqual(blobRow);
|
|
282
|
+
});
|
|
283
|
+
// Dedup path: version INSERT + refCount bump on the existing blob.
|
|
284
|
+
it('dedup path — version row inserted, refCount bumped, returns mode=dedup', async () => {
|
|
285
|
+
const storage = makeStorage();
|
|
286
|
+
// Pre-seed the state after a single normal publish — version row
|
|
287
|
+
// + blob row (refCount=1) committed atomically.
|
|
288
|
+
const sharedDigest = 'b'.repeat(64);
|
|
289
|
+
const seedBlob = makeCompiledBlob({ compiledDigest: sharedDigest, refCount: 1 });
|
|
290
|
+
const firstVersion = makeVersion({
|
|
291
|
+
artifactId: '@test/bp1',
|
|
292
|
+
version: '0.1.0',
|
|
293
|
+
kind: 'blueprint',
|
|
294
|
+
manifest: makeStubBlueprintManifest({ name: 'bp1', source: 'export default () => null;' }),
|
|
295
|
+
compiledDigest: sharedDigest,
|
|
296
|
+
bundleUrl: undefined,
|
|
297
|
+
bundleSri: undefined,
|
|
298
|
+
signatureUrl: undefined,
|
|
299
|
+
});
|
|
300
|
+
await storage.commitVersionAndBlob(firstVersion, seedBlob);
|
|
301
|
+
// Now a SECOND publish with a new (artifactId, version) but the
|
|
302
|
+
// same compiled output → dedup path.
|
|
303
|
+
const secondVersion = makeVersion({
|
|
304
|
+
artifactId: '@test/bp2',
|
|
305
|
+
version: '0.1.0',
|
|
306
|
+
kind: 'blueprint',
|
|
307
|
+
manifest: makeStubBlueprintManifest({ name: 'bp2', source: 'export default () => null;' }),
|
|
308
|
+
compiledDigest: sharedDigest,
|
|
309
|
+
bundleUrl: undefined,
|
|
310
|
+
bundleSri: undefined,
|
|
311
|
+
signatureUrl: undefined,
|
|
312
|
+
});
|
|
313
|
+
const newBlob = makeCompiledBlob({ compiledDigest: sharedDigest, refCount: 1 });
|
|
314
|
+
const result = await storage.commitVersionAndBlob(secondVersion, newBlob);
|
|
315
|
+
expect(result).toEqual({ ok: true, mode: 'dedup' });
|
|
316
|
+
const fetchedSecond = await storage.getArtifactVersion('@test/bp2', '0.1.0');
|
|
317
|
+
expect(fetchedSecond?.compiledDigest).toBe(sharedDigest);
|
|
318
|
+
const fetchedBlob = await storage.getCompiledBlob(sharedDigest);
|
|
319
|
+
// 1 (from seed) + 1 (from commitVersionAndBlob dedup) = 2.
|
|
320
|
+
expect(fetchedBlob?.refCount).toBe(2);
|
|
321
|
+
});
|
|
322
|
+
// Version-exists failure: NEITHER row mutates (atomicity proof).
|
|
323
|
+
it('version_exists — neither row mutates when (artifactId, version) collides', async () => {
|
|
324
|
+
const storage = makeStorage();
|
|
325
|
+
// Pre-publish the version row + matching blob row.
|
|
326
|
+
const existingDigest = 'c'.repeat(64);
|
|
327
|
+
const existingVersion = makeVersion({
|
|
328
|
+
artifactId: '@test/bp',
|
|
329
|
+
version: '0.1.0',
|
|
330
|
+
kind: 'blueprint',
|
|
331
|
+
manifest: makeStubBlueprintManifest({ name: 'bp', source: 'export default () => null;' }),
|
|
332
|
+
compiledDigest: existingDigest,
|
|
333
|
+
bundleUrl: undefined,
|
|
334
|
+
bundleSri: undefined,
|
|
335
|
+
signatureUrl: undefined,
|
|
336
|
+
});
|
|
337
|
+
const existingBlob = makeCompiledBlob({ compiledDigest: existingDigest, refCount: 1 });
|
|
338
|
+
await storage.commitVersionAndBlob(existingVersion, existingBlob);
|
|
339
|
+
// Now try to commit a DIFFERENT blob under the same
|
|
340
|
+
// (artifactId, version). Should refuse + leave both stores
|
|
341
|
+
// untouched.
|
|
342
|
+
const conflictingDigest = 'd'.repeat(64);
|
|
343
|
+
const conflictingVersion = makeVersion({
|
|
344
|
+
artifactId: '@test/bp',
|
|
345
|
+
version: '0.1.0',
|
|
346
|
+
kind: 'blueprint',
|
|
347
|
+
manifest: makeStubBlueprintManifest({ name: 'bp', source: 'export default () => 1;' }),
|
|
348
|
+
compiledDigest: conflictingDigest,
|
|
349
|
+
bundleUrl: undefined,
|
|
350
|
+
bundleSri: undefined,
|
|
351
|
+
signatureUrl: undefined,
|
|
352
|
+
});
|
|
353
|
+
const conflictingBlob = makeCompiledBlob({
|
|
354
|
+
compiledDigest: conflictingDigest,
|
|
355
|
+
refCount: 1,
|
|
356
|
+
});
|
|
357
|
+
const result = await storage.commitVersionAndBlob(conflictingVersion, conflictingBlob);
|
|
358
|
+
expect(result).toEqual({ ok: false, reason: 'version_exists' });
|
|
359
|
+
// Confirm: the existing version row is untouched (still points
|
|
360
|
+
// at the original digest).
|
|
361
|
+
const fetched = await storage.getArtifactVersion('@test/bp', '0.1.0');
|
|
362
|
+
expect(fetched?.compiledDigest).toBe(existingDigest);
|
|
363
|
+
// Confirm: the conflicting blob was NOT inserted.
|
|
364
|
+
const conflictingBlobFetched = await storage.getCompiledBlob(conflictingDigest);
|
|
365
|
+
expect(conflictingBlobFetched).toBeNull();
|
|
366
|
+
// Confirm: the existing blob's refCount was NOT bumped.
|
|
367
|
+
const existingBlobFetched = await storage.getCompiledBlob(existingDigest);
|
|
368
|
+
expect(existingBlobFetched?.refCount).toBe(1);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
describe('author keys', () => {
|
|
372
|
+
it('returns null on miss', async () => {
|
|
373
|
+
const storage = makeStorage();
|
|
374
|
+
expect(await storage.getAuthorKey('nobody', 'nope')).toBe(null);
|
|
375
|
+
});
|
|
376
|
+
it('round-trips an author key', async () => {
|
|
377
|
+
const storage = makeStorage();
|
|
378
|
+
const row = makeAuthorKey();
|
|
379
|
+
await storage.putAuthorKey(row);
|
|
380
|
+
expect(await storage.getAuthorKey(row.subject, row.keyId)).toEqual(row);
|
|
381
|
+
});
|
|
382
|
+
it('lists author keys scoped by subject', async () => {
|
|
383
|
+
const storage = makeStorage();
|
|
384
|
+
await storage.putAuthorKey(makeAuthorKey({ subject: 'alice', keyId: 'k1' }));
|
|
385
|
+
await storage.putAuthorKey(makeAuthorKey({ subject: 'alice', keyId: 'k2' }));
|
|
386
|
+
await storage.putAuthorKey(makeAuthorKey({ subject: 'bob', keyId: 'k1' }));
|
|
387
|
+
const aliceKeys = await storage.listAuthorKeys('alice');
|
|
388
|
+
expect(aliceKeys.map((k) => k.keyId).sort()).toEqual(['k1', 'k2']);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|