@skillsregistry/contracts 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.
@@ -0,0 +1,686 @@
1
+ // ══════════════════════════════════════════════════════════════════════════════
2
+ // Zod round-trip smoke — every exported public schema
3
+ // ══════════════════════════════════════════════════════════════════════════════
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ // common
7
+ SkillIdParam, SkillSlugParam, VersionParam, HoursQuery, LimitQuery, OffsetQuery,
8
+ // responses
9
+ ErrorResponseSchema, SuccessResponseSchema, HealthResponseSchema, SearchResponseSchema, SkillDetailSchema, SkillPullResponseSchema, SkillVersionsResponseSchema, TierDistributionSchema, MatchSourcesResponseSchema, LatencyPercentilesSchema, CostBreakdownSchema, FailedQueriesResponseSchema, Tier3PatternsResponseSchema, RevokedImpactResponseSchema, VulnerableUsageResponseSchema, EvalRunResponseSchema, EvalResultsListSchema, EvalCompareResponseSchema, CompositionDetailSchema, AncestryResponseSchema, ForksResponseSchema, DependentsResponseSchema, StarResultSchema, StarStatusSchema, InvocationAcceptedSchema, CoOccurrenceResponseSchema, LeaderboardResponseSchema, PublishResultSchema, DeleteResultSchema, AuthorProfileSchema, AuthorSkillsResponseSchema,
10
+ // upstream
11
+ TrustScoreRequestSchema, TrustScoreResponseSchema, BudgetResponseSchema, PublishRequestSchema, PublishResponseSchema, TrustScoreDeltaSchema, TrustScoreSyncResponseSchema, UpstreamErrorSchema, } from './index.js';
12
+ // ──────────────────────────────────────────────────────────────────────────────
13
+ // Common OpenAPI params — string-based, minimal validation
14
+ // ──────────────────────────────────────────────────────────────────────────────
15
+ describe('common params', () => {
16
+ it('SkillIdParam accepts a UUID', () => {
17
+ const uuid = '550e8400-e29b-41d4-a716-446655440000';
18
+ expect(SkillIdParam.parse(uuid)).toBe(uuid);
19
+ });
20
+ it('SkillIdParam rejects a non-UUID', () => {
21
+ expect(() => SkillIdParam.parse('not-a-uuid')).toThrow();
22
+ });
23
+ it('SkillSlugParam / VersionParam accept plain strings', () => {
24
+ expect(SkillSlugParam.parse('rust-lint')).toBe('rust-lint');
25
+ expect(VersionParam.parse('1.0.0')).toBe('1.0.0');
26
+ });
27
+ it.each([
28
+ ['HoursQuery', HoursQuery, '24'],
29
+ ['LimitQuery', LimitQuery, '100'],
30
+ ['OffsetQuery', OffsetQuery, '0'],
31
+ ])('%s defaults to %s when omitted', (_, schema, expected) => {
32
+ expect(schema.parse(undefined)).toBe(expected);
33
+ });
34
+ it.each([
35
+ ['HoursQuery', HoursQuery, '72'],
36
+ ['LimitQuery', LimitQuery, '50'],
37
+ ['OffsetQuery', OffsetQuery, '20'],
38
+ ])('%s passes through a supplied string %s', (_, schema, value) => {
39
+ expect(schema.parse(value)).toBe(value);
40
+ });
41
+ });
42
+ // ──────────────────────────────────────────────────────────────────────────────
43
+ // Response envelopes
44
+ // ──────────────────────────────────────────────────────────────────────────────
45
+ describe('generic envelopes', () => {
46
+ it('ErrorResponseSchema round-trips', () => {
47
+ const v = { error: 'boom' };
48
+ expect(ErrorResponseSchema.parse(v)).toEqual(v);
49
+ });
50
+ it('ErrorResponseSchema requires error', () => {
51
+ expect(() => ErrorResponseSchema.parse({})).toThrow();
52
+ });
53
+ it('SuccessResponseSchema round-trips', () => {
54
+ expect(SuccessResponseSchema.parse({ success: true })).toEqual({
55
+ success: true,
56
+ });
57
+ });
58
+ it('SuccessResponseSchema requires boolean success', () => {
59
+ expect(() => SuccessResponseSchema.parse({ success: 'yes' })).toThrow();
60
+ });
61
+ });
62
+ // ──────────────────────────────────────────────────────────────────────────────
63
+ // HealthResponse
64
+ // ──────────────────────────────────────────────────────────────────────────────
65
+ describe('HealthResponseSchema', () => {
66
+ const valid = {
67
+ ok: true,
68
+ service: 'skillsregistry',
69
+ version: '1.0.0',
70
+ environment: 'production',
71
+ dbStatus: 'ok',
72
+ dbLatencyMs: 12,
73
+ dbError: null,
74
+ tables: ['skills'],
75
+ missingTables: [],
76
+ aiStatus: 'ok',
77
+ aiError: null,
78
+ aiIdentity: 'ollama',
79
+ timestamp: '2026-01-01T00:00:00Z',
80
+ };
81
+ it('round-trips a well-formed row', () => {
82
+ expect(HealthResponseSchema.parse(valid)).toEqual(valid);
83
+ });
84
+ it('accepts aiIdentity as null / undefined', () => {
85
+ expect(HealthResponseSchema.parse({ ...valid, aiIdentity: null }).aiIdentity).toBeNull();
86
+ const { aiIdentity: _drop, ...withoutOptional } = valid;
87
+ void _drop;
88
+ expect(HealthResponseSchema.parse(withoutOptional).aiIdentity).toBeUndefined();
89
+ });
90
+ it('rejects wrong-typed dbLatencyMs', () => {
91
+ expect(() => HealthResponseSchema.parse({ ...valid, dbLatencyMs: 'fast' })).toThrow();
92
+ });
93
+ });
94
+ // ──────────────────────────────────────────────────────────────────────────────
95
+ // SearchResponse
96
+ // ──────────────────────────────────────────────────────────────────────────────
97
+ describe('SearchResponseSchema', () => {
98
+ const valid = {
99
+ skills: [
100
+ {
101
+ id: 'sk_1',
102
+ name: 'A',
103
+ slug: 'a',
104
+ description: '',
105
+ score: 0.9,
106
+ },
107
+ ],
108
+ meta: {
109
+ tier: 1,
110
+ confidence: 0.9,
111
+ signals: [{ source: 'bm25', score: 0.5, weight: 0.3 }],
112
+ latencyMs: 42,
113
+ source: 'pgvector',
114
+ cached: false,
115
+ },
116
+ };
117
+ it('round-trips a minimal shape', () => {
118
+ expect(SearchResponseSchema.parse(valid)).toEqual(valid);
119
+ });
120
+ it('tolerates deepSearchUsed being absent', () => {
121
+ const parsed = SearchResponseSchema.parse(valid);
122
+ expect(parsed.meta.deepSearchUsed).toBeUndefined();
123
+ });
124
+ it('accepts all optional ScoredSkill trust fields', () => {
125
+ const enriched = {
126
+ ...valid,
127
+ skills: [
128
+ {
129
+ ...valid.skills[0],
130
+ trustScore: 0.8,
131
+ verificationTier: 'verified',
132
+ tags: ['rust'],
133
+ category: 'dev-tools',
134
+ publisherKeyId: null,
135
+ signatureVerifiedAt: null,
136
+ signatureFailureReason: null,
137
+ },
138
+ ],
139
+ };
140
+ expect(SearchResponseSchema.parse(enriched)).toEqual(enriched);
141
+ });
142
+ it('rejects a signal with no score', () => {
143
+ const bad = {
144
+ ...valid,
145
+ meta: {
146
+ ...valid.meta,
147
+ signals: [{ source: 'bm25', weight: 0.3 }],
148
+ },
149
+ };
150
+ expect(() => SearchResponseSchema.parse(bad)).toThrow();
151
+ });
152
+ });
153
+ // ──────────────────────────────────────────────────────────────────────────────
154
+ // SkillDetail — the heaviest schema; smoke-test nullable fields
155
+ // ──────────────────────────────────────────────────────────────────────────────
156
+ function baseSkillDetail() {
157
+ return {
158
+ id: 'sk_1',
159
+ name: 'A',
160
+ slug: 'a',
161
+ version: '1.0.0',
162
+ description: '',
163
+ agentSummary: null,
164
+ trustScore: 0.5,
165
+ verificationTier: 'verified',
166
+ trustBadge: null,
167
+ status: 'published',
168
+ executionLayer: 'api',
169
+ mcpUrl: null,
170
+ skillMd: null,
171
+ capabilitiesRequired: [],
172
+ skillType: 'atomic',
173
+ schemaJson: null,
174
+ source: 'manual',
175
+ sourceUrl: null,
176
+ tags: [],
177
+ category: null,
178
+ categories: [],
179
+ ecosystem: null,
180
+ language: null,
181
+ license: null,
182
+ readme: null,
183
+ r2BundleKey: null,
184
+ authRequirements: null,
185
+ installMethod: null,
186
+ forkedFrom: null,
187
+ runCount: 0,
188
+ lastRunAt: null,
189
+ authorId: null,
190
+ authorType: 'human',
191
+ tenantId: null,
192
+ revokedReason: null,
193
+ remediationMessage: null,
194
+ remediationUrl: null,
195
+ replacementSkillId: null,
196
+ replacementSlug: null,
197
+ shareUrl: 'https://x/skills/a',
198
+ avgExecutionTimeMs: null,
199
+ errorRate: null,
200
+ humanStarCount: 0,
201
+ humanForkCount: 0,
202
+ agentInvocationCount: 0,
203
+ runtimeEnv: 'api',
204
+ visibility: 'public',
205
+ environmentVariables: [],
206
+ cogniumScanned: false,
207
+ cogniumScannedAt: null,
208
+ scanCoverage: null,
209
+ contentSafetyPassed: null,
210
+ qualityScore: null,
211
+ qualityTier: null,
212
+ qualityResults: null,
213
+ qualityAnalyzedAt: null,
214
+ trustScoreV2: null,
215
+ trustTier: null,
216
+ trustResults: null,
217
+ trustAnalyzedAt: null,
218
+ understandResults: null,
219
+ understandAnalyzedAt: null,
220
+ specAlignmentScore: null,
221
+ specGaps: null,
222
+ specAnalyzedAt: null,
223
+ publisherKeyId: null,
224
+ signatureVerifiedAt: null,
225
+ signatureFailureReason: null,
226
+ createdAt: null,
227
+ updatedAt: null,
228
+ publishedAt: null,
229
+ };
230
+ }
231
+ describe('SkillDetailSchema', () => {
232
+ it('round-trips a fully-null baseline', () => {
233
+ const v = baseSkillDetail();
234
+ expect(SkillDetailSchema.parse(v)).toEqual(v);
235
+ });
236
+ it('rejects when a required non-nullable field is missing', () => {
237
+ const { id: _drop, ...bad } = baseSkillDetail();
238
+ void _drop;
239
+ expect(() => SkillDetailSchema.parse(bad)).toThrow();
240
+ });
241
+ it('rejects when contentSafetyPassed is a string instead of boolean|null', () => {
242
+ expect(() => SkillDetailSchema.parse({ ...baseSkillDetail(), contentSafetyPassed: 'yes' })).toThrow();
243
+ });
244
+ it('accepts D2 publisher-signing fields as strings or nulls', () => {
245
+ const v = {
246
+ ...baseSkillDetail(),
247
+ publisherKeyId: 'pk_1',
248
+ signatureVerifiedAt: '2026-01-01T00:00:00Z',
249
+ signatureFailureReason: null,
250
+ };
251
+ expect(SkillDetailSchema.parse(v).publisherKeyId).toBe('pk_1');
252
+ });
253
+ });
254
+ // ──────────────────────────────────────────────────────────────────────────────
255
+ // SkillPull / SkillVersions
256
+ // ──────────────────────────────────────────────────────────────────────────────
257
+ describe('SkillPullResponseSchema', () => {
258
+ const v = {
259
+ slug: 'a',
260
+ version: '1.0.0',
261
+ name: 'A',
262
+ description: null,
263
+ skillMd: null,
264
+ schemaJson: null,
265
+ executionLayer: 'api',
266
+ runtimeEnv: 'api',
267
+ portable: true,
268
+ trustScore: 0.9,
269
+ verificationTier: 'verified',
270
+ trustBadge: null,
271
+ status: 'published',
272
+ tags: [],
273
+ categories: [],
274
+ authRequirements: null,
275
+ capabilitiesRequired: [],
276
+ mcpUrl: null,
277
+ forkedFrom: null,
278
+ source: 'manual',
279
+ publisherKeyId: null,
280
+ signatureVerifiedAt: null,
281
+ signatureFailureReason: null,
282
+ };
283
+ it('round-trips a well-formed pull row', () => {
284
+ expect(SkillPullResponseSchema.parse(v)).toEqual(v);
285
+ });
286
+ it('rejects when portable is missing', () => {
287
+ const { portable: _drop, ...bad } = v;
288
+ void _drop;
289
+ expect(() => SkillPullResponseSchema.parse(bad)).toThrow();
290
+ });
291
+ });
292
+ describe('SkillVersionsResponseSchema', () => {
293
+ it('round-trips an empty list', () => {
294
+ const v = { slug: 'a', totalVersions: 0, versions: [] };
295
+ expect(SkillVersionsResponseSchema.parse(v)).toEqual(v);
296
+ });
297
+ });
298
+ // ──────────────────────────────────────────────────────────────────────────────
299
+ // Analytics envelopes with passthrough
300
+ // ──────────────────────────────────────────────────────────────────────────────
301
+ describe('analytics passthrough schemas', () => {
302
+ it('TierDistribution keeps extra fields via passthrough', () => {
303
+ const v = { tier1: 1, tier2: 2, tier3: 3, total: 6, extra: 'ok' };
304
+ expect(TierDistributionSchema.parse(v)).toEqual(v);
305
+ });
306
+ it('LatencyPercentiles keeps extra fields', () => {
307
+ const v = { p50: 10, p95: 20, p99: 30, p999: 40 };
308
+ expect(LatencyPercentilesSchema.parse(v)).toEqual(v);
309
+ });
310
+ it('CostBreakdown accepts any object', () => {
311
+ const v = { a: 1, b: 2 };
312
+ expect(CostBreakdownSchema.parse(v)).toEqual(v);
313
+ });
314
+ it('MatchSourcesResponse accepts a numeric record', () => {
315
+ const v = { matchSources: { bm25: 3, vec: 5 } };
316
+ expect(MatchSourcesResponseSchema.parse(v)).toEqual(v);
317
+ });
318
+ it('FailedQueriesResponse accepts unknown[] payloads', () => {
319
+ const v = { queries: [{ q: 'foo', tier: 3 }, 'bar'] };
320
+ expect(FailedQueriesResponseSchema.parse(v)).toEqual(v);
321
+ });
322
+ it('Tier3PatternsResponse accepts unknown[] payloads', () => {
323
+ const v = { patterns: [1, 2, 3] };
324
+ expect(Tier3PatternsResponseSchema.parse(v)).toEqual(v);
325
+ });
326
+ it('RevokedImpactResponse round-trips', () => {
327
+ const v = {
328
+ revokedCount: 2,
329
+ revokedSkills: [{ id: 'a' }],
330
+ affectedSearches30d: 10,
331
+ };
332
+ expect(RevokedImpactResponseSchema.parse(v)).toEqual(v);
333
+ });
334
+ it('VulnerableUsageResponse round-trips', () => {
335
+ const v = { vulnerableCount: 3, vulnerableSkills: [], appearedInSearch30d: 5 };
336
+ expect(VulnerableUsageResponseSchema.parse(v)).toEqual(v);
337
+ });
338
+ });
339
+ // ──────────────────────────────────────────────────────────────────────────────
340
+ // Eval envelopes
341
+ // ──────────────────────────────────────────────────────────────────────────────
342
+ describe('EvalRunResponseSchema', () => {
343
+ const v = {
344
+ success: true,
345
+ runId: 'r_1',
346
+ timestamp: '2026-01-01T00:00:00Z',
347
+ metrics: { recall1: 0.9, recall5: 0.99, mrr: 0.95 },
348
+ summary: { fixtureCount: 91, passed: 91, failed: 0 },
349
+ errors: [],
350
+ };
351
+ it('round-trips', () => {
352
+ expect(EvalRunResponseSchema.parse(v)).toEqual(v);
353
+ });
354
+ it('tolerates extra metrics keys via passthrough', () => {
355
+ const enriched = {
356
+ ...v,
357
+ metrics: { ...v.metrics, custom: 0.5 },
358
+ };
359
+ expect(EvalRunResponseSchema.parse(enriched).metrics.custom).toBe(0.5);
360
+ });
361
+ });
362
+ describe('EvalResultsListSchema + EvalCompareResponseSchema', () => {
363
+ it('EvalResultsList round-trips an empty list', () => {
364
+ expect(EvalResultsListSchema.parse({ runs: [] })).toEqual({ runs: [] });
365
+ });
366
+ it('EvalCompare accepts arbitrary metrics/summary/tierDistribution', () => {
367
+ const v = {
368
+ runA: { runId: 'a', timestamp: 't' },
369
+ runB: { runId: 'b', timestamp: 't' },
370
+ metrics: { any: 1 },
371
+ summary: { any: 2 },
372
+ tierDistribution: { t1: 10 },
373
+ };
374
+ expect(EvalCompareResponseSchema.parse(v)).toEqual(v);
375
+ });
376
+ });
377
+ // ──────────────────────────────────────────────────────────────────────────────
378
+ // Composition / Lineage
379
+ // ──────────────────────────────────────────────────────────────────────────────
380
+ describe('CompositionDetailSchema', () => {
381
+ it('round-trips a composition with steps + passthrough fields', () => {
382
+ const v = {
383
+ id: 'comp_1',
384
+ steps: [
385
+ {
386
+ id: 's1',
387
+ stepOrder: 1,
388
+ skillId: 'sk_a',
389
+ skillName: 'A',
390
+ skillSlug: 'a',
391
+ stepName: null,
392
+ inputMapping: null,
393
+ onError: 'fail',
394
+ },
395
+ ],
396
+ };
397
+ expect(CompositionDetailSchema.parse(v)).toEqual(v);
398
+ });
399
+ });
400
+ describe('lineage envelopes', () => {
401
+ it('AncestryResponse round-trips', () => {
402
+ expect(AncestryResponseSchema.parse({ ancestry: [] })).toEqual({ ancestry: [] });
403
+ });
404
+ it('ForksResponse round-trips', () => {
405
+ expect(ForksResponseSchema.parse({ forks: [] })).toEqual({ forks: [] });
406
+ });
407
+ it('DependentsResponse round-trips', () => {
408
+ expect(DependentsResponseSchema.parse({ dependents: [] })).toEqual({
409
+ dependents: [],
410
+ });
411
+ });
412
+ });
413
+ // ──────────────────────────────────────────────────────────────────────────────
414
+ // Social
415
+ // ──────────────────────────────────────────────────────────────────────────────
416
+ describe('social envelopes', () => {
417
+ it('StarResult keeps extra fields', () => {
418
+ const v = { starred: true, starCount: 5, extra: 1 };
419
+ expect(StarResultSchema.parse(v)).toEqual(v);
420
+ });
421
+ it('StarStatus tolerates missing starred', () => {
422
+ expect(StarStatusSchema.parse({ starCount: 5 })).toEqual({ starCount: 5 });
423
+ });
424
+ it('InvocationAccepted requires both fields', () => {
425
+ expect(InvocationAcceptedSchema.parse({ accepted: true, count: 3 })).toEqual({
426
+ accepted: true,
427
+ count: 3,
428
+ });
429
+ expect(() => InvocationAcceptedSchema.parse({ accepted: true })).toThrow();
430
+ });
431
+ it('CoOccurrence + Leaderboard round-trip', () => {
432
+ expect(CoOccurrenceResponseSchema.parse({ cooccurrence: [] })).toEqual({
433
+ cooccurrence: [],
434
+ });
435
+ expect(LeaderboardResponseSchema.parse({ leaderboard: [] })).toEqual({
436
+ leaderboard: [],
437
+ });
438
+ });
439
+ });
440
+ // ──────────────────────────────────────────────────────────────────────────────
441
+ // Publish / Author
442
+ // ──────────────────────────────────────────────────────────────────────────────
443
+ describe('publish envelopes', () => {
444
+ it('PublishResult round-trips', () => {
445
+ const v = { id: 'sk_1', slug: 'a', version: '1.0.0', status: 'published' };
446
+ expect(PublishResultSchema.parse(v)).toEqual(v);
447
+ });
448
+ it('DeleteResult requires literal "deleted" status', () => {
449
+ expect(DeleteResultSchema.parse({ id: 'sk_1', status: 'deleted' })).toEqual({
450
+ id: 'sk_1',
451
+ status: 'deleted',
452
+ });
453
+ expect(() => DeleteResultSchema.parse({ id: 'sk_1', status: 'archived' })).toThrow();
454
+ });
455
+ });
456
+ describe('author envelopes', () => {
457
+ const profile = {
458
+ id: 'author_1',
459
+ handle: 'eyal',
460
+ displayName: null,
461
+ authorType: 'human',
462
+ bio: null,
463
+ avatarUrl: null,
464
+ homepageUrl: null,
465
+ botModel: null,
466
+ verified: false,
467
+ stats: {
468
+ publishedCount: 0,
469
+ totalStars: 0,
470
+ totalInvocations: 0,
471
+ totalForks: 0,
472
+ },
473
+ createdAt: '2026-01-01T00:00:00Z',
474
+ };
475
+ it('AuthorProfile round-trips', () => {
476
+ expect(AuthorProfileSchema.parse(profile)).toEqual(profile);
477
+ });
478
+ it('AuthorSkillsResponse round-trips', () => {
479
+ const v = { skills: [], limit: 20, offset: 0 };
480
+ expect(AuthorSkillsResponseSchema.parse(v)).toEqual(v);
481
+ });
482
+ });
483
+ // ──────────────────────────────────────────────────────────────────────────────
484
+ // Upstream (mothership) contracts
485
+ // ──────────────────────────────────────────────────────────────────────────────
486
+ describe('TrustScoreRequestSchema', () => {
487
+ it('accepts a bare skill_id', () => {
488
+ expect(TrustScoreRequestSchema.parse({ skill_id: 'sk_1' })).toEqual({
489
+ skill_id: 'sk_1',
490
+ });
491
+ });
492
+ it('rejects empty skill_id (min 1)', () => {
493
+ expect(() => TrustScoreRequestSchema.parse({ skill_id: '' })).toThrow();
494
+ });
495
+ it('accepts optional manifest snapshot', () => {
496
+ const v = {
497
+ skill_id: 'sk_1',
498
+ manifest: { name: 'A', version: '1.0.0', source: 'manual' },
499
+ tenant_id: 'tenant_a',
500
+ };
501
+ expect(TrustScoreRequestSchema.parse(v)).toEqual(v);
502
+ });
503
+ });
504
+ describe('TrustScoreResponseSchema', () => {
505
+ const v = {
506
+ skill_id: 'sk_1',
507
+ trust_score: 0.87,
508
+ trust_tier: 'A',
509
+ trust_breakdown: { security: 0.9, spec: 0.8 },
510
+ scored_at: '2026-01-01T00:00:00Z',
511
+ tokens_consumed: 42,
512
+ };
513
+ it('round-trips', () => {
514
+ expect(TrustScoreResponseSchema.parse(v)).toEqual(v);
515
+ });
516
+ it('rejects trust_score > 1', () => {
517
+ expect(() => TrustScoreResponseSchema.parse({ ...v, trust_score: 1.1 })).toThrow();
518
+ });
519
+ it('rejects unknown trust_tier', () => {
520
+ expect(() => TrustScoreResponseSchema.parse({ ...v, trust_tier: 'S' })).toThrow();
521
+ });
522
+ it('rejects fractional tokens_consumed (int check)', () => {
523
+ expect(() => TrustScoreResponseSchema.parse({ ...v, tokens_consumed: 1.5 })).toThrow();
524
+ });
525
+ it('rejects negative tokens_consumed', () => {
526
+ expect(() => TrustScoreResponseSchema.parse({ ...v, tokens_consumed: -1 })).toThrow();
527
+ });
528
+ });
529
+ describe('BudgetResponseSchema', () => {
530
+ const v = {
531
+ tenant_id: 'tenant_a',
532
+ plan: 'growth',
533
+ tokens_total: 1000,
534
+ tokens_remaining: 500,
535
+ tokens_reset_at: '2026-01-01T00:00:00Z',
536
+ low_balance: false,
537
+ };
538
+ it('round-trips', () => {
539
+ expect(BudgetResponseSchema.parse(v)).toEqual(v);
540
+ });
541
+ it('rejects unknown plan', () => {
542
+ expect(() => BudgetResponseSchema.parse({ ...v, plan: 'gold' })).toThrow();
543
+ });
544
+ });
545
+ describe('PublishRequestSchema', () => {
546
+ it('accepts a minimal publish request', () => {
547
+ const v = {
548
+ manifest: {
549
+ name: 'A',
550
+ slug: 'a',
551
+ version: '1.0.0',
552
+ source: 'manual',
553
+ execution_layer: 'api',
554
+ },
555
+ };
556
+ expect(PublishRequestSchema.parse(v)).toEqual(v);
557
+ });
558
+ it('rejects a bad source_url', () => {
559
+ expect(() => PublishRequestSchema.parse({
560
+ manifest: {
561
+ name: 'A',
562
+ slug: 'a',
563
+ version: '1.0.0',
564
+ source: 'manual',
565
+ execution_layer: 'api',
566
+ source_url: 'not-a-url',
567
+ },
568
+ })).toThrow();
569
+ });
570
+ });
571
+ describe('PublishResponseSchema', () => {
572
+ it('rejects unknown status', () => {
573
+ expect(() => PublishResponseSchema.parse({
574
+ skill_id: 'sk_1',
575
+ slug: 'a',
576
+ version: '1.0.0',
577
+ status: 'weird',
578
+ published_at: '2026-01-01T00:00:00Z',
579
+ url: 'https://x/skills/a',
580
+ })).toThrow();
581
+ });
582
+ it.each(['published', 'pending_review', 'rejected'])('accepts status %s', (status) => {
583
+ const v = {
584
+ skill_id: 'sk_1',
585
+ slug: 'a',
586
+ version: '1.0.0',
587
+ status,
588
+ published_at: '2026-01-01T00:00:00Z',
589
+ url: 'https://x/skills/a',
590
+ };
591
+ expect(PublishResponseSchema.parse(v)).toEqual(v);
592
+ });
593
+ });
594
+ describe('TrustScoreDelta + Sync', () => {
595
+ it('TrustScoreDelta round-trips', () => {
596
+ const v = {
597
+ skill_id: 'sk_1',
598
+ trust_score: 0.5,
599
+ trust_tier: 'B',
600
+ scored_at: '2026-01-01T00:00:00Z',
601
+ };
602
+ expect(TrustScoreDeltaSchema.parse(v)).toEqual(v);
603
+ });
604
+ it('TrustScoreSyncResponse round-trips with next_cursor', () => {
605
+ const v = {
606
+ since: '2026-01-01T00:00:00Z',
607
+ until: '2026-01-02T00:00:00Z',
608
+ count: 1,
609
+ deltas: [
610
+ {
611
+ skill_id: 'sk_1',
612
+ trust_score: 0.5,
613
+ trust_tier: 'B',
614
+ scored_at: '2026-01-01T12:00:00Z',
615
+ },
616
+ ],
617
+ next_cursor: '2026-01-01T12:00:00Z',
618
+ };
619
+ expect(TrustScoreSyncResponseSchema.parse(v)).toEqual(v);
620
+ });
621
+ it('TrustScoreSyncResponse round-trips without next_cursor', () => {
622
+ const v = {
623
+ since: '2026-01-01T00:00:00Z',
624
+ until: '2026-01-02T00:00:00Z',
625
+ count: 0,
626
+ deltas: [],
627
+ };
628
+ expect(TrustScoreSyncResponseSchema.parse(v)).toEqual(v);
629
+ });
630
+ });
631
+ // ──────────────────────────────────────────────────────────────────────────────
632
+ // UpstreamErrorSchema — full taxonomy
633
+ // ──────────────────────────────────────────────────────────────────────────────
634
+ const UPSTREAM_CODES = [
635
+ 'upstream_not_configured',
636
+ 'budget_exhausted',
637
+ 'unauthenticated',
638
+ 'forbidden',
639
+ 'not_found',
640
+ 'rate_limited',
641
+ 'bad_request',
642
+ 'upstream_unavailable',
643
+ 'upstream_timeout',
644
+ ];
645
+ describe('UpstreamErrorSchema', () => {
646
+ it.each(UPSTREAM_CODES)('accepts code %s', (code) => {
647
+ const v = { error: { code, message: 'x' } };
648
+ expect(UpstreamErrorSchema.parse(v)).toEqual(v);
649
+ });
650
+ it('rejects a code outside the taxonomy', () => {
651
+ expect(() => UpstreamErrorSchema.parse({
652
+ error: { code: 'kaboom', message: 'x' },
653
+ })).toThrow();
654
+ });
655
+ it('accepts retry_after and detail as optional fields', () => {
656
+ const v = {
657
+ error: {
658
+ code: 'rate_limited',
659
+ message: 'slow down',
660
+ retry_after: 30,
661
+ detail: { field: 'query' },
662
+ },
663
+ request_id: 'req_1',
664
+ };
665
+ expect(UpstreamErrorSchema.parse(v)).toEqual(v);
666
+ });
667
+ it('rejects a fractional retry_after (int check)', () => {
668
+ expect(() => UpstreamErrorSchema.parse({
669
+ error: {
670
+ code: 'rate_limited',
671
+ message: 'x',
672
+ retry_after: 1.5,
673
+ },
674
+ })).toThrow();
675
+ });
676
+ it('rejects a non-positive retry_after (positive check)', () => {
677
+ expect(() => UpstreamErrorSchema.parse({
678
+ error: {
679
+ code: 'rate_limited',
680
+ message: 'x',
681
+ retry_after: 0,
682
+ },
683
+ })).toThrow();
684
+ });
685
+ });
686
+ //# sourceMappingURL=schemas.test.js.map