Skip to content

Commit a15fcba

Browse files
authored
Merge pull request #631 from objectstack-ai/copilot/evaluate-plugin-development-process
2 parents 2d536ff + 0189de6 commit a15fcba

File tree

9 files changed

+2161
-1
lines changed

9 files changed

+2161
-1
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
ReviewModerationStatusSchema,
4+
UserReviewSchema,
5+
SubmitReviewRequestSchema,
6+
ListReviewsRequestSchema,
7+
ListReviewsResponseSchema,
8+
RecommendationReasonSchema,
9+
RecommendedAppSchema,
10+
AppDiscoveryRequestSchema,
11+
AppDiscoveryResponseSchema,
12+
SubscriptionStatusSchema,
13+
AppSubscriptionSchema,
14+
InstalledAppSummarySchema,
15+
ListInstalledAppsRequestSchema,
16+
ListInstalledAppsResponseSchema,
17+
} from './app-store.zod';
18+
19+
describe('ReviewModerationStatusSchema', () => {
20+
it('should accept all moderation statuses', () => {
21+
const statuses = ['pending', 'approved', 'flagged', 'rejected'];
22+
statuses.forEach(status => {
23+
expect(() => ReviewModerationStatusSchema.parse(status)).not.toThrow();
24+
});
25+
});
26+
});
27+
28+
describe('UserReviewSchema', () => {
29+
it('should accept minimal review (rating only)', () => {
30+
const review = {
31+
id: 'rev-001',
32+
listingId: 'listing-001',
33+
userId: 'user-001',
34+
rating: 5,
35+
submittedAt: '2025-06-01T10:00:00Z',
36+
};
37+
const parsed = UserReviewSchema.parse(review);
38+
expect(parsed.moderationStatus).toBe('pending');
39+
expect(parsed.helpfulCount).toBe(0);
40+
});
41+
42+
it('should accept full review with publisher response', () => {
43+
const review = {
44+
id: 'rev-001',
45+
listingId: 'listing-001',
46+
userId: 'user-001',
47+
displayName: 'John Doe',
48+
rating: 4,
49+
title: 'Great CRM plugin!',
50+
body: 'This plugin transformed our sales process. The pipeline view is excellent.',
51+
appVersion: '2.1.0',
52+
moderationStatus: 'approved' as const,
53+
helpfulCount: 12,
54+
publisherResponse: {
55+
body: 'Thank you for the kind review! We are glad you enjoy the pipeline view.',
56+
respondedAt: '2025-06-02T14:00:00Z',
57+
},
58+
submittedAt: '2025-06-01T10:00:00Z',
59+
updatedAt: '2025-06-02T14:00:00Z',
60+
};
61+
const parsed = UserReviewSchema.parse(review);
62+
expect(parsed.publisherResponse?.body).toContain('Thank you');
63+
expect(parsed.helpfulCount).toBe(12);
64+
});
65+
66+
it('should enforce rating range 1-5', () => {
67+
const base = {
68+
id: 'rev-001',
69+
listingId: 'listing-001',
70+
userId: 'user-001',
71+
submittedAt: '2025-06-01T10:00:00Z',
72+
};
73+
expect(() => UserReviewSchema.parse({ ...base, rating: 0 })).toThrow();
74+
expect(() => UserReviewSchema.parse({ ...base, rating: 6 })).toThrow();
75+
expect(() => UserReviewSchema.parse({ ...base, rating: 1 })).not.toThrow();
76+
expect(() => UserReviewSchema.parse({ ...base, rating: 5 })).not.toThrow();
77+
});
78+
79+
it('should enforce title max length', () => {
80+
const review = {
81+
id: 'rev-001',
82+
listingId: 'listing-001',
83+
userId: 'user-001',
84+
rating: 3,
85+
title: 'x'.repeat(201),
86+
submittedAt: '2025-06-01T10:00:00Z',
87+
};
88+
expect(() => UserReviewSchema.parse(review)).toThrow();
89+
});
90+
91+
it('should enforce body max length', () => {
92+
const review = {
93+
id: 'rev-001',
94+
listingId: 'listing-001',
95+
userId: 'user-001',
96+
rating: 3,
97+
body: 'x'.repeat(5001),
98+
submittedAt: '2025-06-01T10:00:00Z',
99+
};
100+
expect(() => UserReviewSchema.parse(review)).toThrow();
101+
});
102+
});
103+
104+
describe('SubmitReviewRequestSchema', () => {
105+
it('should accept minimal review submission', () => {
106+
const request = {
107+
listingId: 'listing-001',
108+
rating: 5,
109+
};
110+
const parsed = SubmitReviewRequestSchema.parse(request);
111+
expect(parsed.rating).toBe(5);
112+
});
113+
114+
it('should accept review with title and body', () => {
115+
const request = {
116+
listingId: 'listing-001',
117+
rating: 4,
118+
title: 'Great app',
119+
body: 'Works perfectly for our needs.',
120+
};
121+
const parsed = SubmitReviewRequestSchema.parse(request);
122+
expect(parsed.title).toBe('Great app');
123+
});
124+
});
125+
126+
describe('ListReviewsRequestSchema', () => {
127+
it('should accept minimal request', () => {
128+
const request = { listingId: 'listing-001' };
129+
const parsed = ListReviewsRequestSchema.parse(request);
130+
expect(parsed.sortBy).toBe('newest');
131+
expect(parsed.page).toBe(1);
132+
expect(parsed.pageSize).toBe(10);
133+
});
134+
135+
it('should accept filtered request', () => {
136+
const request = {
137+
listingId: 'listing-001',
138+
sortBy: 'most-helpful' as const,
139+
rating: 5,
140+
page: 2,
141+
pageSize: 25,
142+
};
143+
const parsed = ListReviewsRequestSchema.parse(request);
144+
expect(parsed.rating).toBe(5);
145+
});
146+
147+
it('should reject invalid page size', () => {
148+
expect(() => ListReviewsRequestSchema.parse({
149+
listingId: 'listing-001',
150+
pageSize: 0,
151+
})).toThrow();
152+
expect(() => ListReviewsRequestSchema.parse({
153+
listingId: 'listing-001',
154+
pageSize: 51,
155+
})).toThrow();
156+
});
157+
});
158+
159+
describe('ListReviewsResponseSchema', () => {
160+
it('should accept empty response', () => {
161+
const response = {
162+
items: [],
163+
total: 0,
164+
page: 1,
165+
pageSize: 10,
166+
};
167+
const parsed = ListReviewsResponseSchema.parse(response);
168+
expect(parsed.items).toHaveLength(0);
169+
});
170+
171+
it('should accept response with rating summary', () => {
172+
const response = {
173+
items: [{
174+
id: 'rev-001',
175+
listingId: 'listing-001',
176+
userId: 'user-001',
177+
rating: 5,
178+
submittedAt: '2025-06-01T10:00:00Z',
179+
}],
180+
total: 1,
181+
page: 1,
182+
pageSize: 10,
183+
ratingSummary: {
184+
averageRating: 4.5,
185+
totalRatings: 120,
186+
distribution: { 1: 2, 2: 5, 3: 15, 4: 48, 5: 50 },
187+
},
188+
};
189+
const parsed = ListReviewsResponseSchema.parse(response);
190+
expect(parsed.ratingSummary?.averageRating).toBe(4.5);
191+
expect(parsed.ratingSummary?.distribution[5]).toBe(50);
192+
});
193+
});
194+
195+
describe('RecommendationReasonSchema', () => {
196+
it('should accept all recommendation reasons', () => {
197+
const reasons = [
198+
'popular-in-category', 'similar-users', 'complements-installed',
199+
'trending', 'new-release', 'editor-pick',
200+
];
201+
reasons.forEach(reason => {
202+
expect(() => RecommendationReasonSchema.parse(reason)).not.toThrow();
203+
});
204+
});
205+
});
206+
207+
describe('RecommendedAppSchema', () => {
208+
it('should accept recommended app', () => {
209+
const app = {
210+
listingId: 'listing-001',
211+
name: 'Acme CRM',
212+
tagline: 'Complete CRM for ObjectStack',
213+
iconUrl: 'https://acme.com/icon.png',
214+
category: 'crm' as const,
215+
pricing: 'freemium' as const,
216+
averageRating: 4.5,
217+
activeInstalls: 3200,
218+
reason: 'popular-in-category' as const,
219+
};
220+
const parsed = RecommendedAppSchema.parse(app);
221+
expect(parsed.reason).toBe('popular-in-category');
222+
});
223+
});
224+
225+
describe('AppDiscoveryRequestSchema', () => {
226+
it('should accept empty discovery request', () => {
227+
const parsed = AppDiscoveryRequestSchema.parse({});
228+
expect(parsed.limit).toBe(10);
229+
});
230+
231+
it('should accept personalized discovery request', () => {
232+
const request = {
233+
tenantId: 'tenant-001',
234+
categories: ['crm', 'analytics'],
235+
platformVersion: '1.5.0',
236+
limit: 20,
237+
};
238+
const parsed = AppDiscoveryRequestSchema.parse(request);
239+
expect(parsed.categories).toHaveLength(2);
240+
});
241+
});
242+
243+
describe('AppDiscoveryResponseSchema', () => {
244+
it('should accept full discovery response', () => {
245+
const app = {
246+
listingId: 'listing-001',
247+
name: 'Acme CRM',
248+
category: 'crm' as const,
249+
pricing: 'free' as const,
250+
reason: 'editor-pick' as const,
251+
};
252+
const response = {
253+
featured: [app],
254+
recommended: [{ ...app, reason: 'popular-in-category' as const }],
255+
trending: [{ ...app, reason: 'trending' as const }],
256+
newArrivals: [{ ...app, reason: 'new-release' as const }],
257+
collections: [{
258+
id: 'col-001',
259+
name: 'Best for Small Business',
260+
apps: [app],
261+
}],
262+
};
263+
const parsed = AppDiscoveryResponseSchema.parse(response);
264+
expect(parsed.featured).toHaveLength(1);
265+
expect(parsed.collections).toHaveLength(1);
266+
});
267+
268+
it('should accept minimal discovery response', () => {
269+
const response = {};
270+
const parsed = AppDiscoveryResponseSchema.parse(response);
271+
expect(parsed.featured).toBeUndefined();
272+
});
273+
});
274+
275+
describe('SubscriptionStatusSchema', () => {
276+
it('should accept all subscription statuses', () => {
277+
const statuses = ['active', 'trialing', 'past-due', 'cancelled', 'expired'];
278+
statuses.forEach(status => {
279+
expect(() => SubscriptionStatusSchema.parse(status)).not.toThrow();
280+
});
281+
});
282+
});
283+
284+
describe('AppSubscriptionSchema', () => {
285+
it('should accept active subscription', () => {
286+
const subscription = {
287+
id: 'sub-001',
288+
listingId: 'listing-001',
289+
tenantId: 'tenant-001',
290+
status: 'active' as const,
291+
plan: 'Professional',
292+
billingCycle: 'annual' as const,
293+
priceInCents: 11988,
294+
currentPeriodStart: '2025-01-01T00:00:00Z',
295+
currentPeriodEnd: '2026-01-01T00:00:00Z',
296+
autoRenew: true,
297+
createdAt: '2025-01-01T00:00:00Z',
298+
};
299+
const parsed = AppSubscriptionSchema.parse(subscription);
300+
expect(parsed.status).toBe('active');
301+
expect(parsed.autoRenew).toBe(true);
302+
});
303+
304+
it('should accept trial subscription', () => {
305+
const subscription = {
306+
id: 'sub-002',
307+
listingId: 'listing-001',
308+
tenantId: 'tenant-001',
309+
status: 'trialing' as const,
310+
trialEndDate: '2025-07-01T00:00:00Z',
311+
createdAt: '2025-06-01T00:00:00Z',
312+
};
313+
const parsed = AppSubscriptionSchema.parse(subscription);
314+
expect(parsed.status).toBe('trialing');
315+
expect(parsed.trialEndDate).toBe('2025-07-01T00:00:00Z');
316+
});
317+
});
318+
319+
describe('InstalledAppSummarySchema', () => {
320+
it('should accept installed app with update available', () => {
321+
const app = {
322+
listingId: 'listing-001',
323+
packageId: 'com.acme.crm',
324+
name: 'Acme CRM',
325+
iconUrl: 'https://acme.com/icon.png',
326+
installedVersion: '2.0.0',
327+
latestVersion: '2.1.0',
328+
updateAvailable: true,
329+
enabled: true,
330+
subscriptionStatus: 'active' as const,
331+
installedAt: '2025-03-01T00:00:00Z',
332+
};
333+
const parsed = InstalledAppSummarySchema.parse(app);
334+
expect(parsed.updateAvailable).toBe(true);
335+
expect(parsed.subscriptionStatus).toBe('active');
336+
});
337+
338+
it('should accept minimal installed app', () => {
339+
const app = {
340+
listingId: 'listing-002',
341+
packageId: 'com.acme.utils',
342+
name: 'Acme Utils',
343+
installedVersion: '1.0.0',
344+
installedAt: '2025-06-01T00:00:00Z',
345+
};
346+
const parsed = InstalledAppSummarySchema.parse(app);
347+
expect(parsed.updateAvailable).toBe(false);
348+
expect(parsed.enabled).toBe(true);
349+
});
350+
});
351+
352+
describe('ListInstalledAppsRequestSchema', () => {
353+
it('should accept empty request', () => {
354+
const parsed = ListInstalledAppsRequestSchema.parse({});
355+
expect(parsed.sortBy).toBe('name');
356+
expect(parsed.page).toBe(1);
357+
expect(parsed.pageSize).toBe(20);
358+
});
359+
360+
it('should accept filtered request', () => {
361+
const request = {
362+
tenantId: 'tenant-001',
363+
enabled: true,
364+
updateAvailable: true,
365+
sortBy: 'installed-date' as const,
366+
};
367+
const parsed = ListInstalledAppsRequestSchema.parse(request);
368+
expect(parsed.updateAvailable).toBe(true);
369+
});
370+
});
371+
372+
describe('ListInstalledAppsResponseSchema', () => {
373+
it('should accept response with installed apps', () => {
374+
const response = {
375+
items: [{
376+
listingId: 'listing-001',
377+
packageId: 'com.acme.crm',
378+
name: 'Acme CRM',
379+
installedVersion: '2.0.0',
380+
installedAt: '2025-03-01T00:00:00Z',
381+
}],
382+
total: 1,
383+
page: 1,
384+
pageSize: 20,
385+
};
386+
const parsed = ListInstalledAppsResponseSchema.parse(response);
387+
expect(parsed.items).toHaveLength(1);
388+
expect(parsed.total).toBe(1);
389+
});
390+
});

0 commit comments

Comments
 (0)