@agentdocstore/provider-tests 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/README.md +54 -0
- package/dist/conformance.d.ts +8 -0
- package/dist/conformance.js +686 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# `@agentdocstore/provider-tests`
|
|
2
|
+
|
|
3
|
+
The reusable conformance suite for
|
|
4
|
+
[AgentDocStore](https://github.com/koushikginjupally/agentdocstore) storage providers.
|
|
5
|
+
|
|
6
|
+
It registers 60 Vitest cases covering CRUD, compare-and-set conflicts, immutable
|
|
7
|
+
versions, stable pagination, opaque cursors, private-search isolation, Unicode
|
|
8
|
+
and size boundaries, expiry, comments, capabilities and delete cascades.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install --save-dev @agentdocstore/provider-tests vitest
|
|
14
|
+
npm install @agentdocstore/core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Vitest is a peer dependency so the tests register in **your** runner.
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { runProviderConformance } from '@agentdocstore/provider-tests';
|
|
23
|
+
import { createProvider } from '../src/index.js';
|
|
24
|
+
|
|
25
|
+
runProviderConformance('my provider', async () => {
|
|
26
|
+
const provider = await createProvider({ testDatabase: true });
|
|
27
|
+
return {
|
|
28
|
+
provider,
|
|
29
|
+
cleanup: async () => provider.close(),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx vitest run
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use an isolated database/schema/bucket per test factory. The suite creates and
|
|
39
|
+
deletes records and must never point at production data.
|
|
40
|
+
|
|
41
|
+
Passing this suite is necessary but not sufficient: run
|
|
42
|
+
`agentdocstore doctor --provider <your-module>` against a realistic local setup,
|
|
43
|
+
and test provider-specific failure, reconnect, migration and concurrency paths.
|
|
44
|
+
See the full [provider SPI guide](../../docs/PROVIDERS.md).
|
|
45
|
+
|
|
46
|
+
## Compatibility
|
|
47
|
+
|
|
48
|
+
Keep this package on the same minor version as `@agentdocstore/core`. AgentDocStore
|
|
49
|
+
is pre-1.0, so minor releases may evolve the contract; breaking changes are
|
|
50
|
+
called out in the root [CHANGELOG](../../CHANGELOG.md).
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
Apache-2.0.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Provider } from '@agentdocstore/core';
|
|
2
|
+
/**
|
|
3
|
+
* Registers a vitest `describe(name, ...)` containing the entire provider
|
|
4
|
+
* conformance suite. A **fresh** provider is created per test via `factory()`
|
|
5
|
+
* and closed afterwards.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runProviderConformance(name: string, factory: () => Provider | Promise<Provider>): void;
|
|
8
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable provider conformance suite.
|
|
3
|
+
*
|
|
4
|
+
* Call {@link runProviderConformance} from any test file; it registers a full
|
|
5
|
+
* vitest `describe` that exercises every contract clause in the SPI.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { VersionConflictError, NotFoundError, ContentTooLargeError, LIMITS, ID_ALPHABET, ID_LENGTH, isValidId, } from '@agentdocstore/core';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Default creation input. */
|
|
13
|
+
function defaultInput(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
title: overrides.title ?? 'Test Document',
|
|
16
|
+
language: (overrides.language ?? 'plaintext'),
|
|
17
|
+
visibility: (overrides.visibility ?? 'PUBLIC'),
|
|
18
|
+
content: overrides.content ?? 'hello world',
|
|
19
|
+
createdBy: overrides.createdBy ?? 'alice',
|
|
20
|
+
...(overrides.expiresAt !== undefined ? { expiresAt: overrides.expiresAt } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** ISO-8601 regex (loose β accepts any valid ISO date-time with or without ms). */
|
|
24
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
25
|
+
function isIso(s) {
|
|
26
|
+
return ISO_RE.test(s);
|
|
27
|
+
}
|
|
28
|
+
/** Byte length of a string in UTF-8. */
|
|
29
|
+
function byteLen(s) {
|
|
30
|
+
return new TextEncoder().encode(s).length;
|
|
31
|
+
}
|
|
32
|
+
/** Collect all items across all pages via listByOwner. */
|
|
33
|
+
async function collectAll(repo, owner, pageSize) {
|
|
34
|
+
const all = [];
|
|
35
|
+
let query = { limit: pageSize };
|
|
36
|
+
for (;;) {
|
|
37
|
+
const page = await repo.listByOwner(owner, query);
|
|
38
|
+
all.push(...page.items);
|
|
39
|
+
if (!page.nextCursor)
|
|
40
|
+
break;
|
|
41
|
+
query = { limit: pageSize, cursor: page.nextCursor };
|
|
42
|
+
}
|
|
43
|
+
return all;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Public API
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Registers a vitest `describe(name, ...)` containing the entire provider
|
|
50
|
+
* conformance suite. A **fresh** provider is created per test via `factory()`
|
|
51
|
+
* and closed afterwards.
|
|
52
|
+
*/
|
|
53
|
+
export function runProviderConformance(name, factory) {
|
|
54
|
+
describe(name, () => {
|
|
55
|
+
let provider;
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
provider = await factory();
|
|
58
|
+
});
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
await provider.close();
|
|
61
|
+
});
|
|
62
|
+
// -----------------------------------------------------------------------
|
|
63
|
+
// 1. Document CRUD
|
|
64
|
+
// -----------------------------------------------------------------------
|
|
65
|
+
describe('Document CRUD', () => {
|
|
66
|
+
it('create returns a fully-populated Document with a valid id', async () => {
|
|
67
|
+
const doc = await provider.repository.create(defaultInput());
|
|
68
|
+
expect(isValidId(doc.id)).toBe(true);
|
|
69
|
+
expect(doc.id).toHaveLength(ID_LENGTH);
|
|
70
|
+
// every char from the id alphabet
|
|
71
|
+
for (const ch of doc.id) {
|
|
72
|
+
expect(ID_ALPHABET).toContain(ch);
|
|
73
|
+
}
|
|
74
|
+
expect(doc.title).toBe('Test Document');
|
|
75
|
+
expect(doc.language).toBe('plaintext');
|
|
76
|
+
expect(doc.visibility).toBe('PUBLIC');
|
|
77
|
+
expect(doc.createdBy).toBe('alice');
|
|
78
|
+
expect(doc.latestVersion).toBe(1);
|
|
79
|
+
expect(isIso(doc.createdAt)).toBe(true);
|
|
80
|
+
expect(isIso(doc.updatedAt)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('create sets sizeBytes equal to content byte length', async () => {
|
|
83
|
+
const content = 'hello π'; // multi-byte
|
|
84
|
+
const doc = await provider.repository.create(defaultInput({ content }));
|
|
85
|
+
// sizeBytes may or may not be on the interface; if present, verify it
|
|
86
|
+
if ('sizeBytes' in doc) {
|
|
87
|
+
expect(doc['sizeBytes']).toBe(byteLen(content));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it('get round-trips a created doc', async () => {
|
|
91
|
+
const created = await provider.repository.create(defaultInput());
|
|
92
|
+
const got = await provider.repository.get(created.id);
|
|
93
|
+
expect(got).not.toBeNull();
|
|
94
|
+
expect(got.id).toBe(created.id);
|
|
95
|
+
expect(got.title).toBe(created.title);
|
|
96
|
+
expect(got.language).toBe(created.language);
|
|
97
|
+
expect(got.visibility).toBe(created.visibility);
|
|
98
|
+
expect(got.createdBy).toBe(created.createdBy);
|
|
99
|
+
expect(got.latestVersion).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
it('delete removes the doc so get returns null', async () => {
|
|
102
|
+
const doc = await provider.repository.create(defaultInput());
|
|
103
|
+
await provider.repository.delete(doc.id);
|
|
104
|
+
const got = await provider.repository.get(doc.id);
|
|
105
|
+
expect(got).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
it('getVersion retrieves the initial version after create', async () => {
|
|
108
|
+
const doc = await provider.repository.create(defaultInput({ content: 'v1 content' }));
|
|
109
|
+
const v = await provider.repository.getVersion(doc.id, 1);
|
|
110
|
+
expect(v).not.toBeNull();
|
|
111
|
+
expect(v.documentId).toBe(doc.id);
|
|
112
|
+
expect(v.version).toBe(1);
|
|
113
|
+
expect(v.content).toBe('v1 content');
|
|
114
|
+
expect(v.createdBy).toBe('alice');
|
|
115
|
+
expect(isIso(v.createdAt)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it('listVersions returns all versions', async () => {
|
|
118
|
+
const doc = await provider.repository.create(defaultInput());
|
|
119
|
+
const versions = await provider.repository.listVersions(doc.id);
|
|
120
|
+
expect(versions).toHaveLength(1);
|
|
121
|
+
expect(versions[0].version).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
// 2. Versioning + CAS
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
describe('Versioning + CAS', () => {
|
|
128
|
+
it('appendVersion increments latestVersion', async () => {
|
|
129
|
+
const doc = await provider.repository.create(defaultInput());
|
|
130
|
+
const updated = await provider.repository.appendVersion(doc.id, {
|
|
131
|
+
content: 'v2',
|
|
132
|
+
editedBy: 'alice',
|
|
133
|
+
expect: { latestVersion: 1 },
|
|
134
|
+
});
|
|
135
|
+
expect(updated.latestVersion).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
it('each appended version is retrievable by number', async () => {
|
|
138
|
+
const doc = await provider.repository.create(defaultInput({ content: 'v1' }));
|
|
139
|
+
await provider.repository.appendVersion(doc.id, {
|
|
140
|
+
content: 'v2',
|
|
141
|
+
editedBy: 'alice',
|
|
142
|
+
expect: { latestVersion: 1 },
|
|
143
|
+
});
|
|
144
|
+
await provider.repository.appendVersion(doc.id, {
|
|
145
|
+
content: 'v3',
|
|
146
|
+
editedBy: 'bob',
|
|
147
|
+
expect: { latestVersion: 2 },
|
|
148
|
+
});
|
|
149
|
+
const ver1 = await provider.repository.getVersion(doc.id, 1);
|
|
150
|
+
const ver2 = await provider.repository.getVersion(doc.id, 2);
|
|
151
|
+
const ver3 = await provider.repository.getVersion(doc.id, 3);
|
|
152
|
+
expect(ver1.content).toBe('v1');
|
|
153
|
+
expect(ver2.content).toBe('v2');
|
|
154
|
+
expect(ver3.content).toBe('v3');
|
|
155
|
+
expect(ver3.createdBy).toBe('bob');
|
|
156
|
+
});
|
|
157
|
+
it('stores an optional edit message with the version it describes', async () => {
|
|
158
|
+
const doc = await provider.repository.create(defaultInput({ content: 'v1' }));
|
|
159
|
+
await provider.repository.appendVersion(doc.id, {
|
|
160
|
+
content: 'v2',
|
|
161
|
+
editedBy: 'alice',
|
|
162
|
+
message: 'Fix the intro',
|
|
163
|
+
expect: { latestVersion: 1 },
|
|
164
|
+
});
|
|
165
|
+
await provider.repository.appendVersion(doc.id, {
|
|
166
|
+
content: 'v3',
|
|
167
|
+
editedBy: 'alice',
|
|
168
|
+
expect: { latestVersion: 2 },
|
|
169
|
+
});
|
|
170
|
+
expect((await provider.repository.getVersion(doc.id, 2)).message).toBe('Fix the intro');
|
|
171
|
+
expect((await provider.repository.getVersion(doc.id, 3)).message).toBeUndefined();
|
|
172
|
+
const all = await provider.repository.listVersions(doc.id);
|
|
173
|
+
expect(all.map((v) => v.message)).toEqual([undefined, 'Fix the intro', undefined]);
|
|
174
|
+
});
|
|
175
|
+
it('stale expect.latestVersion throws VersionConflictError', async () => {
|
|
176
|
+
const doc = await provider.repository.create(defaultInput());
|
|
177
|
+
// advance to version 2
|
|
178
|
+
await provider.repository.appendVersion(doc.id, {
|
|
179
|
+
content: 'v2',
|
|
180
|
+
editedBy: 'alice',
|
|
181
|
+
expect: { latestVersion: 1 },
|
|
182
|
+
});
|
|
183
|
+
// try with stale version 1
|
|
184
|
+
await expect(provider.repository.appendVersion(doc.id, {
|
|
185
|
+
content: 'v3-stale',
|
|
186
|
+
editedBy: 'alice',
|
|
187
|
+
expect: { latestVersion: 1 },
|
|
188
|
+
})).rejects.toThrow(VersionConflictError);
|
|
189
|
+
});
|
|
190
|
+
it('correct expect.latestVersion succeeds after prior conflict', async () => {
|
|
191
|
+
const doc = await provider.repository.create(defaultInput());
|
|
192
|
+
await provider.repository.appendVersion(doc.id, {
|
|
193
|
+
content: 'v2',
|
|
194
|
+
editedBy: 'alice',
|
|
195
|
+
expect: { latestVersion: 1 },
|
|
196
|
+
});
|
|
197
|
+
// stale attempt
|
|
198
|
+
await expect(provider.repository.appendVersion(doc.id, {
|
|
199
|
+
content: 'v3-stale',
|
|
200
|
+
editedBy: 'alice',
|
|
201
|
+
expect: { latestVersion: 1 },
|
|
202
|
+
})).rejects.toThrow(VersionConflictError);
|
|
203
|
+
// correct attempt
|
|
204
|
+
const updated = await provider.repository.appendVersion(doc.id, {
|
|
205
|
+
content: 'v3-good',
|
|
206
|
+
editedBy: 'alice',
|
|
207
|
+
expect: { latestVersion: 2 },
|
|
208
|
+
});
|
|
209
|
+
expect(updated.latestVersion).toBe(3);
|
|
210
|
+
});
|
|
211
|
+
it('immutability: version 1 content unchanged after multiple appends', async () => {
|
|
212
|
+
const originalContent = 'original content preserved';
|
|
213
|
+
const doc = await provider.repository.create(defaultInput({ content: originalContent }));
|
|
214
|
+
await provider.repository.appendVersion(doc.id, {
|
|
215
|
+
content: 'version 2 overwrite',
|
|
216
|
+
editedBy: 'alice',
|
|
217
|
+
expect: { latestVersion: 1 },
|
|
218
|
+
});
|
|
219
|
+
await provider.repository.appendVersion(doc.id, {
|
|
220
|
+
content: 'version 3 overwrite',
|
|
221
|
+
editedBy: 'alice',
|
|
222
|
+
expect: { latestVersion: 2 },
|
|
223
|
+
});
|
|
224
|
+
const v1 = await provider.repository.getVersion(doc.id, 1);
|
|
225
|
+
expect(v1.content).toBe(originalContent);
|
|
226
|
+
});
|
|
227
|
+
it('listVersions returns all versions after appends', async () => {
|
|
228
|
+
const doc = await provider.repository.create(defaultInput());
|
|
229
|
+
await provider.repository.appendVersion(doc.id, {
|
|
230
|
+
content: 'v2',
|
|
231
|
+
editedBy: 'alice',
|
|
232
|
+
expect: { latestVersion: 1 },
|
|
233
|
+
});
|
|
234
|
+
const versions = await provider.repository.listVersions(doc.id);
|
|
235
|
+
expect(versions).toHaveLength(2);
|
|
236
|
+
const nums = versions.map((v) => v.version).sort();
|
|
237
|
+
expect(nums).toEqual([1, 2]);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// 3. Unknown and malformed ids
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
describe('Unknown and malformed ids', () => {
|
|
244
|
+
it('get returns null for a well-formed but absent id', async () => {
|
|
245
|
+
const result = await provider.repository.get('AAAAAAAAAA');
|
|
246
|
+
expect(result).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
it('get returns null for malformed ids', async () => {
|
|
249
|
+
expect(await provider.repository.get('../etc/passwd')).toBeNull();
|
|
250
|
+
expect(await provider.repository.get('')).toBeNull();
|
|
251
|
+
expect(await provider.repository.get('short')).toBeNull();
|
|
252
|
+
expect(await provider.repository.get('has spaces!')).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
it('getVersion returns null for absent doc', async () => {
|
|
255
|
+
expect(await provider.repository.getVersion('AAAAAAAAAA', 1)).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
it('getVersion returns null for malformed id', async () => {
|
|
258
|
+
expect(await provider.repository.getVersion('../etc/passwd', 1)).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
it('listVersions returns empty for absent doc', async () => {
|
|
261
|
+
const versions = await provider.repository.listVersions('AAAAAAAAAA');
|
|
262
|
+
expect(versions).toHaveLength(0);
|
|
263
|
+
});
|
|
264
|
+
it('listVersions returns empty for malformed id', async () => {
|
|
265
|
+
const versions = await provider.repository.listVersions('../etc/passwd');
|
|
266
|
+
expect(versions).toHaveLength(0);
|
|
267
|
+
});
|
|
268
|
+
it('appendVersion throws NotFoundError for absent doc', async () => {
|
|
269
|
+
await expect(provider.repository.appendVersion('AAAAAAAAAA', {
|
|
270
|
+
content: 'x',
|
|
271
|
+
editedBy: 'alice',
|
|
272
|
+
expect: { latestVersion: 1 },
|
|
273
|
+
})).rejects.toThrow(NotFoundError);
|
|
274
|
+
});
|
|
275
|
+
it('updateMeta throws NotFoundError for absent doc', async () => {
|
|
276
|
+
await expect(provider.repository.updateMeta('AAAAAAAAAA', { title: 'x' })).rejects.toThrow(NotFoundError);
|
|
277
|
+
});
|
|
278
|
+
it('setVisibility throws NotFoundError for absent doc', async () => {
|
|
279
|
+
await expect(provider.repository.setVisibility('AAAAAAAAAA', 'PRIVATE')).rejects.toThrow(NotFoundError);
|
|
280
|
+
});
|
|
281
|
+
it('delete throws NotFoundError for absent doc', async () => {
|
|
282
|
+
await expect(provider.repository.delete('AAAAAAAAAA')).rejects.toThrow(NotFoundError);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
// -----------------------------------------------------------------------
|
|
286
|
+
// 4. Metadata
|
|
287
|
+
// -----------------------------------------------------------------------
|
|
288
|
+
describe('Metadata', () => {
|
|
289
|
+
it('updateMeta changes title', async () => {
|
|
290
|
+
const doc = await provider.repository.create(defaultInput());
|
|
291
|
+
const updated = await provider.repository.updateMeta(doc.id, { title: 'New Title' });
|
|
292
|
+
expect(updated.title).toBe('New Title');
|
|
293
|
+
const got = await provider.repository.get(doc.id);
|
|
294
|
+
expect(got.title).toBe('New Title');
|
|
295
|
+
});
|
|
296
|
+
it('updateMeta changes language', async () => {
|
|
297
|
+
const doc = await provider.repository.create(defaultInput());
|
|
298
|
+
const updated = await provider.repository.updateMeta(doc.id, { language: 'typescript' });
|
|
299
|
+
expect(updated.language).toBe('typescript');
|
|
300
|
+
});
|
|
301
|
+
it('expiresAt: null clears an existing expiry', async () => {
|
|
302
|
+
const future = new Date(Date.now() + 86400_000).toISOString();
|
|
303
|
+
const doc = await provider.repository.create(defaultInput({ expiresAt: future }));
|
|
304
|
+
expect(doc.expiresAt).toBeDefined();
|
|
305
|
+
const updated = await provider.repository.updateMeta(doc.id, { expiresAt: null });
|
|
306
|
+
expect(updated.expiresAt).toBeUndefined();
|
|
307
|
+
const got = await provider.repository.get(doc.id);
|
|
308
|
+
expect(got.expiresAt).toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
it('expiresAt: undefined leaves existing expiry untouched', async () => {
|
|
311
|
+
const future = new Date(Date.now() + 86400_000).toISOString();
|
|
312
|
+
const doc = await provider.repository.create(defaultInput({ expiresAt: future }));
|
|
313
|
+
// update title only, do not pass expiresAt
|
|
314
|
+
const updated = await provider.repository.updateMeta(doc.id, { title: 'Changed' });
|
|
315
|
+
expect(updated.expiresAt).toBe(future);
|
|
316
|
+
});
|
|
317
|
+
it('setVisibility flips PUBLIC to PRIVATE and back', async () => {
|
|
318
|
+
const doc = await provider.repository.create(defaultInput({ visibility: 'PUBLIC' }));
|
|
319
|
+
const priv = await provider.repository.setVisibility(doc.id, 'PRIVATE');
|
|
320
|
+
expect(priv.visibility).toBe('PRIVATE');
|
|
321
|
+
const pub = await provider.repository.setVisibility(doc.id, 'PUBLIC');
|
|
322
|
+
expect(pub.visibility).toBe('PUBLIC');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
// -----------------------------------------------------------------------
|
|
326
|
+
// 5. Pagination
|
|
327
|
+
// -----------------------------------------------------------------------
|
|
328
|
+
describe('Pagination', () => {
|
|
329
|
+
it('pages return every item exactly once (no dups, no gaps)', async () => {
|
|
330
|
+
const owner = 'paginator';
|
|
331
|
+
const total = 7;
|
|
332
|
+
const ids = [];
|
|
333
|
+
for (let i = 0; i < total; i++) {
|
|
334
|
+
const doc = await provider.repository.create(defaultInput({ title: `Document ${i}`, createdBy: owner }));
|
|
335
|
+
ids.push(doc.id);
|
|
336
|
+
}
|
|
337
|
+
const collected = await collectAll(provider.repository, owner, 3);
|
|
338
|
+
const collectedIds = collected.map((b) => b.id);
|
|
339
|
+
expect(collectedIds).toHaveLength(total);
|
|
340
|
+
// every created id appears
|
|
341
|
+
for (const id of ids) {
|
|
342
|
+
expect(collectedIds).toContain(id);
|
|
343
|
+
}
|
|
344
|
+
// no duplicates
|
|
345
|
+
expect(new Set(collectedIds).size).toBe(total);
|
|
346
|
+
});
|
|
347
|
+
it('ordering is newest-first and stable across pages', async () => {
|
|
348
|
+
const owner = 'orderer';
|
|
349
|
+
const created = [];
|
|
350
|
+
for (let i = 0; i < 5; i++) {
|
|
351
|
+
const doc = await provider.repository.create(defaultInput({ title: `Document ${i}`, createdBy: owner }));
|
|
352
|
+
created.push(doc.id);
|
|
353
|
+
// `createdAt` is an ISO-8601 string with millisecond resolution, so a
|
|
354
|
+
// tight creation loop produces ties and "newest-first" becomes
|
|
355
|
+
// ambiguous β the contract then only guarantees a deterministic
|
|
356
|
+
// tie-break, not creation order. Separate the timestamps so this case
|
|
357
|
+
// tests the ordering rule rather than the host's execution speed.
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
359
|
+
}
|
|
360
|
+
const collected = await collectAll(provider.repository, owner, 2);
|
|
361
|
+
const collectedIds = collected.map((b) => b.id);
|
|
362
|
+
// newest-first means reverse of creation order
|
|
363
|
+
expect(collectedIds).toEqual([...created].reverse());
|
|
364
|
+
});
|
|
365
|
+
it('ordering is deterministic when createdAt values tie', async () => {
|
|
366
|
+
const owner = 'tie-breaker';
|
|
367
|
+
// Created as fast as possible, so several documents are expected to share a
|
|
368
|
+
// millisecond. The contract does not say WHICH order ties take, only
|
|
369
|
+
// that it is a stable total order β so assert repeatability, which is
|
|
370
|
+
// the property pagination actually depends on.
|
|
371
|
+
for (let i = 0; i < 5; i++) {
|
|
372
|
+
await provider.repository.create(defaultInput({ title: `Tie ${i}`, createdBy: owner }));
|
|
373
|
+
}
|
|
374
|
+
const first = (await collectAll(provider.repository, owner, 2)).map((b) => b.id);
|
|
375
|
+
const second = (await collectAll(provider.repository, owner, 2)).map((b) => b.id);
|
|
376
|
+
expect(first).toHaveLength(5);
|
|
377
|
+
expect(second).toEqual(first);
|
|
378
|
+
});
|
|
379
|
+
it('garbage cursor does not corrupt results', async () => {
|
|
380
|
+
const owner = 'garbage-cursor';
|
|
381
|
+
await provider.repository.create(defaultInput({ createdBy: owner }));
|
|
382
|
+
// a garbage cursor should either: return an empty page, or throw,
|
|
383
|
+
// but never corrupt the data
|
|
384
|
+
const page = await provider.repository.listByOwner(owner, {
|
|
385
|
+
limit: 10,
|
|
386
|
+
cursor: 'ZZZZ-NOT-A-REAL-CURSOR',
|
|
387
|
+
});
|
|
388
|
+
// just verify it returned a valid page shape
|
|
389
|
+
expect(Array.isArray(page.items)).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
// 6. Search visibility
|
|
394
|
+
// -----------------------------------------------------------------------
|
|
395
|
+
describe('Search visibility', () => {
|
|
396
|
+
it('viewer sees PUBLIC documents', async () => {
|
|
397
|
+
const doc = await provider.repository.create(defaultInput({ title: 'public searchable', visibility: 'PUBLIC', createdBy: 'owner1' }));
|
|
398
|
+
provider.search.add({
|
|
399
|
+
documentId: doc.id,
|
|
400
|
+
owner: 'owner1',
|
|
401
|
+
visibility: 'PUBLIC',
|
|
402
|
+
title: 'public searchable',
|
|
403
|
+
content: 'hello',
|
|
404
|
+
});
|
|
405
|
+
const results = provider.search.query('searchable', 'viewer1');
|
|
406
|
+
expect(results.hits.length).toBeGreaterThanOrEqual(1);
|
|
407
|
+
expect(results.hits.some((h) => h.documentId === doc.id)).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
it('viewer sees own PRIVATE documents', async () => {
|
|
410
|
+
const doc = await provider.repository.create(defaultInput({ title: 'private mine', visibility: 'PRIVATE', createdBy: 'owner1' }));
|
|
411
|
+
provider.search.add({
|
|
412
|
+
documentId: doc.id,
|
|
413
|
+
owner: 'owner1',
|
|
414
|
+
visibility: 'PRIVATE',
|
|
415
|
+
title: 'private mine',
|
|
416
|
+
content: 'secret',
|
|
417
|
+
});
|
|
418
|
+
const results = provider.search.query('private', 'owner1');
|
|
419
|
+
expect(results.hits.some((h) => h.documentId === doc.id)).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
it('viewer cannot see another owner PRIVATE doc', async () => {
|
|
422
|
+
const doc = await provider.repository.create(defaultInput({ title: 'private other', visibility: 'PRIVATE', createdBy: 'owner1' }));
|
|
423
|
+
provider.search.add({
|
|
424
|
+
documentId: doc.id,
|
|
425
|
+
owner: 'owner1',
|
|
426
|
+
visibility: 'PRIVATE',
|
|
427
|
+
title: 'private other',
|
|
428
|
+
content: 'secret other',
|
|
429
|
+
});
|
|
430
|
+
const results = provider.search.query('private', 'intruder');
|
|
431
|
+
expect(results.hits.every((h) => h.documentId !== doc.id)).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
it('null viewer sees only PUBLIC', async () => {
|
|
434
|
+
const pub = await provider.repository.create(defaultInput({ title: 'anon public', visibility: 'PUBLIC', createdBy: 'owner1' }));
|
|
435
|
+
const priv = await provider.repository.create(defaultInput({ title: 'anon private', visibility: 'PRIVATE', createdBy: 'owner1' }));
|
|
436
|
+
provider.search.add({
|
|
437
|
+
documentId: pub.id,
|
|
438
|
+
owner: 'owner1',
|
|
439
|
+
visibility: 'PUBLIC',
|
|
440
|
+
title: 'anon public',
|
|
441
|
+
content: 'public content',
|
|
442
|
+
});
|
|
443
|
+
provider.search.add({
|
|
444
|
+
documentId: priv.id,
|
|
445
|
+
owner: 'owner1',
|
|
446
|
+
visibility: 'PRIVATE',
|
|
447
|
+
title: 'anon private',
|
|
448
|
+
content: 'private content',
|
|
449
|
+
});
|
|
450
|
+
const results = provider.search.query('anon', null);
|
|
451
|
+
expect(results.hits.some((h) => h.documentId === pub.id)).toBe(true);
|
|
452
|
+
expect(results.hits.every((h) => h.documentId !== priv.id)).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
it('total reflects visible matches before pagination', async () => {
|
|
455
|
+
const owner = 'totalowner';
|
|
456
|
+
for (let i = 0; i < 5; i++) {
|
|
457
|
+
const doc = await provider.repository.create(defaultInput({ title: `totaldoc ${i}`, visibility: 'PUBLIC', createdBy: owner }));
|
|
458
|
+
provider.search.add({
|
|
459
|
+
documentId: doc.id,
|
|
460
|
+
owner,
|
|
461
|
+
visibility: 'PUBLIC',
|
|
462
|
+
title: `totaldoc ${i}`,
|
|
463
|
+
content: `totaldoc content ${i}`,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const results = provider.search.query('totaldoc', owner, { limit: 2 });
|
|
467
|
+
expect(results.total).toBe(5);
|
|
468
|
+
expect(results.hits).toHaveLength(2);
|
|
469
|
+
});
|
|
470
|
+
it('remove de-indexes a doc', async () => {
|
|
471
|
+
const doc = await provider.repository.create(defaultInput({ title: 'removable search', visibility: 'PUBLIC', createdBy: 'alice' }));
|
|
472
|
+
provider.search.add({
|
|
473
|
+
documentId: doc.id,
|
|
474
|
+
owner: 'alice',
|
|
475
|
+
visibility: 'PUBLIC',
|
|
476
|
+
title: 'removable search',
|
|
477
|
+
content: 'removable content',
|
|
478
|
+
});
|
|
479
|
+
// confirm it's findable
|
|
480
|
+
let results = provider.search.query('removable', 'alice');
|
|
481
|
+
expect(results.hits.some((h) => h.documentId === doc.id)).toBe(true);
|
|
482
|
+
provider.search.remove(doc.id);
|
|
483
|
+
results = provider.search.query('removable', 'alice');
|
|
484
|
+
expect(results.hits.every((h) => h.documentId !== doc.id)).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
it('snapshot/restore round-trips the index', async () => {
|
|
487
|
+
const doc = await provider.repository.create(defaultInput({ title: 'snapshot target', visibility: 'PUBLIC', createdBy: 'alice' }));
|
|
488
|
+
provider.search.add({
|
|
489
|
+
documentId: doc.id,
|
|
490
|
+
owner: 'alice',
|
|
491
|
+
visibility: 'PUBLIC',
|
|
492
|
+
title: 'snapshot target',
|
|
493
|
+
content: 'snap content',
|
|
494
|
+
});
|
|
495
|
+
const snap = provider.search.snapshot();
|
|
496
|
+
provider.search.clear();
|
|
497
|
+
expect(provider.search.size()).toBe(0);
|
|
498
|
+
provider.search.restore(snap);
|
|
499
|
+
expect(provider.search.size()).toBe(1);
|
|
500
|
+
const results = provider.search.query('snapshot', 'alice');
|
|
501
|
+
expect(results.hits.some((h) => h.documentId === doc.id)).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
// -----------------------------------------------------------------------
|
|
505
|
+
// 7. Comments
|
|
506
|
+
// -----------------------------------------------------------------------
|
|
507
|
+
describe('Comments', () => {
|
|
508
|
+
it('add returns a populated comment', async () => {
|
|
509
|
+
const doc = await provider.repository.create(defaultInput());
|
|
510
|
+
const comment = await provider.comments.add(doc.id, {
|
|
511
|
+
author: 'commenter',
|
|
512
|
+
body: 'Nice doc!',
|
|
513
|
+
});
|
|
514
|
+
expect(comment.documentId).toBe(doc.id);
|
|
515
|
+
expect(comment.author).toBe('commenter');
|
|
516
|
+
expect(comment.body).toBe('Nice doc!');
|
|
517
|
+
expect(comment.resolved).toBe(false);
|
|
518
|
+
expect(isIso(comment.createdAt)).toBe(true);
|
|
519
|
+
expect(isValidId(comment.id)).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
it('list returns all comments for a doc', async () => {
|
|
522
|
+
const doc = await provider.repository.create(defaultInput());
|
|
523
|
+
await provider.comments.add(doc.id, { author: 'a', body: 'first' });
|
|
524
|
+
await provider.comments.add(doc.id, { author: 'b', body: 'second' });
|
|
525
|
+
const comments = await provider.comments.list(doc.id);
|
|
526
|
+
expect(comments).toHaveLength(2);
|
|
527
|
+
const bodies = comments.map((c) => c.body);
|
|
528
|
+
expect(bodies).toContain('first');
|
|
529
|
+
expect(bodies).toContain('second');
|
|
530
|
+
});
|
|
531
|
+
it('setResolved toggles the resolved flag', async () => {
|
|
532
|
+
const doc = await provider.repository.create(defaultInput());
|
|
533
|
+
const comment = await provider.comments.add(doc.id, {
|
|
534
|
+
author: 'alice',
|
|
535
|
+
body: 'todo',
|
|
536
|
+
});
|
|
537
|
+
expect(comment.resolved).toBe(false);
|
|
538
|
+
const resolved = await provider.comments.setResolved(doc.id, comment.id, true);
|
|
539
|
+
expect(resolved.resolved).toBe(true);
|
|
540
|
+
const unresolved = await provider.comments.setResolved(doc.id, comment.id, false);
|
|
541
|
+
expect(unresolved.resolved).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
it('delete removes a specific comment', async () => {
|
|
544
|
+
const doc = await provider.repository.create(defaultInput());
|
|
545
|
+
const c1 = await provider.comments.add(doc.id, { author: 'a', body: 'keep' });
|
|
546
|
+
const c2 = await provider.comments.add(doc.id, { author: 'b', body: 'remove' });
|
|
547
|
+
await provider.comments.delete(doc.id, c2.id);
|
|
548
|
+
const remaining = await provider.comments.list(doc.id);
|
|
549
|
+
expect(remaining).toHaveLength(1);
|
|
550
|
+
expect(remaining[0].id).toBe(c1.id);
|
|
551
|
+
});
|
|
552
|
+
it('deleting a doc cascades and removes its comments', async () => {
|
|
553
|
+
const doc = await provider.repository.create(defaultInput());
|
|
554
|
+
await provider.comments.add(doc.id, { author: 'a', body: 'orphaned' });
|
|
555
|
+
await provider.repository.delete(doc.id);
|
|
556
|
+
const comments = await provider.comments.list(doc.id);
|
|
557
|
+
expect(comments).toHaveLength(0);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
// -----------------------------------------------------------------------
|
|
561
|
+
// 8. Expiry
|
|
562
|
+
// -----------------------------------------------------------------------
|
|
563
|
+
describe('Expiry', () => {
|
|
564
|
+
it('listExpired returns documents at or before nowIso', async () => {
|
|
565
|
+
const past = new Date(Date.now() - 86400_000).toISOString();
|
|
566
|
+
const future = new Date(Date.now() + 86400_000).toISOString();
|
|
567
|
+
const expired = await provider.repository.create(defaultInput({ title: 'expired', expiresAt: past }));
|
|
568
|
+
await provider.repository.create(defaultInput({ title: 'still alive', expiresAt: future }));
|
|
569
|
+
const now = new Date().toISOString();
|
|
570
|
+
const ids = await provider.repository.listExpired(now, 100);
|
|
571
|
+
expect(ids).toContain(expired.id);
|
|
572
|
+
});
|
|
573
|
+
it('listExpired does not return non-expired documents', async () => {
|
|
574
|
+
const future = new Date(Date.now() + 86400_000).toISOString();
|
|
575
|
+
const alive = await provider.repository.create(defaultInput({ title: 'alive', expiresAt: future }));
|
|
576
|
+
const now = new Date().toISOString();
|
|
577
|
+
const ids = await provider.repository.listExpired(now, 100);
|
|
578
|
+
expect(ids).not.toContain(alive.id);
|
|
579
|
+
});
|
|
580
|
+
it('listExpired honours limit', async () => {
|
|
581
|
+
const past = new Date(Date.now() - 86400_000).toISOString();
|
|
582
|
+
for (let i = 0; i < 5; i++) {
|
|
583
|
+
await provider.repository.create(defaultInput({ title: `exp ${i}`, expiresAt: past }));
|
|
584
|
+
}
|
|
585
|
+
const ids = await provider.repository.listExpired(new Date().toISOString(), 2);
|
|
586
|
+
expect(ids.length).toBeLessThanOrEqual(2);
|
|
587
|
+
expect(ids.length).toBeGreaterThanOrEqual(1);
|
|
588
|
+
});
|
|
589
|
+
it('documents without expiresAt are never returned by listExpired', async () => {
|
|
590
|
+
const doc = await provider.repository.create(defaultInput());
|
|
591
|
+
const ids = await provider.repository.listExpired(new Date(Date.now() + 86400_000).toISOString(), 100);
|
|
592
|
+
expect(ids).not.toContain(doc.id);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
// -----------------------------------------------------------------------
|
|
596
|
+
// 9. Size limits
|
|
597
|
+
// -----------------------------------------------------------------------
|
|
598
|
+
describe('Size limits', () => {
|
|
599
|
+
it('create throws ContentTooLargeError for oversized content', async () => {
|
|
600
|
+
const oversize = 'x'.repeat(LIMITS.MAX_CONTENT_BYTES + 1);
|
|
601
|
+
await expect(provider.repository.create(defaultInput({ content: oversize }))).rejects.toThrow(ContentTooLargeError);
|
|
602
|
+
});
|
|
603
|
+
it('appendVersion throws ContentTooLargeError for oversized content', async () => {
|
|
604
|
+
const doc = await provider.repository.create(defaultInput());
|
|
605
|
+
const oversize = 'x'.repeat(LIMITS.MAX_CONTENT_BYTES + 1);
|
|
606
|
+
await expect(provider.repository.appendVersion(doc.id, {
|
|
607
|
+
content: oversize,
|
|
608
|
+
editedBy: 'alice',
|
|
609
|
+
expect: { latestVersion: 1 },
|
|
610
|
+
})).rejects.toThrow(ContentTooLargeError);
|
|
611
|
+
});
|
|
612
|
+
it('content exactly at size boundary is accepted', async () => {
|
|
613
|
+
const exact = 'x'.repeat(LIMITS.MAX_CONTENT_BYTES);
|
|
614
|
+
const doc = await provider.repository.create(defaultInput({ content: exact }));
|
|
615
|
+
expect(doc.latestVersion).toBe(1);
|
|
616
|
+
});
|
|
617
|
+
it('content one byte over boundary is rejected', async () => {
|
|
618
|
+
const overByOne = 'x'.repeat(LIMITS.MAX_CONTENT_BYTES + 1);
|
|
619
|
+
await expect(provider.repository.create(defaultInput({ content: overByOne }))).rejects.toThrow(ContentTooLargeError);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
// -----------------------------------------------------------------------
|
|
623
|
+
// 10. Unicode and boundaries
|
|
624
|
+
// -----------------------------------------------------------------------
|
|
625
|
+
describe('Unicode and boundaries', () => {
|
|
626
|
+
it('multi-byte content (emoji) round-trips byte-exactly', async () => {
|
|
627
|
+
const content = 'πππ¦ Hello, δΈη! ΓoΓ±o π';
|
|
628
|
+
const doc = await provider.repository.create(defaultInput({ content }));
|
|
629
|
+
const v = await provider.repository.getVersion(doc.id, 1);
|
|
630
|
+
expect(v.content).toBe(content);
|
|
631
|
+
});
|
|
632
|
+
it('CJK content round-trips', async () => {
|
|
633
|
+
const content = 'ζΌ’εγγΉγνκ΅μ΄ζ΅θ―';
|
|
634
|
+
const doc = await provider.repository.create(defaultInput({ content }));
|
|
635
|
+
const v = await provider.repository.getVersion(doc.id, 1);
|
|
636
|
+
expect(v.content).toBe(content);
|
|
637
|
+
});
|
|
638
|
+
it('combining marks round-trip', async () => {
|
|
639
|
+
// Γ© as e + combining acute accent
|
|
640
|
+
const content = 'e\u0301 cafe\u0301';
|
|
641
|
+
const doc = await provider.repository.create(defaultInput({ content }));
|
|
642
|
+
const v = await provider.repository.getVersion(doc.id, 1);
|
|
643
|
+
expect(v.content).toBe(content);
|
|
644
|
+
});
|
|
645
|
+
it('empty-string content is accepted and round-trips', async () => {
|
|
646
|
+
const doc = await provider.repository.create(defaultInput({ content: '' }));
|
|
647
|
+
const v = await provider.repository.getVersion(doc.id, 1);
|
|
648
|
+
expect(v.content).toBe('');
|
|
649
|
+
});
|
|
650
|
+
it('multi-byte boundary: content at exact byte limit accepted', async () => {
|
|
651
|
+
// Create content that uses multi-byte chars to exactly hit the limit.
|
|
652
|
+
// Each 'π' is 4 bytes. Fill with ASCII then pad to exact boundary.
|
|
653
|
+
const asciiPart = 'x'.repeat(LIMITS.MAX_CONTENT_BYTES - 4);
|
|
654
|
+
const content = asciiPart + 'π'; // exactly MAX_CONTENT_BYTES bytes
|
|
655
|
+
expect(byteLen(content)).toBe(LIMITS.MAX_CONTENT_BYTES);
|
|
656
|
+
const doc = await provider.repository.create(defaultInput({ content }));
|
|
657
|
+
expect(doc.latestVersion).toBe(1);
|
|
658
|
+
});
|
|
659
|
+
it('multi-byte boundary: one byte over limit rejected', async () => {
|
|
660
|
+
const asciiPart = 'x'.repeat(LIMITS.MAX_CONTENT_BYTES - 3);
|
|
661
|
+
const content = asciiPart + 'π'; // MAX_CONTENT_BYTES + 1 bytes
|
|
662
|
+
expect(byteLen(content)).toBe(LIMITS.MAX_CONTENT_BYTES + 1);
|
|
663
|
+
await expect(provider.repository.create(defaultInput({ content }))).rejects.toThrow(ContentTooLargeError);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
describe('Capabilities declaration', () => {
|
|
667
|
+
it('declares every capability field with the right type', () => {
|
|
668
|
+
const caps = provider.capabilities;
|
|
669
|
+
expect(['core-fallback', 'native']).toContain(caps.search);
|
|
670
|
+
expect(typeof caps.nativeTtl).toBe('boolean');
|
|
671
|
+
expect(typeof caps.atomicVersioning).toBe('boolean');
|
|
672
|
+
// requiresNetwork gates offline mode: an undeclared value would let a
|
|
673
|
+
// remote store start up inside an instance promising zero egress.
|
|
674
|
+
expect(typeof caps.requiresNetwork).toBe('boolean');
|
|
675
|
+
});
|
|
676
|
+
it('exposes healthCheck as a function when present, and it does not throw', async () => {
|
|
677
|
+
if (provider.healthCheck === undefined)
|
|
678
|
+
return; // optional by contract
|
|
679
|
+
expect(typeof provider.healthCheck).toBe('function');
|
|
680
|
+
const health = await provider.healthCheck();
|
|
681
|
+
expect(typeof health.healthy).toBe('boolean');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
//# sourceMappingURL=conformance.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentdocstore/provider-tests β reusable provider conformance suite.
|
|
3
|
+
*
|
|
4
|
+
* Third-party storage backends import {@link runProviderConformance} and call
|
|
5
|
+
* it from their own test file to prove SPI compliance.
|
|
6
|
+
*/
|
|
7
|
+
export { runProviderConformance } from './conformance.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentdocstore/provider-tests β reusable provider conformance suite.
|
|
3
|
+
*
|
|
4
|
+
* Third-party storage backends import {@link runProviderConformance} and call
|
|
5
|
+
* it from their own test file to prove SPI compliance.
|
|
6
|
+
*/
|
|
7
|
+
export { runProviderConformance } from './conformance.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentdocstore/provider-tests",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Reusable conformance suite for AgentDocStore storage providers. Run it against your own Provider implementation to prove SPI compliance.",
|
|
5
|
+
"homepage": "https://github.com/koushikginjupally/agentdocstore/tree/main/packages/provider-tests#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/koushikginjupally/agentdocstore/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/koushikginjupally/agentdocstore.git",
|
|
12
|
+
"directory": "packages/provider-tests"
|
|
13
|
+
},
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"!dist/**/*.map",
|
|
27
|
+
"!dist/self-check.*"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"provenance": true
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"agentdocstore",
|
|
38
|
+
"conformance",
|
|
39
|
+
"contract-tests",
|
|
40
|
+
"storage-provider"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"clean": "node ../../scripts/clean.mjs packages/provider-tests",
|
|
44
|
+
"prebuild": "npm run clean",
|
|
45
|
+
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"prepack": "npm run build",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@agentdocstore/core": "^0.2.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"vitest": ">=2.1.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"typescript": "^5.6.0",
|
|
58
|
+
"vitest": "^5.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|