Skip to content

Commit bf3a942

Browse files
Copilothotlong
andcommitted
Add tests for metadata-persistence, migration, registry-config, and search-engine schemas
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 43be582 commit bf3a942

4 files changed

Lines changed: 1129 additions & 0 deletions

File tree

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
MetadataScopeSchema,
4+
MetadataStateSchema,
5+
MetadataRecordSchema,
6+
MetadataFormatSchema,
7+
MetadataStatsSchema,
8+
MetadataLoaderContractSchema,
9+
MetadataLoadOptionsSchema,
10+
MetadataLoadResultSchema,
11+
MetadataSaveOptionsSchema,
12+
MetadataSaveResultSchema,
13+
MetadataWatchEventSchema,
14+
MetadataCollectionInfoSchema,
15+
MetadataExportOptionsSchema,
16+
MetadataImportOptionsSchema,
17+
MetadataManagerConfigSchema,
18+
} from './metadata-persistence.zod';
19+
20+
describe('MetadataScopeSchema', () => {
21+
it('should accept valid scopes', () => {
22+
const scopes = ['system', 'platform', 'user'];
23+
scopes.forEach((scope) => {
24+
expect(() => MetadataScopeSchema.parse(scope)).not.toThrow();
25+
});
26+
});
27+
28+
it('should reject invalid scopes', () => {
29+
expect(() => MetadataScopeSchema.parse('invalid')).toThrow();
30+
expect(() => MetadataScopeSchema.parse('admin')).toThrow();
31+
});
32+
});
33+
34+
describe('MetadataStateSchema', () => {
35+
it('should accept valid states', () => {
36+
const states = ['draft', 'active', 'archived', 'deprecated'];
37+
states.forEach((state) => {
38+
expect(() => MetadataStateSchema.parse(state)).not.toThrow();
39+
});
40+
});
41+
42+
it('should reject invalid states', () => {
43+
expect(() => MetadataStateSchema.parse('deleted')).toThrow();
44+
});
45+
});
46+
47+
describe('MetadataRecordSchema', () => {
48+
it('should accept valid record with defaults', () => {
49+
const record = MetadataRecordSchema.parse({
50+
id: 'abc-123',
51+
name: 'account_list_view',
52+
type: 'view',
53+
metadata: { columns: ['name', 'email'] },
54+
});
55+
56+
expect(record.id).toBe('abc-123');
57+
expect(record.name).toBe('account_list_view');
58+
expect(record.type).toBe('view');
59+
expect(record.namespace).toBe('default');
60+
expect(record.scope).toBe('platform');
61+
expect(record.strategy).toBe('merge');
62+
expect(record.state).toBe('active');
63+
});
64+
65+
it('should accept full record', () => {
66+
const record = MetadataRecordSchema.parse({
67+
id: 'abc-123',
68+
name: 'account_list_view',
69+
type: 'view',
70+
namespace: 'crm',
71+
scope: 'system',
72+
metadata: { columns: ['name'] },
73+
extends: 'base_view',
74+
strategy: 'replace',
75+
owner: 'user-1',
76+
state: 'draft',
77+
createdBy: 'admin',
78+
createdAt: '2025-01-01T00:00:00Z',
79+
updatedBy: 'admin',
80+
updatedAt: '2025-01-02T00:00:00Z',
81+
});
82+
83+
expect(record.namespace).toBe('crm');
84+
expect(record.scope).toBe('system');
85+
expect(record.extends).toBe('base_view');
86+
expect(record.strategy).toBe('replace');
87+
expect(record.state).toBe('draft');
88+
});
89+
90+
it('should reject missing required fields', () => {
91+
expect(() => MetadataRecordSchema.parse({})).toThrow();
92+
expect(() => MetadataRecordSchema.parse({ id: 'x' })).toThrow();
93+
expect(() => MetadataRecordSchema.parse({ id: 'x', name: 'y', type: 'z' })).toThrow();
94+
});
95+
96+
it('should reject invalid datetime for createdAt', () => {
97+
expect(() =>
98+
MetadataRecordSchema.parse({
99+
id: 'x',
100+
name: 'y',
101+
type: 'z',
102+
metadata: {},
103+
createdAt: 'not-a-date',
104+
}),
105+
).toThrow();
106+
});
107+
});
108+
109+
describe('MetadataFormatSchema', () => {
110+
it('should accept valid formats', () => {
111+
const formats = ['json', 'yaml', 'yml', 'ts', 'js', 'typescript', 'javascript'];
112+
formats.forEach((format) => {
113+
expect(() => MetadataFormatSchema.parse(format)).not.toThrow();
114+
});
115+
});
116+
117+
it('should reject invalid formats', () => {
118+
expect(() => MetadataFormatSchema.parse('xml')).toThrow();
119+
});
120+
});
121+
122+
describe('MetadataStatsSchema', () => {
123+
it('should accept empty object (all optional)', () => {
124+
const stats = MetadataStatsSchema.parse({});
125+
expect(stats).toBeDefined();
126+
});
127+
128+
it('should accept full stats', () => {
129+
const stats = MetadataStatsSchema.parse({
130+
path: '/metadata/view.json',
131+
size: 1024,
132+
mtime: '2025-01-01T00:00:00Z',
133+
hash: 'sha256:abc',
134+
etag: '"abc123"',
135+
modifiedAt: '2025-01-01T00:00:00Z',
136+
format: 'json',
137+
});
138+
139+
expect(stats.path).toBe('/metadata/view.json');
140+
expect(stats.size).toBe(1024);
141+
expect(stats.format).toBe('json');
142+
});
143+
});
144+
145+
describe('MetadataLoaderContractSchema', () => {
146+
it('should accept valid loader with capability defaults', () => {
147+
const loader = MetadataLoaderContractSchema.parse({
148+
name: 'filesystem',
149+
protocol: 'file:',
150+
capabilities: {},
151+
});
152+
153+
expect(loader.name).toBe('filesystem');
154+
expect(loader.capabilities.read).toBe(true);
155+
expect(loader.capabilities.write).toBe(false);
156+
expect(loader.capabilities.watch).toBe(false);
157+
expect(loader.capabilities.list).toBe(true);
158+
});
159+
160+
it('should accept full loader config', () => {
161+
const loader = MetadataLoaderContractSchema.parse({
162+
name: 'http-loader',
163+
protocol: 'http:',
164+
description: 'Loads metadata over HTTP',
165+
supportedFormats: ['json', 'yaml'],
166+
supportsWatch: false,
167+
supportsWrite: true,
168+
supportsCache: true,
169+
capabilities: { read: true, write: true, watch: false, list: true },
170+
});
171+
172+
expect(loader.description).toBe('Loads metadata over HTTP');
173+
expect(loader.supportedFormats).toEqual(['json', 'yaml']);
174+
});
175+
176+
it('should reject missing required fields', () => {
177+
expect(() => MetadataLoaderContractSchema.parse({})).toThrow();
178+
expect(() => MetadataLoaderContractSchema.parse({ name: 'x' })).toThrow();
179+
});
180+
});
181+
182+
describe('MetadataLoadOptionsSchema', () => {
183+
it('should accept empty object (all optional)', () => {
184+
const opts = MetadataLoadOptionsSchema.parse({});
185+
expect(opts).toBeDefined();
186+
});
187+
188+
it('should accept full options', () => {
189+
const opts = MetadataLoadOptionsSchema.parse({
190+
scope: 'user',
191+
namespace: 'crm',
192+
raw: true,
193+
cache: true,
194+
useCache: true,
195+
validate: true,
196+
ifNoneMatch: '"etag-123"',
197+
recursive: true,
198+
limit: 50,
199+
patterns: ['*.view.json'],
200+
loader: 'filesystem',
201+
});
202+
203+
expect(opts.scope).toBe('user');
204+
expect(opts.limit).toBe(50);
205+
});
206+
});
207+
208+
describe('MetadataLoadResultSchema', () => {
209+
it('should accept minimal result', () => {
210+
const result = MetadataLoadResultSchema.parse({ data: null });
211+
expect(result.data).toBeNull();
212+
});
213+
214+
it('should accept full result', () => {
215+
const result = MetadataLoadResultSchema.parse({
216+
data: { name: 'test' },
217+
stats: { size: 100 },
218+
format: 'yaml',
219+
source: '/metadata/test.yaml',
220+
fromCache: true,
221+
etag: '"abc"',
222+
notModified: false,
223+
loadTime: 42,
224+
});
225+
226+
expect(result.format).toBe('yaml');
227+
expect(result.fromCache).toBe(true);
228+
expect(result.loadTime).toBe(42);
229+
});
230+
});
231+
232+
describe('MetadataSaveOptionsSchema', () => {
233+
it('should accept empty object with defaults', () => {
234+
const opts = MetadataSaveOptionsSchema.parse({});
235+
expect(opts.create).toBe(true);
236+
expect(opts.overwrite).toBe(true);
237+
});
238+
239+
it('should accept full options', () => {
240+
const opts = MetadataSaveOptionsSchema.parse({
241+
format: 'json',
242+
create: false,
243+
overwrite: false,
244+
path: '/metadata/out.json',
245+
prettify: true,
246+
indent: 2,
247+
sortKeys: true,
248+
backup: true,
249+
atomic: true,
250+
loader: 'filesystem',
251+
});
252+
253+
expect(opts.create).toBe(false);
254+
expect(opts.indent).toBe(2);
255+
});
256+
});
257+
258+
describe('MetadataSaveResultSchema', () => {
259+
it('should accept minimal result', () => {
260+
const result = MetadataSaveResultSchema.parse({ success: true });
261+
expect(result.success).toBe(true);
262+
});
263+
264+
it('should accept full result', () => {
265+
const result = MetadataSaveResultSchema.parse({
266+
success: true,
267+
path: '/metadata/out.json',
268+
stats: { size: 200 },
269+
etag: '"xyz"',
270+
size: 200,
271+
saveTime: 10,
272+
backupPath: '/metadata/out.json.bak',
273+
});
274+
275+
expect(result.backupPath).toBe('/metadata/out.json.bak');
276+
});
277+
278+
it('should reject missing success', () => {
279+
expect(() => MetadataSaveResultSchema.parse({})).toThrow();
280+
});
281+
});
282+
283+
describe('MetadataWatchEventSchema', () => {
284+
it('should accept valid event types', () => {
285+
const types = ['add', 'change', 'unlink', 'added', 'changed', 'deleted'];
286+
types.forEach((type) => {
287+
expect(() => MetadataWatchEventSchema.parse({ type, path: '/test' })).not.toThrow();
288+
});
289+
});
290+
291+
it('should accept full event', () => {
292+
const event = MetadataWatchEventSchema.parse({
293+
type: 'change',
294+
path: '/metadata/view.json',
295+
name: 'account_view',
296+
stats: { size: 512 },
297+
metadataType: 'view',
298+
data: { columns: ['name'] },
299+
timestamp: '2025-01-01T00:00:00Z',
300+
});
301+
302+
expect(event.name).toBe('account_view');
303+
expect(event.metadataType).toBe('view');
304+
});
305+
306+
it('should reject invalid event type', () => {
307+
expect(() => MetadataWatchEventSchema.parse({ type: 'modify', path: '/test' })).toThrow();
308+
});
309+
310+
it('should reject missing path', () => {
311+
expect(() => MetadataWatchEventSchema.parse({ type: 'add' })).toThrow();
312+
});
313+
});
314+
315+
describe('MetadataCollectionInfoSchema', () => {
316+
it('should accept valid collection info', () => {
317+
const info = MetadataCollectionInfoSchema.parse({
318+
type: 'view',
319+
count: 15,
320+
namespaces: ['crm', 'finance'],
321+
});
322+
323+
expect(info.type).toBe('view');
324+
expect(info.count).toBe(15);
325+
expect(info.namespaces).toEqual(['crm', 'finance']);
326+
});
327+
328+
it('should reject missing required fields', () => {
329+
expect(() => MetadataCollectionInfoSchema.parse({})).toThrow();
330+
expect(() => MetadataCollectionInfoSchema.parse({ type: 'view' })).toThrow();
331+
});
332+
});
333+
334+
describe('MetadataExportOptionsSchema', () => {
335+
it('should accept minimal export options with defaults', () => {
336+
const opts = MetadataExportOptionsSchema.parse({ output: '/export' });
337+
expect(opts.output).toBe('/export');
338+
expect(opts.format).toBe('json');
339+
});
340+
341+
it('should accept full export options', () => {
342+
const opts = MetadataExportOptionsSchema.parse({
343+
types: ['view', 'object'],
344+
namespaces: ['crm'],
345+
output: '/export/crm',
346+
format: 'yaml',
347+
});
348+
349+
expect(opts.types).toEqual(['view', 'object']);
350+
expect(opts.format).toBe('yaml');
351+
});
352+
353+
it('should reject missing output', () => {
354+
expect(() => MetadataExportOptionsSchema.parse({})).toThrow();
355+
});
356+
});
357+
358+
describe('MetadataImportOptionsSchema', () => {
359+
it('should accept minimal import options with defaults', () => {
360+
const opts = MetadataImportOptionsSchema.parse({ source: '/import' });
361+
expect(opts.source).toBe('/import');
362+
expect(opts.strategy).toBe('merge');
363+
expect(opts.validate).toBe(true);
364+
});
365+
366+
it('should accept all strategies', () => {
367+
const strategies = ['merge', 'replace', 'skip'];
368+
strategies.forEach((strategy) => {
369+
expect(() => MetadataImportOptionsSchema.parse({ source: '/x', strategy })).not.toThrow();
370+
});
371+
});
372+
373+
it('should reject missing source', () => {
374+
expect(() => MetadataImportOptionsSchema.parse({})).toThrow();
375+
});
376+
});
377+
378+
describe('MetadataManagerConfigSchema', () => {
379+
it('should accept empty object (all optional)', () => {
380+
const config = MetadataManagerConfigSchema.parse({});
381+
expect(config).toBeDefined();
382+
});
383+
384+
it('should accept full config', () => {
385+
const config = MetadataManagerConfigSchema.parse({
386+
loaders: [{ name: 'fs' }],
387+
watch: true,
388+
cache: true,
389+
basePath: '/app/metadata',
390+
rootDir: '/app',
391+
formats: ['json', 'yaml'],
392+
watchOptions: { persistent: true },
393+
});
394+
395+
expect(config.watch).toBe(true);
396+
expect(config.formats).toEqual(['json', 'yaml']);
397+
});
398+
});

0 commit comments

Comments
 (0)