@deneb-ui/cli 2.0.88 → 2.0.90
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/bin/index.js +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/common/template-visual-edit-contract.ts +4 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { canonicalField, fieldRef, isValidCanonicalPath, validatePathAlignment } = require('../canonical-paths.cjs');
|
|
6
|
+
|
|
7
|
+
test('Canonical Field: builds canonical fields and extracts segments', () => {
|
|
8
|
+
const field = canonicalField('home.hero.title');
|
|
9
|
+
assert.equal(field.path, 'home.hero.title');
|
|
10
|
+
assert.equal(field.scope, 'home');
|
|
11
|
+
assert.equal(field.section, 'hero');
|
|
12
|
+
assert.equal(field.fieldName, 'title');
|
|
13
|
+
assert.equal(field.previewPath, 'home.hero.title');
|
|
14
|
+
assert.equal(field.previewMarker, 'data-preview-field-path="home.hero.title"');
|
|
15
|
+
assert.equal(field.runtimePath, 'siteData.content.home.hero.title');
|
|
16
|
+
assert.equal(field.manifestSchemaPath, 'home.hero.title');
|
|
17
|
+
assert.equal(field.diagnosticsId, 'field:home.hero.title');
|
|
18
|
+
assert.equal(field.toSetterCode('val'), 'siteData.content.home.hero.title = val;');
|
|
19
|
+
assert.equal(field.validate().valid, true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('Canonical Field: handles collection child derivations and paths', () => {
|
|
23
|
+
const col = canonicalField('home.categories');
|
|
24
|
+
const item = col.toCollectionItem(0);
|
|
25
|
+
assert.equal(item.path, 'home.categories[0]');
|
|
26
|
+
assert.equal(item.isCollection, true);
|
|
27
|
+
|
|
28
|
+
const nestedChild = item.toCollectionChild('products', 1);
|
|
29
|
+
assert.equal(nestedChild.path, 'home.categories[0].products[1]');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Canonical Field: generates recast AST for getter, fallback binding, and preview attributes', () => {
|
|
33
|
+
const field = canonicalField('home.story.title');
|
|
34
|
+
const getterAst = field.toGetterAst();
|
|
35
|
+
assert.equal(getterAst.type, 'OptionalMemberExpression');
|
|
36
|
+
|
|
37
|
+
const bindingAst = field.toBindingAst('Our Story');
|
|
38
|
+
assert.equal(bindingAst.type, 'LogicalExpression');
|
|
39
|
+
|
|
40
|
+
const attrAst = field.toPreviewAttrAst();
|
|
41
|
+
assert.equal(attrAst.name.name, 'data-preview-field-path');
|
|
42
|
+
assert.equal(attrAst.value.value, 'home.story.title');
|
|
43
|
+
|
|
44
|
+
const styleAttrAst = field.toPreviewStyleTargetAst();
|
|
45
|
+
assert.equal(styleAttrAst.name.name, 'data-preview-style-target');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Canonical Field: validates path syntax and getter-to-marker alignment', () => {
|
|
49
|
+
assert.ok(isValidCanonicalPath('home.hero.title'));
|
|
50
|
+
assert.ok(isValidCanonicalPath('common.header.logoText'));
|
|
51
|
+
assert.ok(isValidCanonicalPath('home.categories[0].products[${index}].title'));
|
|
52
|
+
assert.ok(!isValidCanonicalPath(''));
|
|
53
|
+
assert.ok(!isValidCanonicalPath('invalid path with spaces'));
|
|
54
|
+
|
|
55
|
+
assert.ok(validatePathAlignment('siteData?.content?.home?.hero?.title', 'data-preview-field-path="home.hero.title"'));
|
|
56
|
+
assert.ok(!validatePathAlignment('siteData?.content?.about?.title', 'data-preview-field-path="home.hero.title"'));
|
|
57
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const {
|
|
6
|
+
DATA_CLASSIFICATION,
|
|
7
|
+
classifyDataCandidate,
|
|
8
|
+
isEditableClassification,
|
|
9
|
+
isPlatformControlledPath,
|
|
10
|
+
} = require('../data-classification.cjs');
|
|
11
|
+
|
|
12
|
+
test('Data Classification: correctly assigns all 7 data tiers', () => {
|
|
13
|
+
// 1. CONTENT
|
|
14
|
+
assert.equal(classifyDataCandidate({ tag: 'h1', kind: 'text', value: 'Fresh Roasted Beans' }).classification, DATA_CLASSIFICATION.CONTENT);
|
|
15
|
+
|
|
16
|
+
// 2. COMMERCE_CONTENT
|
|
17
|
+
assert.equal(classifyDataCandidate({ tag: 'h2', kind: 'text', value: 'Espresso Blend', propName: 'productTitle' }).classification, DATA_CLASSIFICATION.COMMERCE_CONTENT);
|
|
18
|
+
|
|
19
|
+
// 3. PLATFORM_CONTROLLED
|
|
20
|
+
assert.equal(classifyDataCandidate({ tag: 'span', kind: 'text', value: 'USD', field: 'common.currency' }).classification, DATA_CLASSIFICATION.PLATFORM_CONTROLLED);
|
|
21
|
+
assert.equal(classifyDataCandidate({ tag: 'span', kind: 'text', value: 'prod-123', propName: 'productId' }).classification, DATA_CLASSIFICATION.PLATFORM_CONTROLLED);
|
|
22
|
+
|
|
23
|
+
// 4. INTERACTION_STATE
|
|
24
|
+
assert.equal(classifyDataCandidate({ tag: 'div', kind: 'text', propName: 'isOpen', value: 'true' }).classification, DATA_CLASSIFICATION.INTERACTION_STATE);
|
|
25
|
+
assert.equal(classifyDataCandidate({ tag: 'div', kind: 'text', propName: 'activeTab', value: 'reviews' }).classification, DATA_CLASSIFICATION.INTERACTION_STATE);
|
|
26
|
+
|
|
27
|
+
// 5. COMPUTED_DATA
|
|
28
|
+
assert.equal(classifyDataCandidate({ tag: 'span', kind: 'text', value: 'formatPrice(item.price * qty)' }).classification, DATA_CLASSIFICATION.COMPUTED_DATA);
|
|
29
|
+
|
|
30
|
+
// 6. RUNTIME_DATA
|
|
31
|
+
assert.equal(classifyDataCandidate({ tag: 'span', kind: 'text', value: 'user.email' }).classification, DATA_CLASSIFICATION.RUNTIME_DATA);
|
|
32
|
+
assert.equal(classifyDataCandidate({ tag: 'span', kind: 'text', value: 'cart.total' }).classification, DATA_CLASSIFICATION.RUNTIME_DATA);
|
|
33
|
+
|
|
34
|
+
// 7. DECORATIVE
|
|
35
|
+
assert.equal(classifyDataCandidate({ tag: 'span', kind: 'text', value: '•' }).classification, DATA_CLASSIFICATION.DECORATIVE);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Data Classification: recognizes editable vs non-editable classifications', () => {
|
|
39
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.CONTENT), true);
|
|
40
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.COMMERCE_CONTENT), true);
|
|
41
|
+
|
|
42
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.PLATFORM_CONTROLLED), false);
|
|
43
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.RUNTIME_DATA), false);
|
|
44
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.COMPUTED_DATA), false);
|
|
45
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.DECORATIVE), false);
|
|
46
|
+
assert.equal(isEditableClassification(DATA_CLASSIFICATION.INTERACTION_STATE), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('Data Classification: identifies platform controlled paths accurately', () => {
|
|
50
|
+
assert.equal(isPlatformControlledPath('common.currency'), true);
|
|
51
|
+
assert.equal(isPlatformControlledPath('products[0].id'), true);
|
|
52
|
+
assert.equal(isPlatformControlledPath('categories[2].slug'), true);
|
|
53
|
+
assert.equal(isPlatformControlledPath('home.hero.title'), false);
|
|
54
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const {
|
|
9
|
+
KNOWN_STOREFRONTS,
|
|
10
|
+
resolveWorkspaceStorefrontsDir,
|
|
11
|
+
auditStorefrontTemplate,
|
|
12
|
+
verifyStorefrontCorpus,
|
|
13
|
+
} = require('../corpus-verifier.cjs');
|
|
14
|
+
|
|
15
|
+
function createMockCompliantProject() {
|
|
16
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-corpus-compliant-'));
|
|
17
|
+
const appDir = path.join(tempDir, 'src', 'app');
|
|
18
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
19
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
20
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'sample', dependencies: { next: '15.0.0' } }));
|
|
22
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
23
|
+
framework: 'nextjs-static-export',
|
|
24
|
+
version: 2,
|
|
25
|
+
arcVersion: '2.1.0',
|
|
26
|
+
schemaVersion: 2,
|
|
27
|
+
siteDataFile: 'src/data/site-data.json',
|
|
28
|
+
pages: [{ id: 'home', label: 'Home', route: '/', required: true }],
|
|
29
|
+
editorSchema: {
|
|
30
|
+
version: 1,
|
|
31
|
+
sections: [
|
|
32
|
+
{ id: 'home', path: 'home', type: 'object', label: 'Home', fields: [{ key: 'heroTitle', type: 'text' }] }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
visualEditing: {
|
|
36
|
+
contractVersion: 1,
|
|
37
|
+
mode: 'strict',
|
|
38
|
+
controlOnlyPaths: []
|
|
39
|
+
}
|
|
40
|
+
}, null, 2));
|
|
41
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({
|
|
42
|
+
content: {
|
|
43
|
+
home: { heroTitle: 'Welcome' }
|
|
44
|
+
}
|
|
45
|
+
}, null, 2));
|
|
46
|
+
fs.writeFileSync(path.join(appDir, 'layout.tsx'), 'export default function RootLayout({ children }: any) { return <html><body>{children}</body></html>; }');
|
|
47
|
+
fs.writeFileSync(path.join(appDir, 'page.tsx'), 'export default function HomePage() { return <main data-preview-page-id="home"><h1 data-preview-field-path="home.heroTitle">Welcome</h1></main>; }');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
dir: tempDir,
|
|
51
|
+
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createMockBlockedProject() {
|
|
56
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-corpus-blocked-'));
|
|
57
|
+
const appDir = path.join(tempDir, 'src', 'app');
|
|
58
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
59
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
60
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
61
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'blocked-sample', dependencies: { next: '15.0.0' } }));
|
|
62
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
63
|
+
framework: 'nextjs-static-export',
|
|
64
|
+
version: 2,
|
|
65
|
+
arcVersion: '2.1.0',
|
|
66
|
+
schemaVersion: 2,
|
|
67
|
+
siteDataFile: 'src/data/site-data.json',
|
|
68
|
+
pages: [{ id: 'home', label: 'Home', route: '/', required: true }],
|
|
69
|
+
editorSchema: {
|
|
70
|
+
sections: [
|
|
71
|
+
{ id: 'home', path: 'home', type: 'object', fields: [{ key: 'unmappedField', type: 'currency' }] }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({
|
|
76
|
+
content: { home: { unmappedField: 'Hello' } }
|
|
77
|
+
}));
|
|
78
|
+
fs.writeFileSync(path.join(appDir, 'page.tsx'), 'export default function Page() { return <div>No Marker</div>; }');
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
dir: tempDir,
|
|
82
|
+
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('Corpus Verifier: audits storefront and verifies 100% pass across all 12 gates', () => {
|
|
87
|
+
const baseDir = resolveWorkspaceStorefrontsDir();
|
|
88
|
+
const coffeeDir = path.join(baseDir, 'coffee');
|
|
89
|
+
const hasRealCoffee = fs.existsSync(coffeeDir) && fs.existsSync(path.join(coffeeDir, 'fivora-template.json'));
|
|
90
|
+
|
|
91
|
+
let targetDir = coffeeDir;
|
|
92
|
+
let cleanup = null;
|
|
93
|
+
if (!hasRealCoffee) {
|
|
94
|
+
const fixture = createMockCompliantProject();
|
|
95
|
+
targetDir = fixture.dir;
|
|
96
|
+
cleanup = fixture.cleanup;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = auditStorefrontTemplate(targetDir, { templateId: 'coffee', templateName: 'Coffee Shop' });
|
|
101
|
+
assert.equal(result.validStructure, true);
|
|
102
|
+
assert.equal(result.fivoraContract.passed, true);
|
|
103
|
+
assert.equal(result.fivoraContract.violationCount, 0);
|
|
104
|
+
assert.equal(result.runtimeEditability.passed, true);
|
|
105
|
+
assert.equal(result.collectionOperations.passed, true);
|
|
106
|
+
assert.equal(result.acceptanceGates.passed, true);
|
|
107
|
+
assert.equal(result.acceptanceGates.status, 'CONVERSION_PASSED');
|
|
108
|
+
assert.equal(result.passed, true);
|
|
109
|
+
} finally {
|
|
110
|
+
if (cleanup) cleanup();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('Corpus Verifier: blocks conversions with contract violations and refuses false success', () => {
|
|
115
|
+
const fixture = createMockBlockedProject();
|
|
116
|
+
try {
|
|
117
|
+
const result = auditStorefrontTemplate(fixture.dir, { templateId: 'blocked', templateName: 'Blocked Template' });
|
|
118
|
+
assert.equal(result.fivoraContract.passed, false);
|
|
119
|
+
assert.ok(result.fivoraContract.violationCount > 0);
|
|
120
|
+
assert.equal(result.acceptanceGates.status, 'CONVERSION_FAILED');
|
|
121
|
+
assert.equal(result.passed, false);
|
|
122
|
+
} finally {
|
|
123
|
+
fixture.cleanup();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('Corpus Verifier: executes corpus verification across all known storefronts', () => {
|
|
128
|
+
const baseDir = resolveWorkspaceStorefrontsDir();
|
|
129
|
+
const hasWorkspaceCorpus = fs.existsSync(path.join(baseDir, 'coffee')) && fs.existsSync(path.join(baseDir, 'car-sale'));
|
|
130
|
+
|
|
131
|
+
const report = verifyStorefrontCorpus();
|
|
132
|
+
assert.equal(report.totalTemplates, 6);
|
|
133
|
+
|
|
134
|
+
if (hasWorkspaceCorpus) {
|
|
135
|
+
assert.equal(report.passedTemplates, 6);
|
|
136
|
+
assert.equal(report.failedTemplates, 0);
|
|
137
|
+
assert.equal(report.corpusSuccessRate, 100);
|
|
138
|
+
assert.equal(report.allTemplatesPassed, true);
|
|
139
|
+
} else {
|
|
140
|
+
assert.ok(report.results.length === 6);
|
|
141
|
+
assert.equal(report.failedTemplates, 6);
|
|
142
|
+
assert.equal(report.allTemplatesPassed, false);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const {
|
|
9
|
+
analyzeBlockedProject,
|
|
10
|
+
formatBlockedExplanationTerminal,
|
|
11
|
+
} = require('../explain-blocked.cjs');
|
|
12
|
+
const { resolveWorkspaceStorefrontsDir } = require('../corpus-verifier.cjs');
|
|
13
|
+
|
|
14
|
+
function createMockCompliantProject() {
|
|
15
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-compliant-'));
|
|
16
|
+
const appDir = path.join(tempDir, 'src', 'app');
|
|
17
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
18
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
19
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'sample', dependencies: { next: '15.0.0' } }));
|
|
21
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
22
|
+
framework: 'nextjs-static-export',
|
|
23
|
+
version: 2,
|
|
24
|
+
arcVersion: '2.1.0',
|
|
25
|
+
schemaVersion: 2,
|
|
26
|
+
siteDataFile: 'src/data/site-data.json',
|
|
27
|
+
pages: [{ id: 'home', label: 'Home', route: '/', required: true }],
|
|
28
|
+
editorSchema: {
|
|
29
|
+
version: 1,
|
|
30
|
+
sections: [
|
|
31
|
+
{ id: 'home', path: 'home', type: 'object', label: 'Home', fields: [{ key: 'heroTitle', type: 'text' }] }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
visualEditing: {
|
|
35
|
+
contractVersion: 1,
|
|
36
|
+
mode: 'strict',
|
|
37
|
+
controlOnlyPaths: []
|
|
38
|
+
}
|
|
39
|
+
}, null, 2));
|
|
40
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({
|
|
41
|
+
content: {
|
|
42
|
+
home: { heroTitle: 'Welcome' }
|
|
43
|
+
}
|
|
44
|
+
}, null, 2));
|
|
45
|
+
fs.writeFileSync(path.join(appDir, 'layout.tsx'), 'export default function RootLayout({ children }: any) { return <html><body>{children}</body></html>; }');
|
|
46
|
+
fs.writeFileSync(path.join(appDir, 'page.tsx'), 'export default function HomePage() { return <main data-preview-page-id="home"><h1 data-preview-field-path="home.heroTitle">Welcome</h1></main>; }');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
dir: tempDir,
|
|
50
|
+
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('Explain Blocked: identifies clean status for compliant storefront (coffee)', () => {
|
|
55
|
+
const baseDir = resolveWorkspaceStorefrontsDir();
|
|
56
|
+
const coffeeDir = path.join(baseDir, 'coffee');
|
|
57
|
+
const hasRealCoffee = fs.existsSync(coffeeDir) && fs.existsSync(path.join(coffeeDir, 'fivora-template.json'));
|
|
58
|
+
|
|
59
|
+
let compliantDir = coffeeDir;
|
|
60
|
+
let compliantCleanup = null;
|
|
61
|
+
if (!hasRealCoffee) {
|
|
62
|
+
const fixture = createMockCompliantProject();
|
|
63
|
+
compliantDir = fixture.dir;
|
|
64
|
+
compliantCleanup = fixture.cleanup;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const report = analyzeBlockedProject(compliantDir);
|
|
69
|
+
assert.equal(report.blocked, false);
|
|
70
|
+
assert.equal(report.totalBlockingIssues, 0);
|
|
71
|
+
assert.equal(report.findings.length, 0);
|
|
72
|
+
assert.equal(report.summary.contractPassed, true);
|
|
73
|
+
} finally {
|
|
74
|
+
if (compliantCleanup) compliantCleanup();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('Explain Blocked: pinpoints exact unmapped field paths and locations for blocked storefront', () => {
|
|
79
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-explain-unmapped-'));
|
|
80
|
+
const compDir = path.join(tempDir, 'src', 'components');
|
|
81
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
82
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
83
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
84
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({
|
|
85
|
+
content: {
|
|
86
|
+
home: {
|
|
87
|
+
missingField: 'Unmapped Content',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
92
|
+
framework: 'nextjs-static-export',
|
|
93
|
+
siteDataFile: 'src/data/site-data.json',
|
|
94
|
+
editorSchema: {
|
|
95
|
+
sections: [{ id: 'home', path: 'home', type: 'object', fields: [{ key: 'missingField', type: 'text' }] }],
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
fs.writeFileSync(path.join(compDir, 'Showcase.tsx'), 'export function Showcase() {\n return <div><span>No Marker</span></div>;\n}');
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const report = analyzeBlockedProject(tempDir);
|
|
102
|
+
assert.equal(report.blocked, true);
|
|
103
|
+
assert.equal(report.totalBlockingIssues, 1);
|
|
104
|
+
assert.equal(report.summary.contractPassed, false);
|
|
105
|
+
|
|
106
|
+
const first = report.findings[0];
|
|
107
|
+
assert.equal(first.ruleId, 'FIVORA_UNMAPPED_FIELD');
|
|
108
|
+
assert.ok(first.file.includes('Showcase.tsx'));
|
|
109
|
+
assert.ok(first.line >= 1);
|
|
110
|
+
assert.ok(first.remediation.includes('data-preview-field-path') || first.remediation.includes('controlOnlyPaths'));
|
|
111
|
+
} finally {
|
|
112
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Explain Blocked: identifies unsupported schema type and duplicate sections for blocked storefront', () => {
|
|
117
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-explain-schema-'));
|
|
118
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
119
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({ content: {} }));
|
|
121
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
122
|
+
framework: 'nextjs-static-export',
|
|
123
|
+
siteDataFile: 'src/data/site-data.json',
|
|
124
|
+
editorSchema: {
|
|
125
|
+
sections: [
|
|
126
|
+
{
|
|
127
|
+
id: 'shop',
|
|
128
|
+
path: 'shop',
|
|
129
|
+
type: 'object',
|
|
130
|
+
fields: [
|
|
131
|
+
{ key: 'price', type: 'currency', label: 'Price' },
|
|
132
|
+
{ key: 'dup', type: 'text', label: 'Dup' },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'shop_dup',
|
|
137
|
+
path: 'shop',
|
|
138
|
+
type: 'object',
|
|
139
|
+
fields: [
|
|
140
|
+
{ key: 'dup', type: 'text', label: 'Dup' },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const report = analyzeBlockedProject(tempDir);
|
|
149
|
+
assert.equal(report.blocked, true);
|
|
150
|
+
assert.ok(report.totalBlockingIssues >= 2);
|
|
151
|
+
|
|
152
|
+
const hasUnsupportedType = report.findings.some((f) => f.ruleId === 'SCHEMA_UNSUPPORTED_TYPE');
|
|
153
|
+
assert.equal(hasUnsupportedType, true);
|
|
154
|
+
|
|
155
|
+
const unsupported = report.findings.find((f) => f.ruleId === 'SCHEMA_UNSUPPORTED_TYPE');
|
|
156
|
+
assert.ok(unsupported.message.includes('currency'));
|
|
157
|
+
assert.ok(unsupported.remediation.includes('Replace type \'currency\' with \'number\' or \'text\''));
|
|
158
|
+
} finally {
|
|
159
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('Explain Blocked: formats terminal UI with clear problem and fix descriptions', () => {
|
|
164
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-explain-ui-'));
|
|
165
|
+
const compDir = path.join(tempDir, 'src', 'components');
|
|
166
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
167
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
168
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
169
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({
|
|
170
|
+
content: { home: { testField: 'Hello' } },
|
|
171
|
+
}));
|
|
172
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
173
|
+
framework: 'nextjs-static-export',
|
|
174
|
+
siteDataFile: 'src/data/site-data.json',
|
|
175
|
+
editorSchema: {
|
|
176
|
+
sections: [{ id: 'home', path: 'home', type: 'object', fields: [{ key: 'testField', type: 'text' }] }],
|
|
177
|
+
},
|
|
178
|
+
}));
|
|
179
|
+
fs.writeFileSync(path.join(compDir, 'Comp.tsx'), 'export function Comp() { return <div>Unbound</div>; }');
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const report = analyzeBlockedProject(tempDir);
|
|
183
|
+
const terminal = formatBlockedExplanationTerminal(report);
|
|
184
|
+
|
|
185
|
+
assert.ok(terminal.includes('DENEB CONVERSION BLOCKED'));
|
|
186
|
+
assert.ok(terminal.includes('Comp.tsx'));
|
|
187
|
+
assert.ok(terminal.includes('Fix:'));
|
|
188
|
+
assert.ok(terminal.includes('npx @deneb-ui/cli init'));
|
|
189
|
+
} finally {
|
|
190
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const {
|
|
6
|
+
MUTATION_TYPES,
|
|
7
|
+
DEFAULT_SAMPLE_COMPONENT,
|
|
8
|
+
applyMutation,
|
|
9
|
+
generateFuzzCorpus,
|
|
10
|
+
testMutationResilience,
|
|
11
|
+
runFuzzHarness,
|
|
12
|
+
} = require('../fuzz-engine.cjs');
|
|
13
|
+
|
|
14
|
+
test('Fuzz Engine: applies individual AST mutations cleanly', () => {
|
|
15
|
+
// 1. PROP_RENAME
|
|
16
|
+
const renameResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'PROP_RENAME');
|
|
17
|
+
assert.equal(renameResult.applied, true);
|
|
18
|
+
assert.ok(renameResult.mutatedCode.includes('fuzzed_') || renameResult.mutatedCode !== DEFAULT_SAMPLE_COMPONENT);
|
|
19
|
+
|
|
20
|
+
// 2. WRAP_FRAGMENT
|
|
21
|
+
const fragmentResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'WRAP_FRAGMENT');
|
|
22
|
+
assert.equal(fragmentResult.applied, true);
|
|
23
|
+
assert.ok(fragmentResult.mutatedCode.includes('<>') && fragmentResult.mutatedCode.includes('</>'));
|
|
24
|
+
|
|
25
|
+
// 3. WRAP_DIV
|
|
26
|
+
const divResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'WRAP_DIV');
|
|
27
|
+
assert.equal(divResult.applied, true);
|
|
28
|
+
assert.ok(divResult.mutatedCode.includes('fuzzed-container-wrapper'));
|
|
29
|
+
|
|
30
|
+
// 4. SPREAD_PROPS
|
|
31
|
+
const spreadResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'SPREAD_PROPS');
|
|
32
|
+
assert.equal(spreadResult.applied, true);
|
|
33
|
+
assert.ok(spreadResult.mutatedCode.includes('...fuzzedProps'));
|
|
34
|
+
|
|
35
|
+
// 5. CONDITIONAL_TERNARY
|
|
36
|
+
const ternaryResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'CONDITIONAL_TERNARY');
|
|
37
|
+
assert.equal(ternaryResult.applied, true);
|
|
38
|
+
assert.ok(ternaryResult.mutatedCode.includes('isFuzzedConditionActive'));
|
|
39
|
+
|
|
40
|
+
// 6. CONDITIONAL_LOGICAL
|
|
41
|
+
const logicalResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'CONDITIONAL_LOGICAL');
|
|
42
|
+
assert.equal(logicalResult.applied, true);
|
|
43
|
+
assert.ok(logicalResult.mutatedCode.includes('showFuzzedContent &&'));
|
|
44
|
+
|
|
45
|
+
// 7. INJECT_OPTIONAL_CHAIN
|
|
46
|
+
const optionalResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'INJECT_OPTIONAL_CHAIN');
|
|
47
|
+
assert.equal(optionalResult.applied, true);
|
|
48
|
+
assert.ok(optionalResult.mutatedCode.includes('?.'));
|
|
49
|
+
|
|
50
|
+
// 8. NEST_MEMBER_COLLECTION
|
|
51
|
+
const nestResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'NEST_MEMBER_COLLECTION');
|
|
52
|
+
assert.equal(nestResult.applied, true);
|
|
53
|
+
assert.ok(nestResult.mutatedCode.includes('.map('));
|
|
54
|
+
|
|
55
|
+
// 9. CORRUPT_SYNTAX
|
|
56
|
+
const corruptResult = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'CORRUPT_SYNTAX');
|
|
57
|
+
assert.equal(corruptResult.applied, true);
|
|
58
|
+
assert.ok(corruptResult.mutatedCode.includes('MALFORMED_SYNTAX'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Fuzz Engine: generateFuzzCorpus creates a full mutation suite', () => {
|
|
62
|
+
const corpus = generateFuzzCorpus(DEFAULT_SAMPLE_COMPONENT);
|
|
63
|
+
assert.equal(corpus.length, MUTATION_TYPES.length);
|
|
64
|
+
|
|
65
|
+
for (const item of corpus) {
|
|
66
|
+
assert.equal(item.applied, true);
|
|
67
|
+
assert.notEqual(item.mutatedCode, DEFAULT_SAMPLE_COMPONENT);
|
|
68
|
+
assert.ok(MUTATION_TYPES.includes(item.mutationType));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Fuzz Engine: cleanly catches corrupt syntax without unhandled crash', () => {
|
|
73
|
+
const corrupt = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'CORRUPT_SYNTAX');
|
|
74
|
+
const result = testMutationResilience(corrupt.mutatedCode, 'CORRUPT_SYNTAX');
|
|
75
|
+
|
|
76
|
+
assert.equal(result.crashed, false);
|
|
77
|
+
assert.equal(result.resilient, true);
|
|
78
|
+
assert.equal(result.status, 'PARSER_REJECTED_CLEANLY');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('Fuzz Engine: verifies compiler resilience across structural mutations', () => {
|
|
82
|
+
// Test spread props resilience
|
|
83
|
+
const spread = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'SPREAD_PROPS');
|
|
84
|
+
const spreadRes = testMutationResilience(spread.mutatedCode, 'SPREAD_PROPS');
|
|
85
|
+
assert.equal(spreadRes.crashed, false);
|
|
86
|
+
assert.equal(spreadRes.resilient, true);
|
|
87
|
+
assert.equal(spreadRes.outputValidSyntax, true);
|
|
88
|
+
|
|
89
|
+
// Test fragment wrapping resilience
|
|
90
|
+
const frag = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'WRAP_FRAGMENT');
|
|
91
|
+
const fragRes = testMutationResilience(frag.mutatedCode, 'WRAP_FRAGMENT');
|
|
92
|
+
assert.equal(fragRes.crashed, false);
|
|
93
|
+
assert.equal(fragRes.resilient, true);
|
|
94
|
+
assert.equal(fragRes.outputValidSyntax, true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('Fuzz Engine: runFuzzHarness executes end-to-end with 100% resilience score', () => {
|
|
98
|
+
const report = runFuzzHarness(DEFAULT_SAMPLE_COMPONENT);
|
|
99
|
+
|
|
100
|
+
assert.equal(report.totalMutations, MUTATION_TYPES.length);
|
|
101
|
+
assert.equal(report.crashedCount, 0, 'Must have zero unhandled crashes');
|
|
102
|
+
assert.equal(report.syntaxErrorsInOutput, 0, 'Must have zero syntax errors in output');
|
|
103
|
+
assert.equal(report.resilienceScore, 100.0, 'Resilience score must be 100%');
|
|
104
|
+
assert.equal(report.allResilient, true);
|
|
105
|
+
assert.ok(report.results.length === MUTATION_TYPES.length);
|
|
106
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const {
|
|
6
|
+
INTERACTIVE_PATTERNS,
|
|
7
|
+
analyzeComponentInteractivity,
|
|
8
|
+
verifyInteractionsSync,
|
|
9
|
+
} = require('../interaction-verifier.cjs');
|
|
10
|
+
|
|
11
|
+
test('Interaction Verifier: detects mobile menu toggles and event handlers', () => {
|
|
12
|
+
const code = `
|
|
13
|
+
export function Navbar() {
|
|
14
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
15
|
+
return (
|
|
16
|
+
<nav>
|
|
17
|
+
<button
|
|
18
|
+
aria-label="Toggle Menu"
|
|
19
|
+
className="hamburger-menu"
|
|
20
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
21
|
+
>
|
|
22
|
+
<span />
|
|
23
|
+
</button>
|
|
24
|
+
</nav>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const report = verifyInteractionsSync({ code, componentName: 'Navbar' });
|
|
30
|
+
assert.equal(report.passed, true);
|
|
31
|
+
assert.equal(report.interactionScore, 100.0);
|
|
32
|
+
|
|
33
|
+
const toggleCheck = report.patterns.find((p) => p.pattern === INTERACTIVE_PATTERNS.NAVBAR_MOBILE_TOGGLE);
|
|
34
|
+
assert.ok(toggleCheck);
|
|
35
|
+
assert.equal(toggleCheck.passed, true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Interaction Verifier: detects accordions, tabs, dialogs, and carousels across files', () => {
|
|
39
|
+
const files = [
|
|
40
|
+
{
|
|
41
|
+
file: 'components/Faq.tsx',
|
|
42
|
+
code: `export function Faq() { return <AccordionTrigger>Question 1</AccordionTrigger>; }`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
file: 'components/ProductTabs.tsx',
|
|
46
|
+
code: `export function ProductTabs() { return <TabsTrigger value="details">Details</TabsTrigger>; }`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
file: 'components/QuickView.tsx',
|
|
50
|
+
code: `export function QuickView() { return <Dialog>Content</Dialog>; }`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
file: 'components/HeroCarousel.tsx',
|
|
54
|
+
code: `export function HeroCarousel() { return <Swiper><SwiperSlide>1</SwiperSlide></Swiper>; }`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
file: 'components/ContactForm.tsx',
|
|
58
|
+
code: `export function ContactForm() { return <form onSubmit={(e) => handleSubmit(e)}><button type="submit">Send</button></form>; }`,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const report = verifyInteractionsSync({ files });
|
|
63
|
+
assert.equal(report.passed, true);
|
|
64
|
+
assert.equal(report.totalTested, 5);
|
|
65
|
+
assert.equal(report.passedTests, 5);
|
|
66
|
+
assert.equal(report.failedTests, 0);
|
|
67
|
+
assert.equal(report.interactionScore, 100.0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('Interaction Verifier: flags missing event handler on interactive trigger', () => {
|
|
71
|
+
const code = `
|
|
72
|
+
export function BrokenNav() {
|
|
73
|
+
return (
|
|
74
|
+
<button aria-label="Toggle Menu" className="nav-toggle">
|
|
75
|
+
<span>Click</span>
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const report = verifyInteractionsSync({ code, componentName: 'BrokenNav' });
|
|
82
|
+
assert.equal(report.passed, false);
|
|
83
|
+
assert.equal(report.failedTests, 1);
|
|
84
|
+
assert.ok(report.patterns[0].reason.includes('Missing onClick handler'));
|
|
85
|
+
});
|