@ucptools/validator 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.
- package/CLAUDE.md +109 -0
- package/CONTRIBUTING.md +113 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/api/analyze-feed.js +140 -0
- package/api/badge.js +185 -0
- package/api/benchmark.js +177 -0
- package/api/directory-stats.ts +29 -0
- package/api/directory.ts +73 -0
- package/api/generate-compliance.js +143 -0
- package/api/generate-schema.js +457 -0
- package/api/generate.js +132 -0
- package/api/security-scan.js +133 -0
- package/api/simulate.js +187 -0
- package/api/tsconfig.json +10 -0
- package/api/validate.js +1351 -0
- package/apify-actor/.actor/actor.json +68 -0
- package/apify-actor/.actor/input_schema.json +32 -0
- package/apify-actor/APIFY-STORE-LISTING.md +412 -0
- package/apify-actor/Dockerfile +8 -0
- package/apify-actor/README.md +166 -0
- package/apify-actor/main.ts +111 -0
- package/apify-actor/package.json +17 -0
- package/apify-actor/src/main.js +199 -0
- package/docs/BRAND-IDENTITY.md +238 -0
- package/docs/BRAND-STYLE-GUIDE.md +356 -0
- package/drizzle/0000_black_king_cobra.sql +39 -0
- package/drizzle/meta/0000_snapshot.json +309 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/examples/full-profile.json +70 -0
- package/examples/minimal-profile.json +23 -0
- package/package.json +69 -0
- package/public/.well-known/ucp +25 -0
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/brand.css +321 -0
- package/public/directory.html +701 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/guides/bigcommerce.html +743 -0
- package/public/guides/fastucp.html +838 -0
- package/public/guides/magento.html +779 -0
- package/public/guides/shopify.html +726 -0
- package/public/guides/squarespace.html +749 -0
- package/public/guides/wix.html +747 -0
- package/public/guides/woocommerce.html +733 -0
- package/public/index.html +3835 -0
- package/public/learn.html +396 -0
- package/public/logo.jpeg +0 -0
- package/public/og-image-icon.png +0 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +31 -0
- package/public/sitemap.xml +69 -0
- package/public/social/linkedin-banner-1128x191.png +0 -0
- package/public/social/temp.PNG +0 -0
- package/public/social/x-header-1500x500.png +0 -0
- package/public/verify.html +410 -0
- package/scripts/generate-favicons.js +44 -0
- package/scripts/generate-ico.js +23 -0
- package/scripts/generate-og-image.js +45 -0
- package/scripts/reset-db.ts +77 -0
- package/scripts/seed-db.ts +71 -0
- package/scripts/setup-benchmark-db.js +70 -0
- package/src/api/server.ts +266 -0
- package/src/cli/index.ts +302 -0
- package/src/compliance/compliance-generator.ts +452 -0
- package/src/compliance/index.ts +28 -0
- package/src/compliance/templates.ts +338 -0
- package/src/compliance/types.ts +170 -0
- package/src/db/index.ts +28 -0
- package/src/db/schema.ts +84 -0
- package/src/feed-analyzer/feed-analyzer.ts +726 -0
- package/src/feed-analyzer/index.ts +34 -0
- package/src/feed-analyzer/types.ts +354 -0
- package/src/generator/index.ts +7 -0
- package/src/generator/key-generator.ts +124 -0
- package/src/generator/profile-builder.ts +402 -0
- package/src/hosting/artifacts-generator.ts +679 -0
- package/src/hosting/index.ts +6 -0
- package/src/index.ts +105 -0
- package/src/security/index.ts +15 -0
- package/src/security/security-scanner.ts +604 -0
- package/src/security/types.ts +55 -0
- package/src/services/directory.ts +434 -0
- package/src/simulator/agent-simulator.ts +941 -0
- package/src/simulator/index.ts +7 -0
- package/src/simulator/types.ts +170 -0
- package/src/types/generator.ts +140 -0
- package/src/types/index.ts +7 -0
- package/src/types/ucp-profile.ts +140 -0
- package/src/types/validation.ts +89 -0
- package/src/validator/index.ts +194 -0
- package/src/validator/network-validator.ts +417 -0
- package/src/validator/rules-validator.ts +297 -0
- package/src/validator/sdk-validator.ts +330 -0
- package/src/validator/structural-validator.ts +476 -0
- package/tests/fixtures/non-compliant-profile.json +25 -0
- package/tests/fixtures/official-sample-profile.json +75 -0
- package/tests/integration/benchmark.test.ts +207 -0
- package/tests/integration/database.test.ts +163 -0
- package/tests/integration/directory-api.test.ts +268 -0
- package/tests/integration/simulate-api.test.ts +230 -0
- package/tests/integration/validate-api.test.ts +269 -0
- package/tests/setup.ts +15 -0
- package/tests/unit/agent-simulator.test.ts +575 -0
- package/tests/unit/compliance-generator.test.ts +374 -0
- package/tests/unit/directory-service.test.ts +272 -0
- package/tests/unit/feed-analyzer.test.ts +517 -0
- package/tests/unit/lint-suggestions.test.ts +423 -0
- package/tests/unit/official-samples.test.ts +211 -0
- package/tests/unit/pdf-report.test.ts +390 -0
- package/tests/unit/sdk-validator.test.ts +531 -0
- package/tests/unit/security-scanner.test.ts +410 -0
- package/tests/unit/validation.test.ts +390 -0
- package/tsconfig.json +20 -0
- package/vercel.json +34 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Lint Suggestions Feature (Issue #9)
|
|
3
|
+
* Tests the generateLintSuggestions function logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
|
|
8
|
+
// Recreate the lint suggestion generation logic for testing
|
|
9
|
+
const suggestionMap: Record<string, { severity: string; title: string; impact: string }> = {
|
|
10
|
+
// Critical issues
|
|
11
|
+
'UCP_FETCH_FAILED': {
|
|
12
|
+
severity: 'critical',
|
|
13
|
+
title: 'Create a UCP Profile',
|
|
14
|
+
impact: 'AI shopping agents cannot discover your store without a UCP profile',
|
|
15
|
+
},
|
|
16
|
+
'UCP_MISSING_ROOT': {
|
|
17
|
+
severity: 'critical',
|
|
18
|
+
title: 'Add "ucp" Root Object',
|
|
19
|
+
impact: 'Profile cannot be parsed without the required root structure',
|
|
20
|
+
},
|
|
21
|
+
'UCP_MISSING_VERSION': {
|
|
22
|
+
severity: 'critical',
|
|
23
|
+
title: 'Add UCP Version',
|
|
24
|
+
impact: 'Agents cannot determine compatibility without a version',
|
|
25
|
+
},
|
|
26
|
+
'UCP_INVALID_VERSION': {
|
|
27
|
+
severity: 'critical',
|
|
28
|
+
title: 'Fix Version Format',
|
|
29
|
+
impact: 'Invalid version format will cause parsing errors',
|
|
30
|
+
},
|
|
31
|
+
'UCP_MISSING_SERVICES': {
|
|
32
|
+
severity: 'critical',
|
|
33
|
+
title: 'Add Services Configuration',
|
|
34
|
+
impact: 'No services means AI agents have nothing to interact with',
|
|
35
|
+
},
|
|
36
|
+
'UCP_MISSING_CAPABILITIES': {
|
|
37
|
+
severity: 'critical',
|
|
38
|
+
title: 'Add Capabilities Array',
|
|
39
|
+
impact: 'Without capabilities, agents cannot perform any actions',
|
|
40
|
+
},
|
|
41
|
+
'UCP_MISSING_KEYS': {
|
|
42
|
+
severity: 'critical',
|
|
43
|
+
title: 'Add Signing Keys for Order Capability',
|
|
44
|
+
impact: 'Order transactions cannot be verified without signing keys',
|
|
45
|
+
},
|
|
46
|
+
'UCP_ENDPOINT_NOT_HTTPS': {
|
|
47
|
+
severity: 'critical',
|
|
48
|
+
title: 'Use HTTPS for Endpoints',
|
|
49
|
+
impact: 'HTTP endpoints are insecure and rejected by AI agents',
|
|
50
|
+
},
|
|
51
|
+
'UCP_NS_MISMATCH': {
|
|
52
|
+
severity: 'critical',
|
|
53
|
+
title: 'Fix Namespace Origin',
|
|
54
|
+
impact: 'Spec/schema URLs must match the capability namespace',
|
|
55
|
+
},
|
|
56
|
+
'SCHEMA_NO_RETURN_POLICY': {
|
|
57
|
+
severity: 'critical',
|
|
58
|
+
title: 'Add MerchantReturnPolicy Schema',
|
|
59
|
+
impact: 'Required for AI commerce eligibility (Jan 2026 deadline)',
|
|
60
|
+
},
|
|
61
|
+
'SCHEMA_NO_SHIPPING': {
|
|
62
|
+
severity: 'critical',
|
|
63
|
+
title: 'Add OfferShippingDetails Schema',
|
|
64
|
+
impact: 'Required for AI commerce eligibility (Jan 2026 deadline)',
|
|
65
|
+
},
|
|
66
|
+
// Warnings
|
|
67
|
+
'UCP_NO_TRANSPORT': {
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
title: 'Add Transport Binding',
|
|
70
|
+
impact: 'Service has no way for agents to communicate with it',
|
|
71
|
+
},
|
|
72
|
+
'UCP_TRAILING_SLASH': {
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
title: 'Remove Trailing Slash from Endpoint',
|
|
75
|
+
impact: 'May cause URL concatenation issues',
|
|
76
|
+
},
|
|
77
|
+
'UCP_ORPHAN_EXT': {
|
|
78
|
+
severity: 'warning',
|
|
79
|
+
title: 'Fix Orphaned Extension',
|
|
80
|
+
impact: 'Capability extends a parent that does not exist',
|
|
81
|
+
},
|
|
82
|
+
'SCHEMA_NO_ORG': {
|
|
83
|
+
severity: 'warning',
|
|
84
|
+
title: 'Add Organization Schema',
|
|
85
|
+
impact: 'AI agents may not recognize your business identity',
|
|
86
|
+
},
|
|
87
|
+
'PRODUCT_NO_DESCRIPTION': {
|
|
88
|
+
severity: 'warning',
|
|
89
|
+
title: 'Add Product Description',
|
|
90
|
+
impact: 'AI agents may hallucinate product details',
|
|
91
|
+
},
|
|
92
|
+
'PRODUCT_NO_IMAGE': {
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
title: 'Add Product Image',
|
|
95
|
+
impact: 'Visual context helps AI product matching',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
interface Issue {
|
|
100
|
+
code: string;
|
|
101
|
+
message: string;
|
|
102
|
+
severity: 'error' | 'warn';
|
|
103
|
+
category?: string;
|
|
104
|
+
path?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface LintSuggestion {
|
|
108
|
+
severity: string;
|
|
109
|
+
title: string;
|
|
110
|
+
code: string;
|
|
111
|
+
impact: string;
|
|
112
|
+
fix?: string;
|
|
113
|
+
codeSnippet?: string;
|
|
114
|
+
docLink?: string;
|
|
115
|
+
path?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generateLintSuggestions(
|
|
119
|
+
ucpIssues: Issue[],
|
|
120
|
+
schemaIssues: Issue[],
|
|
121
|
+
hasUcp: boolean,
|
|
122
|
+
profile: any,
|
|
123
|
+
schemaStats: { products: number }
|
|
124
|
+
): LintSuggestion[] {
|
|
125
|
+
const suggestions: LintSuggestion[] = [];
|
|
126
|
+
const allIssues = [...ucpIssues, ...schemaIssues];
|
|
127
|
+
const processedCodes = new Set<string>();
|
|
128
|
+
|
|
129
|
+
allIssues.forEach(issue => {
|
|
130
|
+
const template = suggestionMap[issue.code];
|
|
131
|
+
if (template && !processedCodes.has(issue.code)) {
|
|
132
|
+
processedCodes.add(issue.code);
|
|
133
|
+
suggestions.push({
|
|
134
|
+
severity: template.severity,
|
|
135
|
+
title: template.title,
|
|
136
|
+
code: issue.code,
|
|
137
|
+
impact: template.impact,
|
|
138
|
+
path: issue.path,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Add contextual suggestions
|
|
144
|
+
if (hasUcp) {
|
|
145
|
+
const capabilities = profile?.ucp?.capabilities?.map((c: any) => c.name) || [];
|
|
146
|
+
if (!capabilities.includes('dev.ucp.shopping.checkout') && capabilities.length > 0) {
|
|
147
|
+
suggestions.push({
|
|
148
|
+
severity: 'info',
|
|
149
|
+
title: 'Consider Adding Checkout Capability',
|
|
150
|
+
code: 'SUGGESTION_ADD_CHECKOUT',
|
|
151
|
+
impact: 'Enables AI agents to complete purchases on your site',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (schemaStats.products === 0 && hasUcp) {
|
|
157
|
+
suggestions.push({
|
|
158
|
+
severity: 'info',
|
|
159
|
+
title: 'Add Product Schema for Better AI Discovery',
|
|
160
|
+
code: 'SUGGESTION_ADD_PRODUCTS',
|
|
161
|
+
impact: 'Product schemas help AI agents understand your catalog',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sort by severity
|
|
166
|
+
const severityOrder: Record<string, number> = { critical: 0, warning: 1, info: 2 };
|
|
167
|
+
suggestions.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
|
|
168
|
+
|
|
169
|
+
return suggestions;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('Lint Suggestions Generation (Issue #9)', () => {
|
|
173
|
+
describe('Critical Issues', () => {
|
|
174
|
+
it('should generate suggestion for missing UCP profile', () => {
|
|
175
|
+
const ucpIssues: Issue[] = [
|
|
176
|
+
{ code: 'UCP_FETCH_FAILED', message: 'No UCP profile found', severity: 'error' }
|
|
177
|
+
];
|
|
178
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], false, null, { products: 0 });
|
|
179
|
+
|
|
180
|
+
expect(suggestions.length).toBeGreaterThanOrEqual(1);
|
|
181
|
+
const fetchSuggestion = suggestions.find(s => s.code === 'UCP_FETCH_FAILED');
|
|
182
|
+
expect(fetchSuggestion).toBeDefined();
|
|
183
|
+
expect(fetchSuggestion?.severity).toBe('critical');
|
|
184
|
+
expect(fetchSuggestion?.title).toBe('Create a UCP Profile');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should generate suggestion for missing version', () => {
|
|
188
|
+
const ucpIssues: Issue[] = [
|
|
189
|
+
{ code: 'UCP_MISSING_VERSION', message: 'Missing version field', severity: 'error' }
|
|
190
|
+
];
|
|
191
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
192
|
+
|
|
193
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_MISSING_VERSION');
|
|
194
|
+
expect(suggestion).toBeDefined();
|
|
195
|
+
expect(suggestion?.severity).toBe('critical');
|
|
196
|
+
expect(suggestion?.title).toBe('Add UCP Version');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should generate suggestion for invalid version format', () => {
|
|
200
|
+
const ucpIssues: Issue[] = [
|
|
201
|
+
{ code: 'UCP_INVALID_VERSION', message: 'Invalid version: v1.0', severity: 'error' }
|
|
202
|
+
];
|
|
203
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
204
|
+
|
|
205
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_INVALID_VERSION');
|
|
206
|
+
expect(suggestion).toBeDefined();
|
|
207
|
+
expect(suggestion?.severity).toBe('critical');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should generate suggestion for missing services', () => {
|
|
211
|
+
const ucpIssues: Issue[] = [
|
|
212
|
+
{ code: 'UCP_MISSING_SERVICES', message: 'Missing services', severity: 'error' }
|
|
213
|
+
];
|
|
214
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
215
|
+
|
|
216
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_MISSING_SERVICES');
|
|
217
|
+
expect(suggestion).toBeDefined();
|
|
218
|
+
expect(suggestion?.severity).toBe('critical');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should generate suggestion for missing capabilities', () => {
|
|
222
|
+
const ucpIssues: Issue[] = [
|
|
223
|
+
{ code: 'UCP_MISSING_CAPABILITIES', message: 'Missing capabilities array', severity: 'error' }
|
|
224
|
+
];
|
|
225
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
226
|
+
|
|
227
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_MISSING_CAPABILITIES');
|
|
228
|
+
expect(suggestion).toBeDefined();
|
|
229
|
+
expect(suggestion?.severity).toBe('critical');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should generate suggestion for non-HTTPS endpoint', () => {
|
|
233
|
+
const ucpIssues: Issue[] = [
|
|
234
|
+
{ code: 'UCP_ENDPOINT_NOT_HTTPS', message: 'Endpoint must use HTTPS', severity: 'error' }
|
|
235
|
+
];
|
|
236
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
237
|
+
|
|
238
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_ENDPOINT_NOT_HTTPS');
|
|
239
|
+
expect(suggestion).toBeDefined();
|
|
240
|
+
expect(suggestion?.severity).toBe('critical');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should generate suggestion for missing signing keys', () => {
|
|
244
|
+
const ucpIssues: Issue[] = [
|
|
245
|
+
{ code: 'UCP_MISSING_KEYS', message: 'Order requires signing_keys', severity: 'error' }
|
|
246
|
+
];
|
|
247
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
248
|
+
|
|
249
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_MISSING_KEYS');
|
|
250
|
+
expect(suggestion).toBeDefined();
|
|
251
|
+
expect(suggestion?.severity).toBe('critical');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should generate suggestion for missing return policy', () => {
|
|
255
|
+
const schemaIssues: Issue[] = [
|
|
256
|
+
{ code: 'SCHEMA_NO_RETURN_POLICY', message: 'No return policy found', severity: 'error', category: 'schema' }
|
|
257
|
+
];
|
|
258
|
+
const suggestions = generateLintSuggestions([], schemaIssues, true, {}, { products: 5 });
|
|
259
|
+
|
|
260
|
+
const suggestion = suggestions.find(s => s.code === 'SCHEMA_NO_RETURN_POLICY');
|
|
261
|
+
expect(suggestion).toBeDefined();
|
|
262
|
+
expect(suggestion?.severity).toBe('critical');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should generate suggestion for missing shipping details', () => {
|
|
266
|
+
const schemaIssues: Issue[] = [
|
|
267
|
+
{ code: 'SCHEMA_NO_SHIPPING', message: 'No shipping info', severity: 'error', category: 'schema' }
|
|
268
|
+
];
|
|
269
|
+
const suggestions = generateLintSuggestions([], schemaIssues, true, {}, { products: 5 });
|
|
270
|
+
|
|
271
|
+
const suggestion = suggestions.find(s => s.code === 'SCHEMA_NO_SHIPPING');
|
|
272
|
+
expect(suggestion).toBeDefined();
|
|
273
|
+
expect(suggestion?.severity).toBe('critical');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('Warning Issues', () => {
|
|
278
|
+
it('should generate suggestion for missing transport', () => {
|
|
279
|
+
const ucpIssues: Issue[] = [
|
|
280
|
+
{ code: 'UCP_NO_TRANSPORT', message: 'Service has no transport bindings', severity: 'warn' }
|
|
281
|
+
];
|
|
282
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
283
|
+
|
|
284
|
+
const suggestion = suggestions.find(s => s.code === 'UCP_NO_TRANSPORT');
|
|
285
|
+
expect(suggestion).toBeDefined();
|
|
286
|
+
expect(suggestion?.severity).toBe('warning');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should generate suggestion for missing organization schema', () => {
|
|
290
|
+
const schemaIssues: Issue[] = [
|
|
291
|
+
{ code: 'SCHEMA_NO_ORG', message: 'No organization schema', severity: 'warn', category: 'schema' }
|
|
292
|
+
];
|
|
293
|
+
const suggestions = generateLintSuggestions([], schemaIssues, true, {}, { products: 5 });
|
|
294
|
+
|
|
295
|
+
const suggestion = suggestions.find(s => s.code === 'SCHEMA_NO_ORG');
|
|
296
|
+
expect(suggestion).toBeDefined();
|
|
297
|
+
expect(suggestion?.severity).toBe('warning');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should generate suggestion for missing product description', () => {
|
|
301
|
+
const schemaIssues: Issue[] = [
|
|
302
|
+
{ code: 'PRODUCT_NO_DESCRIPTION', message: 'Product missing description', severity: 'warn', category: 'product_quality' }
|
|
303
|
+
];
|
|
304
|
+
const suggestions = generateLintSuggestions([], schemaIssues, true, {}, { products: 5 });
|
|
305
|
+
|
|
306
|
+
const suggestion = suggestions.find(s => s.code === 'PRODUCT_NO_DESCRIPTION');
|
|
307
|
+
expect(suggestion).toBeDefined();
|
|
308
|
+
expect(suggestion?.severity).toBe('warning');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('Contextual Suggestions', () => {
|
|
313
|
+
it('should suggest checkout capability when profile has other capabilities', () => {
|
|
314
|
+
const profile = {
|
|
315
|
+
ucp: {
|
|
316
|
+
capabilities: [
|
|
317
|
+
{ name: 'dev.ucp.shopping.catalog', version: '1.0' }
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const suggestions = generateLintSuggestions([], [], true, profile, { products: 5 });
|
|
322
|
+
|
|
323
|
+
const suggestion = suggestions.find(s => s.code === 'SUGGESTION_ADD_CHECKOUT');
|
|
324
|
+
expect(suggestion).toBeDefined();
|
|
325
|
+
expect(suggestion?.severity).toBe('info');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should not suggest checkout when already present', () => {
|
|
329
|
+
const profile = {
|
|
330
|
+
ucp: {
|
|
331
|
+
capabilities: [
|
|
332
|
+
{ name: 'dev.ucp.shopping.checkout', version: '1.0' }
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const suggestions = generateLintSuggestions([], [], true, profile, { products: 5 });
|
|
337
|
+
|
|
338
|
+
const suggestion = suggestions.find(s => s.code === 'SUGGESTION_ADD_CHECKOUT');
|
|
339
|
+
expect(suggestion).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should suggest product schema when UCP exists but no products', () => {
|
|
343
|
+
const profile = { ucp: { capabilities: [] } };
|
|
344
|
+
const suggestions = generateLintSuggestions([], [], true, profile, { products: 0 });
|
|
345
|
+
|
|
346
|
+
const suggestion = suggestions.find(s => s.code === 'SUGGESTION_ADD_PRODUCTS');
|
|
347
|
+
expect(suggestion).toBeDefined();
|
|
348
|
+
expect(suggestion?.severity).toBe('info');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should not suggest product schema when products exist', () => {
|
|
352
|
+
const profile = { ucp: { capabilities: [] } };
|
|
353
|
+
const suggestions = generateLintSuggestions([], [], true, profile, { products: 5 });
|
|
354
|
+
|
|
355
|
+
const suggestion = suggestions.find(s => s.code === 'SUGGESTION_ADD_PRODUCTS');
|
|
356
|
+
expect(suggestion).toBeUndefined();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('Deduplication', () => {
|
|
361
|
+
it('should not duplicate suggestions for same issue code', () => {
|
|
362
|
+
const ucpIssues: Issue[] = [
|
|
363
|
+
{ code: 'UCP_MISSING_VERSION', message: 'Missing version 1', severity: 'error' },
|
|
364
|
+
{ code: 'UCP_MISSING_VERSION', message: 'Missing version 2', severity: 'error' }
|
|
365
|
+
];
|
|
366
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
367
|
+
|
|
368
|
+
const versionSuggestions = suggestions.filter(s => s.code === 'UCP_MISSING_VERSION');
|
|
369
|
+
expect(versionSuggestions).toHaveLength(1);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('Sorting by Severity', () => {
|
|
374
|
+
it('should sort critical issues before warnings', () => {
|
|
375
|
+
const ucpIssues: Issue[] = [
|
|
376
|
+
{ code: 'UCP_NO_TRANSPORT', message: 'No transport', severity: 'warn' },
|
|
377
|
+
{ code: 'UCP_MISSING_VERSION', message: 'Missing version', severity: 'error' }
|
|
378
|
+
];
|
|
379
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, {}, { products: 0 });
|
|
380
|
+
|
|
381
|
+
expect(suggestions[0].severity).toBe('critical');
|
|
382
|
+
expect(suggestions[1].severity).toBe('warning');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should sort warnings before info suggestions', () => {
|
|
386
|
+
const ucpIssues: Issue[] = [
|
|
387
|
+
{ code: 'UCP_NO_TRANSPORT', message: 'No transport', severity: 'warn' }
|
|
388
|
+
];
|
|
389
|
+
const profile = { ucp: { capabilities: [{ name: 'dev.ucp.shopping.catalog' }] } };
|
|
390
|
+
const suggestions = generateLintSuggestions(ucpIssues, [], true, profile, { products: 0 });
|
|
391
|
+
|
|
392
|
+
const warningIndex = suggestions.findIndex(s => s.severity === 'warning');
|
|
393
|
+
const infoIndex = suggestions.findIndex(s => s.severity === 'info');
|
|
394
|
+
|
|
395
|
+
if (warningIndex !== -1 && infoIndex !== -1) {
|
|
396
|
+
expect(warningIndex).toBeLessThan(infoIndex);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('Combined Issues', () => {
|
|
402
|
+
it('should handle mix of UCP and Schema issues', () => {
|
|
403
|
+
const ucpIssues: Issue[] = [
|
|
404
|
+
{ code: 'UCP_MISSING_VERSION', message: 'Missing version', severity: 'error' }
|
|
405
|
+
];
|
|
406
|
+
const schemaIssues: Issue[] = [
|
|
407
|
+
{ code: 'SCHEMA_NO_RETURN_POLICY', message: 'No return policy', severity: 'error', category: 'schema' }
|
|
408
|
+
];
|
|
409
|
+
const suggestions = generateLintSuggestions(ucpIssues, schemaIssues, true, {}, { products: 5 });
|
|
410
|
+
|
|
411
|
+
expect(suggestions.some(s => s.code === 'UCP_MISSING_VERSION')).toBe(true);
|
|
412
|
+
expect(suggestions.some(s => s.code === 'SCHEMA_NO_RETURN_POLICY')).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should return empty array when no issues', () => {
|
|
416
|
+
const profile = { ucp: { capabilities: [{ name: 'dev.ucp.shopping.checkout' }] } };
|
|
417
|
+
const suggestions = generateLintSuggestions([], [], true, profile, { products: 5 });
|
|
418
|
+
|
|
419
|
+
// Only contextual suggestions, no issue-based ones
|
|
420
|
+
expect(suggestions.every(s => s.code.startsWith('SUGGESTION_'))).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for UCP Profile Validation against Official Samples
|
|
3
|
+
* Based on: https://github.com/Universal-Commerce-Protocol/samples
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
// Import validators
|
|
11
|
+
import { validateStructure } from '../../src/validator/structural-validator.js';
|
|
12
|
+
import { validateRules } from '../../src/validator/rules-validator.js';
|
|
13
|
+
|
|
14
|
+
// Load test fixtures
|
|
15
|
+
const fixturesDir = join(__dirname, '../fixtures');
|
|
16
|
+
const officialSampleProfile = JSON.parse(
|
|
17
|
+
readFileSync(join(fixturesDir, 'official-sample-profile.json'), 'utf-8')
|
|
18
|
+
);
|
|
19
|
+
const nonCompliantProfile = JSON.parse(
|
|
20
|
+
readFileSync(join(fixturesDir, 'non-compliant-profile.json'), 'utf-8')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
describe('Official UCP Sample Profile Validation', () => {
|
|
24
|
+
describe('Structural Validation', () => {
|
|
25
|
+
it('should pass structural validation for official sample', () => {
|
|
26
|
+
const issues = validateStructure(officialSampleProfile);
|
|
27
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
28
|
+
|
|
29
|
+
expect(errors).toHaveLength(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should identify structural issues in non-compliant profile', () => {
|
|
33
|
+
const issues = validateStructure(nonCompliantProfile);
|
|
34
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
35
|
+
|
|
36
|
+
// Non-compliant profile should have errors
|
|
37
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Rules Validation', () => {
|
|
42
|
+
it('should pass rules validation for official sample', () => {
|
|
43
|
+
const issues = validateRules(officialSampleProfile);
|
|
44
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
45
|
+
|
|
46
|
+
expect(errors).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Version Format', () => {
|
|
51
|
+
it('should accept YYYY-MM-DD version format', () => {
|
|
52
|
+
const issues = validateStructure(officialSampleProfile);
|
|
53
|
+
const versionIssues = issues.filter(i => i.code === 'UCP_INVALID_VERSION' || i.code === 'UCP_INVALID_VERSION_FORMAT');
|
|
54
|
+
|
|
55
|
+
expect(versionIssues).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should reject semver-style version format', () => {
|
|
59
|
+
const issues = validateStructure(nonCompliantProfile);
|
|
60
|
+
const versionIssues = issues.filter(i => i.code === 'UCP_INVALID_VERSION' || i.code === 'UCP_INVALID_VERSION_FORMAT');
|
|
61
|
+
|
|
62
|
+
expect(versionIssues.length).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Services Structure', () => {
|
|
67
|
+
it('should accept properly structured services', () => {
|
|
68
|
+
const issues = validateStructure(officialSampleProfile);
|
|
69
|
+
const serviceIssues = issues.filter(i =>
|
|
70
|
+
i.code === 'UCP_INVALID_SERVICE' || i.code === 'UCP_NO_TRANSPORT'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(serviceIssues).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should reject string-only service definitions', () => {
|
|
77
|
+
const issues = validateStructure(nonCompliantProfile);
|
|
78
|
+
const serviceIssues = issues.filter(i =>
|
|
79
|
+
i.code === 'UCP_INVALID_SERVICE' || i.code === 'UCP_NO_TRANSPORT'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(serviceIssues.length).toBeGreaterThan(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Capabilities Structure', () => {
|
|
87
|
+
it('should accept properly structured capabilities', () => {
|
|
88
|
+
const issues = validateStructure(officialSampleProfile);
|
|
89
|
+
const capIssues = issues.filter(i => i.code === 'UCP_INVALID_CAP' || i.code === 'UCP_INVALID_CAPABILITY');
|
|
90
|
+
|
|
91
|
+
expect(capIssues).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should reject string-only capability definitions', () => {
|
|
95
|
+
const issues = validateStructure(nonCompliantProfile);
|
|
96
|
+
const capIssues = issues.filter(i =>
|
|
97
|
+
i.code === 'UCP_INVALID_CAP' || i.code === 'UCP_INVALID_CAPABILITY' || i.code === 'UCP_MISSING_CAPABILITIES'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(capIssues.length).toBeGreaterThan(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Capability Extensions', () => {
|
|
105
|
+
it('should validate extends references in official sample', () => {
|
|
106
|
+
const issues = validateRules(officialSampleProfile);
|
|
107
|
+
const extIssues = issues.filter(i => i.code === 'UCP_ORPHAN_EXT');
|
|
108
|
+
|
|
109
|
+
// Official sample has valid extension chain:
|
|
110
|
+
// order extends checkout, fulfillment extends order
|
|
111
|
+
expect(extIssues).toHaveLength(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Signing Keys', () => {
|
|
116
|
+
it('should accept signing_keys when order capability is present', () => {
|
|
117
|
+
const issues = validateRules(officialSampleProfile);
|
|
118
|
+
const keyIssues = issues.filter(i => i.code === 'UCP_MISSING_KEYS' || i.code === 'UCP_MISSING_SIGNING_KEYS');
|
|
119
|
+
|
|
120
|
+
expect(keyIssues).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should require signing_keys when order capability is present', () => {
|
|
124
|
+
// Create a profile with order but no signing_keys
|
|
125
|
+
const profileWithOrder = {
|
|
126
|
+
ucp: {
|
|
127
|
+
version: '2026-01-11',
|
|
128
|
+
services: {
|
|
129
|
+
'dev.ucp.shopping': {
|
|
130
|
+
version: '2026-01-11',
|
|
131
|
+
spec: 'https://ucp.dev/specs/shopping',
|
|
132
|
+
rest: { endpoint: 'https://example.com/api' }
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
capabilities: [
|
|
136
|
+
{
|
|
137
|
+
name: 'dev.ucp.shopping.order',
|
|
138
|
+
version: '2026-01-11',
|
|
139
|
+
spec: 'https://ucp.dev/specs/shopping/order',
|
|
140
|
+
schema: 'https://ucp.dev/schemas/shopping/order.json'
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const issues = validateRules(profileWithOrder);
|
|
147
|
+
const keyIssues = issues.filter(i => i.code === 'UCP_MISSING_KEYS' || i.code === 'UCP_MISSING_SIGNING_KEYS');
|
|
148
|
+
|
|
149
|
+
expect(keyIssues.length).toBeGreaterThan(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Namespace Binding', () => {
|
|
154
|
+
it('should enforce dev.ucp.* spec URL domain', () => {
|
|
155
|
+
const profileWithWrongSpec = {
|
|
156
|
+
ucp: {
|
|
157
|
+
version: '2026-01-11',
|
|
158
|
+
services: {
|
|
159
|
+
'dev.ucp.shopping': {
|
|
160
|
+
version: '2026-01-11',
|
|
161
|
+
spec: 'https://ucp.dev/specs/shopping',
|
|
162
|
+
rest: { endpoint: 'https://example.com/api' }
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
capabilities: [
|
|
166
|
+
{
|
|
167
|
+
name: 'dev.ucp.shopping.checkout',
|
|
168
|
+
version: '2026-01-11',
|
|
169
|
+
spec: 'https://wrong-domain.com/spec', // Wrong domain
|
|
170
|
+
schema: 'https://ucp.dev/schemas/shopping/checkout.json'
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const issues = validateRules(profileWithWrongSpec);
|
|
177
|
+
const nsIssues = issues.filter(i => i.code === 'UCP_NS_ORIGIN_MISMATCH');
|
|
178
|
+
|
|
179
|
+
expect(nsIssues.length).toBeGreaterThan(0);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Profile Compliance Summary', () => {
|
|
185
|
+
it('official sample should be fully compliant', () => {
|
|
186
|
+
const structuralIssues = validateStructure(officialSampleProfile);
|
|
187
|
+
const rulesIssues = validateRules(officialSampleProfile);
|
|
188
|
+
const allIssues = [...structuralIssues, ...rulesIssues];
|
|
189
|
+
|
|
190
|
+
const errors = allIssues.filter(i => i.severity === 'error');
|
|
191
|
+
const warnings = allIssues.filter(i => i.severity === 'warn');
|
|
192
|
+
|
|
193
|
+
expect(errors).toHaveLength(0);
|
|
194
|
+
// May have some warnings, that's okay
|
|
195
|
+
console.log(`Official sample: ${errors.length} errors, ${warnings.length} warnings`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('non-compliant sample should have multiple errors', () => {
|
|
199
|
+
const structuralIssues = validateStructure(nonCompliantProfile);
|
|
200
|
+
const allIssues = [...structuralIssues];
|
|
201
|
+
|
|
202
|
+
const errors = allIssues.filter(i => i.severity === 'error');
|
|
203
|
+
|
|
204
|
+
// Non-compliant profile should have errors for:
|
|
205
|
+
// - Invalid version format
|
|
206
|
+
// - Services as strings instead of objects
|
|
207
|
+
// - Capabilities as strings instead of objects
|
|
208
|
+
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
209
|
+
console.log(`Non-compliant sample: ${errors.length} errors`);
|
|
210
|
+
});
|
|
211
|
+
});
|