Skip to content

Commit 2d536ff

Browse files
authored
Merge pull request #629 from objectstack-ai/copilot/evaluate-metadata-protocols
2 parents ac98523 + 351faa3 commit 2d536ff

File tree

11 files changed

+1968
-0
lines changed

11 files changed

+1968
-0
lines changed

packages/spec/src/cloud/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Cloud Protocol
5+
*
6+
* Cloud-specific protocols for the ObjectStack SaaS platform.
7+
* These schemas define the contract for cloud services like:
8+
* - Marketplace (listing, publishing, review, search, install)
9+
* - Future: Composer, Space, Hub Federation
10+
*/
11+
export * from './marketplace.zod';
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
PublisherVerificationSchema,
4+
PublisherSchema,
5+
MarketplaceCategorySchema,
6+
ListingStatusSchema,
7+
PricingModelSchema,
8+
MarketplaceListingSchema,
9+
PackageSubmissionSchema,
10+
MarketplaceSearchRequestSchema,
11+
MarketplaceSearchResponseSchema,
12+
MarketplaceInstallRequestSchema,
13+
MarketplaceInstallResponseSchema,
14+
} from './marketplace.zod';
15+
16+
describe('PublisherVerificationSchema', () => {
17+
it('should accept valid verification statuses', () => {
18+
const statuses = ['unverified', 'pending', 'verified', 'trusted', 'partner'];
19+
statuses.forEach(status => {
20+
expect(() => PublisherVerificationSchema.parse(status)).not.toThrow();
21+
});
22+
});
23+
24+
it('should reject invalid status', () => {
25+
expect(() => PublisherVerificationSchema.parse('approved')).toThrow();
26+
});
27+
});
28+
29+
describe('PublisherSchema', () => {
30+
it('should accept minimal publisher', () => {
31+
const publisher = {
32+
id: 'pub-001',
33+
name: 'Acme Corp',
34+
type: 'organization' as const,
35+
};
36+
const parsed = PublisherSchema.parse(publisher);
37+
expect(parsed.verification).toBe('unverified');
38+
});
39+
40+
it('should accept full publisher profile', () => {
41+
const publisher = {
42+
id: 'pub-001',
43+
name: 'Acme Corp',
44+
type: 'organization' as const,
45+
verification: 'verified' as const,
46+
email: 'support@acme.com',
47+
website: 'https://acme.com',
48+
logoUrl: 'https://acme.com/logo.png',
49+
description: 'Leading enterprise solutions provider',
50+
registeredAt: '2025-01-15T10:00:00Z',
51+
};
52+
const parsed = PublisherSchema.parse(publisher);
53+
expect(parsed.verification).toBe('verified');
54+
});
55+
});
56+
57+
describe('MarketplaceCategorySchema', () => {
58+
it('should accept all valid categories', () => {
59+
const categories = [
60+
'crm', 'erp', 'hr', 'finance', 'project', 'collaboration',
61+
'analytics', 'integration', 'automation', 'ai', 'security',
62+
'developer-tools', 'ui-theme', 'storage', 'other',
63+
];
64+
categories.forEach(cat => {
65+
expect(() => MarketplaceCategorySchema.parse(cat)).not.toThrow();
66+
});
67+
});
68+
});
69+
70+
describe('ListingStatusSchema', () => {
71+
it('should accept all listing statuses', () => {
72+
const statuses = [
73+
'draft', 'submitted', 'in-review', 'approved', 'published',
74+
'rejected', 'suspended', 'deprecated', 'unlisted',
75+
];
76+
statuses.forEach(status => {
77+
expect(() => ListingStatusSchema.parse(status)).not.toThrow();
78+
});
79+
});
80+
});
81+
82+
describe('PricingModelSchema', () => {
83+
it('should accept all pricing models', () => {
84+
const models = ['free', 'freemium', 'paid', 'subscription', 'usage-based', 'contact-sales'];
85+
models.forEach(model => {
86+
expect(() => PricingModelSchema.parse(model)).not.toThrow();
87+
});
88+
});
89+
});
90+
91+
describe('MarketplaceListingSchema', () => {
92+
it('should accept minimal listing', () => {
93+
const listing = {
94+
id: 'listing-001',
95+
packageId: 'com.acme.crm',
96+
publisherId: 'pub-001',
97+
name: 'Acme CRM',
98+
category: 'crm' as const,
99+
latestVersion: '1.0.0',
100+
};
101+
const parsed = MarketplaceListingSchema.parse(listing);
102+
expect(parsed.status).toBe('draft');
103+
expect(parsed.pricing).toBe('free');
104+
});
105+
106+
it('should accept full listing', () => {
107+
const listing = {
108+
id: 'listing-001',
109+
packageId: 'com.acme.crm',
110+
publisherId: 'pub-001',
111+
status: 'published' as const,
112+
name: 'Acme CRM',
113+
tagline: 'Complete customer relationship management for ObjectStack',
114+
description: '# Acme CRM\n\nFull-featured CRM with sales pipeline...',
115+
category: 'crm' as const,
116+
tags: ['sales', 'pipeline', 'contacts'],
117+
iconUrl: 'https://acme.com/crm-icon.png',
118+
screenshots: [
119+
{ url: 'https://acme.com/screenshot1.png', caption: 'Sales Pipeline' },
120+
{ url: 'https://acme.com/screenshot2.png', caption: 'Contact Management' },
121+
],
122+
documentationUrl: 'https://docs.acme.com/crm',
123+
supportUrl: 'https://support.acme.com',
124+
repositoryUrl: 'https://github.com/acme/crm',
125+
pricing: 'freemium' as const,
126+
priceInCents: 999,
127+
latestVersion: '2.1.0',
128+
minPlatformVersion: '1.0.0',
129+
versions: [
130+
{
131+
version: '2.1.0',
132+
releaseDate: '2025-06-01T00:00:00Z',
133+
releaseNotes: 'Added deal forecasting',
134+
},
135+
{
136+
version: '2.0.0',
137+
releaseDate: '2025-03-01T00:00:00Z',
138+
releaseNotes: 'Major update with new pipeline view',
139+
deprecated: false,
140+
},
141+
],
142+
stats: {
143+
totalInstalls: 5000,
144+
activeInstalls: 3200,
145+
averageRating: 4.5,
146+
totalRatings: 120,
147+
totalReviews: 45,
148+
},
149+
publishedAt: '2025-01-15T00:00:00Z',
150+
updatedAt: '2025-06-01T00:00:00Z',
151+
};
152+
const parsed = MarketplaceListingSchema.parse(listing);
153+
expect(parsed.screenshots).toHaveLength(2);
154+
expect(parsed.versions).toHaveLength(2);
155+
expect(parsed.stats?.totalInstalls).toBe(5000);
156+
});
157+
158+
it('should enforce tagline max length', () => {
159+
const listing = {
160+
id: 'listing-001',
161+
packageId: 'com.acme.crm',
162+
publisherId: 'pub-001',
163+
name: 'Test',
164+
tagline: 'x'.repeat(121),
165+
category: 'other' as const,
166+
latestVersion: '1.0.0',
167+
};
168+
expect(() => MarketplaceListingSchema.parse(listing)).toThrow();
169+
});
170+
});
171+
172+
describe('PackageSubmissionSchema', () => {
173+
it('should accept minimal submission', () => {
174+
const submission = {
175+
id: 'sub-001',
176+
packageId: 'com.acme.crm',
177+
version: '2.0.0',
178+
publisherId: 'pub-001',
179+
artifactUrl: 'https://registry.objectstack.io/packages/com.acme.crm-2.0.0.tgz',
180+
};
181+
const parsed = PackageSubmissionSchema.parse(submission);
182+
expect(parsed.status).toBe('pending');
183+
expect(parsed.isNewListing).toBe(false);
184+
});
185+
186+
it('should accept submission with scan results', () => {
187+
const submission = {
188+
id: 'sub-001',
189+
packageId: 'com.acme.crm',
190+
version: '2.0.0',
191+
publisherId: 'pub-001',
192+
status: 'scanning' as const,
193+
artifactUrl: 'https://registry.objectstack.io/packages/com.acme.crm-2.0.0.tgz',
194+
releaseNotes: 'Added deal module and improved account views',
195+
isNewListing: false,
196+
scanResults: {
197+
passed: true,
198+
securityScore: 92,
199+
compatibilityCheck: true,
200+
issues: [
201+
{ severity: 'low' as const, message: 'Unused dependency detected', file: 'package.json' },
202+
],
203+
},
204+
};
205+
const parsed = PackageSubmissionSchema.parse(submission);
206+
expect(parsed.scanResults?.passed).toBe(true);
207+
expect(parsed.scanResults?.securityScore).toBe(92);
208+
});
209+
210+
it('should accept all submission statuses', () => {
211+
const statuses = ['pending', 'scanning', 'in-review', 'changes-requested', 'approved', 'rejected'];
212+
statuses.forEach(status => {
213+
const submission = {
214+
id: 'sub-001',
215+
packageId: 'com.acme.crm',
216+
version: '1.0.0',
217+
publisherId: 'pub-001',
218+
status,
219+
artifactUrl: 'https://registry.objectstack.io/pkg.tgz',
220+
};
221+
expect(() => PackageSubmissionSchema.parse(submission)).not.toThrow();
222+
});
223+
});
224+
});
225+
226+
describe('MarketplaceSearchRequestSchema', () => {
227+
it('should accept empty search (browse all)', () => {
228+
const parsed = MarketplaceSearchRequestSchema.parse({});
229+
expect(parsed.sortBy).toBe('relevance');
230+
expect(parsed.sortDirection).toBe('desc');
231+
expect(parsed.page).toBe(1);
232+
expect(parsed.pageSize).toBe(20);
233+
});
234+
235+
it('should accept search with filters', () => {
236+
const request = {
237+
query: 'crm',
238+
category: 'crm' as const,
239+
pricing: 'free' as const,
240+
publisherVerification: 'verified' as const,
241+
sortBy: 'popularity' as const,
242+
page: 2,
243+
pageSize: 50,
244+
};
245+
const parsed = MarketplaceSearchRequestSchema.parse(request);
246+
expect(parsed.query).toBe('crm');
247+
expect(parsed.category).toBe('crm');
248+
expect(parsed.page).toBe(2);
249+
});
250+
251+
it('should reject invalid page size', () => {
252+
expect(() => MarketplaceSearchRequestSchema.parse({ pageSize: 0 })).toThrow();
253+
expect(() => MarketplaceSearchRequestSchema.parse({ pageSize: 101 })).toThrow();
254+
});
255+
});
256+
257+
describe('MarketplaceSearchResponseSchema', () => {
258+
it('should accept empty search results', () => {
259+
const response = {
260+
items: [],
261+
total: 0,
262+
page: 1,
263+
pageSize: 20,
264+
};
265+
const parsed = MarketplaceSearchResponseSchema.parse(response);
266+
expect(parsed.items).toHaveLength(0);
267+
});
268+
269+
it('should accept search results with facets', () => {
270+
const response = {
271+
items: [{
272+
id: 'listing-001',
273+
packageId: 'com.acme.crm',
274+
publisherId: 'pub-001',
275+
name: 'Acme CRM',
276+
category: 'crm' as const,
277+
latestVersion: '1.0.0',
278+
}],
279+
total: 1,
280+
page: 1,
281+
pageSize: 20,
282+
facets: {
283+
categories: [
284+
{ category: 'crm' as const, count: 12 },
285+
{ category: 'erp' as const, count: 8 },
286+
],
287+
pricing: [
288+
{ model: 'free' as const, count: 15 },
289+
{ model: 'paid' as const, count: 5 },
290+
],
291+
},
292+
};
293+
const parsed = MarketplaceSearchResponseSchema.parse(response);
294+
expect(parsed.facets?.categories).toHaveLength(2);
295+
});
296+
});
297+
298+
describe('MarketplaceInstallRequestSchema', () => {
299+
it('should accept minimal install request', () => {
300+
const request = {
301+
listingId: 'listing-001',
302+
};
303+
const parsed = MarketplaceInstallRequestSchema.parse(request);
304+
expect(parsed.enableOnInstall).toBe(true);
305+
});
306+
307+
it('should accept full install request with license', () => {
308+
const request = {
309+
listingId: 'listing-001',
310+
version: '2.0.0',
311+
licenseKey: 'LICENSE-KEY-12345',
312+
settings: { apiKey: 'sk-test-123' },
313+
enableOnInstall: true,
314+
tenantId: 'tenant-001',
315+
};
316+
const parsed = MarketplaceInstallRequestSchema.parse(request);
317+
expect(parsed.version).toBe('2.0.0');
318+
expect(parsed.licenseKey).toBe('LICENSE-KEY-12345');
319+
});
320+
});
321+
322+
describe('MarketplaceInstallResponseSchema', () => {
323+
it('should accept successful install', () => {
324+
const response = {
325+
success: true,
326+
packageId: 'com.acme.crm',
327+
version: '2.0.0',
328+
message: 'Package installed successfully',
329+
};
330+
const parsed = MarketplaceInstallResponseSchema.parse(response);
331+
expect(parsed.success).toBe(true);
332+
});
333+
334+
it('should accept failed install', () => {
335+
const response = {
336+
success: false,
337+
message: 'License key invalid',
338+
};
339+
const parsed = MarketplaceInstallResponseSchema.parse(response);
340+
expect(parsed.success).toBe(false);
341+
});
342+
});

0 commit comments

Comments
 (0)